Scaffold Tauri v2 Desktop App
Add a Tauri v2 desktop app to an existing monorepo scaffolded with /dlc-scaffold-monorepo.
Arguments
$1- app name (required). Createsapps/$1/. Example:launchpad
If $1 is empty, ask the user for an app name. Detect @{org} scope from root package.json.
Step 1 - App Config Files
apps/$1/package.json
{
"name": "@{org}/$1",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "tauri dev",
"dev:frontend": "vite",
"build": "tauri build",
"build:frontend": "tsc && vite build",
"typecheck": "tsc --noEmit",
"fmt": "biome format --write ./src",
"lint": "biome lint --write ./src",
"check": "biome check ./src",
"test": "vitest run",
"test:watch": "vitest"
}
}
apps/$1/tsconfig.json
Use the Tauri desktop app variant from /dlc-scaffold-tsconfig (extends react, noUnusedLocals: false, references ./tsconfig.node.json).
apps/$1/biome.jsonc
Use the React app variant (TanStack / Tauri) from /dlc-scaffold-biome.
apps/$1/src-tauri/tauri.conf.json
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "{AppName}",
"identifier": "dev.{org}.{app}",
"version": "0.1.0",
"build": {
"beforeDevCommand": "bun run dev:frontend",
"beforeBuildCommand": "bun run build:frontend",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "{AppName}",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"resizable": true,
"decorations": true
}
],
"security": { "csp": null }
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
apps/$1/src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"store:default",
"dialog:default",
"log:default"
]
}
Step 2 - Directory Structure
apps/$1/src/
├── _routes/ # TanStack Router file-based routes
│ ├── __root.tsx
│ ├── index.tsx
│ ├── landing.tsx
│ └── settings.tsx
├── core/
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ ├── api/
│ │ └── {domain}.api.ts # invoke() wrappers
│ ├── types/
│ │ └── {domain}.types.ts
│ ├── queries/
│ │ ├── {domain}.query-client.ts # QueryClient subclass
│ │ └── index.ts # createApiClients()
│ └── lib/
│ ├── query-client.ts
│ └── result.ts
├── modules/
│ ├── AGENTS.md
│ ├── CLAUDE.md
│ └── {module-name}/
│ ├── components/
│ ├── types/
│ └── utils/
├── components/
│ ├── ui/
│ └── layouts/
├── hooks/
├── lib/
│ └── utils.ts
├── main.tsx
├── app.css
└── routeTree.gen.ts
apps/$1/src-tauri/
├── src/
│ ├── main.rs
│ ├── lib.rs
│ └── {domain}.rs
├── Cargo.toml
├── tauri.conf.json
└── capabilities/default.json
Step 3 - Key Patterns
Rust Tauri Command
// src-tauri/src/colors.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Color {
pub id: String,
pub name: String,
pub hex: String,
}
#[tauri::command]
pub async fn list_colors(api_key: String) -> Result<Vec<Color>, String> {
// Backend logic here
Ok(vec![])
}
Register in lib.rs
// src-tauri/src/lib.rs
mod colors;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.invoke_handler(tauri::generate_handler![
colors::list_colors,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
invoke() Wrapper
// core/api/colors.api.ts
import { invoke } from "@tauri-apps/api/core";
import type { Color } from "../types/colors.types";
export async function listColors(apiKey: string): Promise<Color[]> {
return invoke<Color[]>("list_colors", { apiKey });
}
Query Client
// core/queries/colors.query-client.ts
import { QueryClient } from "../lib/query-client";
import { listColors } from "../api/colors.api";
export class ColorsClient extends QueryClient<"colors"> {
constructor(queryClient: TanStackQueryClient) {
super(queryClient, "colors");
}
list = (apiKey: string) =>
this.query("list", {
queryFn: () => listColors(apiKey),
});
}
createApiClients() Factory
// core/queries/index.ts
export function createApiClients(queryClient: TanStackQueryClient) {
return {
colors: new ColorsClient(queryClient),
};
}
export type ApiClients = ReturnType<typeof createApiClients>;
Tauri Store for Credentials
// lib/storage.ts
import { Store } from "@tauri-apps/plugin-store";
const store = await Store.load("credentials.json");
export async function getApiKey(): Promise<string | null> {
return store.get<string>("api_key");
}
export async function setApiKey(key: string): Promise<void> {
await store.set("api_key", key);
await store.save();
}
Data Flow
Tauri Store (credentials)
↓
api.ts (invoke → Rust command)
↓
QueryClient subclass (caches via TanStack Query)
↓
Component → useQuery / useMutation
Step 4 - AGENTS.md Files
apps/$1/AGENTS.md
Contains:
- Stack: Tauri v2, Vite, TanStack Router, TanStack Query, Tailwind CSS v4, Rust
- Dev commands (
bun run dev- starts Vite + Tauri) - Full
src/andsrc-tauri/directory trees - TDD workflow
- Key patterns: Tauri commands, invoke wrappers, credential flow
apps/$1/src/core/AGENTS.md
Contains:
- Overview: "Data layer - Tauri invoke wrappers, query clients, and shared utilities"
- File structure tree
- Naming conventions table (includes Rust files)
- Architecture: Rust command → invoke wrapper → QueryClient → useQuery
- Rules 1-4 (see below)
- "Adding a New Domain" checklist (8 steps):
- Create Rust commands in
src-tauri/src/{domain}.rs - Register commands in
src-tauri/src/lib.rs - Create types in
core/types/{domain}.types.ts - Create
core/api/{domain}.api.tswith invoke wrappers - Create
core/queries/{domain}.query-client.ts - Register in
core/queries/index.ts - Add capabilities if new plugins needed
- Write tests:
{domain}.test.ts
- Create Rust commands in
apps/$1/src/modules/AGENTS.md
Contains:
- Purpose: "Feature modules - self-contained UI features"
- Structure template per module
- 5 rules: no barrel exports, colocate tests, import from core/ not between modules, single responsibility, keep components small
Additional naming conventions (Tauri-specific)
| Type | Pattern | Example |
|---|---|---|
| API wrapper (invoke) | {domain}.api.ts |
s3.api.ts |
| Query client | {domain}.query-client.ts |
s3.query-client.ts |
| Rust commands | {domain}.rs (in src-tauri/src/) |
s3.rs |
Step 5 - Tauri v2 Rules
In addition to the 8 shared rules from /dlc-scaffold-monorepo:
- All backend ops via Rust Tauri commands -
#[tauri::command]+invoke() - Frontend never holds SDK instances - only
invoke()calls to Rust - Credentials passed per-call - from Tauri Store to Rust, never stored in JS memory long-term
Step 6 - Verify
bun install- resolves workspace depscargo check(insrc-tauri/) - Rust compilesbun run typecheck- zero TS errorsbun run check- biome passesbun run test- all tests passbun run dev- Tauri dev starts (Vite + Rust)- Verify
capabilities/default.jsonpermissions match plugins - Verify
tauri.conf.jsonbuild commands are correct - Verify AGENTS.md files at: app root,
src/core/,src/modules/