Here’s how you could structure a single codebase to support both Tauri (desktop) and Capacitor (mobile), leveraging shared logic while accommodating platform-specific needs:
your-app/
├── src/ # Shared frontend code (Nuxt + Ionic + TypeScript)
│ ├── app.vue # Root component
│ ├── composables/ # Shared business logic (e.g., P2P networking)
│ ├── components/ # Shared UI components (Ionic components here)
│ └── assets/ # Shared images/styles
│
├── tauri/ # Tauri-specific configuration (desktop)
│ ├── src/ # Rust backend logic (P2P encryption, file I/O)
│ └── tauri.conf.json # Tauri config (bundle IDs, desktop settings)
│
├── capacitor/ # Capacitor-specific config (mobile)
│ ├── android/ # Android platform code
│ ├── ios/ # iOS platform code
│ └── capacitor.config.ts # Mobile-specific settings
│
├── .nuxt/ # Nuxt auto-generated files
├── nuxt.config.ts # Nuxt config (SSR disabled, shared plugins)
├── package.json # Shared dependencies + scripts
└── tsconfig.json # Shared TypeScript config
- Use Ionic’s web components for cross-platform UI (e.g.,
<ion-button>
,<ion-list>
). They render consistently in both Tauri’s WebView and Capacitor’s native WebView. - Conditionally apply platform-specific styles using Ionic’s platform utilities:
// Shared component import { isPlatform } from '@ionic/vue'; if (isPlatform('desktop')) { // Tauri-specific UI tweaks (e.g., hover states) } else if (isPlatform('mobile')) { // Mobile-specific gestures (e.g., swipe-to-refresh) }
- Shared Interface: Define a TypeScript interface for platform-agnostic APIs (e.g., file system access, P2P networking):
// src/composables/p2p.ts interface P2PInterface { sendData(data: string): Promise<void>; receiveData(): Promise<string>; }
- Tauri Implementation (Rust backend):
// tauri/src/p2p.rs #[tauri::command] fn send_data(data: String) { // Rust logic for desktop P2P (e.g., libp2p) }
- Capacitor Implementation (TypeScript + Plugins):
// capacitor/src/p2p.ts import { Network } from '@capacitor/network'; export const sendData = async (data: string) => { await Network.connect(); // Use Capacitor’s network plugin // Mobile P2P logic (e.g., WebRTC) };
- Use compile-time flags to exclude platform-specific code during builds:
// package.json { "scripts": { "build:desktop": "NUXT_PLATFORM=tauri nuxt build", "build:mobile": "NUXT_PLATFORM=capacitor nuxt build" } }
- In code:
if (process.env.NUXT_PLATFORM === 'tauri') { // Load Tauri-specific modules } else { // Load Capacitor-specific modules }
-
Shared Frontend:
- Nuxt compiles the app into static files (
dist/
). - Ionic components are tree-shaken for desktop/mobile targets.
- Nuxt compiles the app into static files (
-
Desktop (Tauri):
npm run build:desktop cd tauri && tauri build
- Bundles the Nuxt
dist/
with Tauri’s Rust backend.
- Bundles the Nuxt
-
Mobile (Capacitor):
npm run build:mobile npx cap sync android && npx cap sync ios
- Syncs the Nuxt
dist/
with Capacitor’s native projects.
- Syncs the Nuxt
Challenge | Solution |
---|---|
Native Feature Parity | Use dependency injection to swap Tauri (Rust) and Capacitor (TypeScript) implementations. |
UI Consistency | Test Ionic components in both WebViews; override styles with CSS variables. |
Build Complexity | Use a monorepo tool (e.g., Turborepo) to manage parallel builds. |
Plugin Mismatches | Wrap Tauri/Capacitor plugins in a shared interface (e.g., StorageService ). |
Pros:
- ✅ Maximized code reuse: 80-90% shared code (UI, business logic).
- ✅ Desktop-first + mobile fallback: Optimize for Tauri’s strengths while using Capacitor for mature mobile support.
- ✅ Flexible native access: Combine Rust (desktop) and Capacitor plugins (mobile).
Cons:
- ❌ Double maintenance: Two native layers (Rust for Tauri, TypeScript for Capacitor).
- ❌ Complex debugging: Requires testing on 5 platforms (3 desktop + 2 mobile).
- ❌ Build tooling overhead: Separate configs for Tauri/Capacitor.
- You need desktop-grade performance (Tauri’s Rust backend) but native mobile UX (Capacitor).
- Your team can manage Rust (Tauri) and TypeScript (Capacitor) codebases.
- The app’s core logic (e.g., P2P networking) can be abstracted from platform specifics.
For a streamlined workflow, consider using Turborepo or Nx to orchestrate shared code and platform-specific builds.
Using Turborepo in this scenario would streamline your monorepo setup, allowing you to manage shared code, platform-specific builds (Tauri/Capacitor), and cross-platform dependencies efficiently. Here’s how you’d structure and configure it:
your-app/
├── apps/
│ ├── tauri-desktop/ # Tauri app (macOS, Windows, Linux)
│ │ ├── src-tauri/ # Rust backend
│ │ ├── public/ # Web assets (shared via symlink)
│ │ └── package.json # Tauri-specific deps
│ │
│ └── capacitor-mobile/ # Capacitor app (iOS/Android)
│ ├── android/ # Android platform code
│ ├── ios/ # iOS platform code
│ └── package.json # Capacitor-specific deps
│
├── packages/
│ ├── ui/ # Shared Ionic UI components (Vue)
│ │ ├── components/ # <ion-button>, <ion-list>, etc.
│ │ └── package.json # Shared UI dependencies
│ │
│ ├── core/ # Shared business logic (TypeScript)
│ │ ├── p2p/ # P2P networking abstractions
│ │ ├── storage/ # Data storage interface
│ │ └── package.json
│ │
│ └── config/ # Shared configs (Nuxt, TS, ESLint)
│ └── nuxt.config.ts # Base Nuxt config
│
├── turbo.json # Turborepo task pipeline
├── package.json # Root workspace deps
└── pnpm-workspace.yaml # Workspace config (if using pnpm)
Define pipelines to handle shared and platform-specific tasks:
{
"pipeline": {
"build": {
"dependsOn": ["^build"], // Build dependencies first
"outputs": ["dist/**"], // Cache outputs
"cache": true
},
"dev": {
"cache": false, // Disable cache for dev servers
"persistent": true
},
"lint": {
"outputs": []
}
}
}
packages:
- 'apps/*'
- 'packages/*'
- Share Ionic components across apps using TypeScript path aliases:
// apps/tauri-desktop/nuxt.config.ts import { defineNuxtConfig } from '@nuxt/bridge'; import { join } from 'path'; export default defineNuxtConfig({ alias: { '@ui': join(__dirname, '../../packages/ui'), }, });
- Use in components:
<script setup> import { IonButton } from '@ui/components'; </script>
- Abstract platform-specific logic with dependency injection:
// packages/core/p2p/interface.ts export interface P2PProvider { send(data: string): Promise<void>; } // apps/tauri-desktop/p2p.ts (Tauri impl) import { invoke } from '@tauri-apps/api'; export const TauriP2P: P2PProvider = { send: (data) => invoke('send_data', { data }), }; // apps/capacitor-mobile/p2p.ts (Capacitor impl) import { Network } from '@capacitor/network'; export const CapacitorP2P: P2PProvider = { send: async (data) => { await Network.connect(); // WebRTC logic... }, };
# Build shared packages first
turbo run build --filter=core... --filter=ui...
# Build Tauri app
cd apps/tauri-desktop
pnpm tauri build
# Build shared packages
turbo run build --filter=core... --filter=ui...
# Sync Capacitor with shared web assets
cd apps/capacitor-mobile
pnpm cap sync
Turborepo automatically caches:
- Build outputs (e.g.,
dist/
,.nuxt/
) - Dependencies (node_modules)
- Task results (e.g., TypeScript type-checking)
Example: After modifying a shared UI component, only dependent apps rebuild:
turbo run build --filter=ui... # Rebuilds UI package and apps depending on it
Use conditional imports in shared code:
// packages/core/storage.ts
import { isTauri } from '../utils/platform';
export const saveData = async (data: string) => {
if (isTauri()) {
const { writeFile } = await import('@tauri-apps/api/fs');
await writeFile({ path: 'data.txt', contents: data });
} else {
const { Filesystem } = await import('@capacitor/filesystem');
await Filesystem.writeFile({ path: 'data.txt', data });
}
};
Run parallel dev servers for all platforms:
# Start Tauri desktop dev
turbo run dev --filter=tauri-desktop
# Start Capacitor mobile dev
turbo run dev --filter=capacitor-mobile
- Shared Code Isolation:
- UI, core logic, and configs are versioned as separate packages.
- Parallelized Builds:
- Build Tauri and Capacitor apps simultaneously.
- Deterministic Caching:
- Skip redundant builds (e.g., unchanged UI components).
- Cross-Platform Testing:
turbo run test # Runs tests in all workspaces
- Dependency Conflicts: Ensure Tauri (Rust) and Capacitor (Java/Swift) toolchains don’t clash.
- Platform Detection Overhead: Abstracting platform-specific code adds complexity.
- Cache Invalidation: Turborepo may occasionally require
turbo run build --force
for major dependency updates.
For a working example, clone the Turborepo + Tauri + Capacitor starter (hypothetical template – adapt as needed). This structure lets you maintain a single codebase while leveraging Turborepo’s optimizations for multi-platform development.