Skip to content


If you have a desktop Tauri project and want to target mobile platforms using the same code, you must configure your Rust crate.

The Rust crate must output a library that will be embedded in the Android and iOS packages. Add the following section to your Cargo.toml file:

crate-type = ["staticlib", "cdylib", "rlib"]

The default entry point for the Rust library is the file. Let’s write a tauri::Builder wrapper that will be reused by both the desktop and mobile targets.

use tauri::App;
mod mobile;
pub use mobile::*;
pub type SetupHook = Box<dyn FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send>;
pub struct AppBuilder {
setup: Option<SetupHook>,
impl AppBuilder {
pub fn new() -> Self {
pub fn setup<F>(mut self, setup: F) -> Self
F: FnOnce(&mut App) -> Result<(), Box<dyn std::error::Error>> + Send + 'static,
pub fn run(self) {
let setup = self.setup;
.setup(move |app| {
if let Some(setup) = setup {
.expect("error while running tauri application");

In this example, the AppBuilder::run function is where you write all the shared logic. Custom mobile/desktop code can be defined in the AppBuilder methods, such as the setup method.

Now let’s create the mobile module that will have the entry point for iOS and Android and consume the shared AppBuilder logic:

fn main() {

The default Tauri project has the binary source code under src-tauri/src/ Let’s change it to use the AppBuilder struct:

all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
pub fn main() {

To develop mobile Tauri applications, your frontend must serve the assets listening on your public network address. The network address can be found using the internal-ip NPM:

Terminal window
npm install --save-dev internal-ip@7.0.0
Terminal window
yarn add -D internal-ip@7.0.0
Terminal window
pnpm add -D internal-ip@7.0.0

Then you need to configure your framework to use the internal IP.

For Vite, you need to change your configuration to be defined using the defineConfig helper with an async closure. Then, resolve the internal IP address and set it to the server object.

import { defineConfig } from 'vite';
import { internalIpV4 } from 'internal-ip';
export default defineConfig(async () => {
const host = await internalIpV4();
/** @type {import('vite').UserConfig} */
const config = {
server: {
host: '', // listen on all addresses
port: 5173,
strictPort: true,
hmr: {
protocol: 'ws',
port: 5183,
return config;

For Next.js, you need to configure the assetPrefix to use the internal IP so the server properly resolves your assets.

const isProd = process.env.NODE_ENV === 'production';
module.exports = async (phase, { defaultConfig }) => {
let internalHost = null;
if (!isProd) {
const { internalIpV4 } = await import('internal-ip');
internalHost = await internalIpV4();
* @type {import('next').NextConfig}
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
// Note: This experimental feature is required to use NextJS Image in SSG mode.
// See for different workarounds.
images: {
unoptimized: true,
assetPrefix: isProd ? null : `http://${internalHost}:3000`,
return nextConfig;

Currently, there is no configuration option to configure Next.js to use the internal IP address, only the CLI allows changing it. So you need to append --hostname $HOST to the beforeDevCommand.

Webpack has a built-in option to use the local IP address as the host for the development server:

export default {
devServer: {
host: 'local-ipv4',

The #[cfg(desktop)] and #[cfg(mobile)] conditional checks can be used to conditionally compile code for each target.
fn do_something() {
println!("Hello from Mobile!");
fn do_something() {
println!("Hello from Desktop!");
fn run() {
if cfg!(mobile) {
println!("Hello from Mobile!");
} else {
println!("Hello from Desktop!");

© 2024 Tauri Contributors. CC-BY / MIT