Skip to Content
GuidesMigrate from Tauri

Migrating from Tauri v2

Tauri and Tynd share the same native stack (wry + tao) and many of the same IPC primitives. The biggest change is the backend language: Rust → TypeScript. You stop maintaining two languages.

What changes

Tauri v2Tynd
#[tauri::command] fn greet(name: String) -> Stringexport async function greet(name: string): Promise<string>
invoke("greet", { name })await api.greet(name) (typed via typeof backend)
app.emit("event", payload)events.emit("event", payload) (typed via createEmitter)
await listen("event", handler)api.on("event", handler)
Capability ACLs in capabilities/*.tomlNo equivalent. Export = exposure; do auth/authz in backend logic.
Plugins (fs, http, shell, dialog, …)Built-in OS APIs in @tynd/core/client.
src-tauri/ with Cargo.tomlRemoved. TypeScript only.
tauri.conf.jsontynd.config.ts (TypeScript, validated by valibot).

What stays the same

  • Native WebView (WebView2 / WKWebView / WebKitGTK) — identical rendering target.
  • Zero-network IPC — custom scheme, no TCP port, no firewall prompt.
  • Code signingsigntool / codesign / notarytool — same tools.
  • Updater manifest format — Tynd is Tauri-compatible (version, pub_date, notes, platforms.<os>-<arch>.{url,signature}). Reuse existing manifests.
  • Ed25519 update signatures — same primitive, same verification path.

Step-by-step

Init Tynd next to Tauri

Don’t delete src-tauri/ yet. Run:

bunx @tynd/cli init

This:

  • Adds @tynd/cli, @tynd/core, @tynd/host to package.json.
  • Writes tynd.config.ts pointing at your existing frontend outDir.
  • Scaffolds backend/main.ts with a minimal app.start.

Port commands

For each #[tauri::command] in src-tauri/src/, write a TypeScript equivalent in backend/main.ts:

// src-tauri/src/lib.rs — OLD #[tauri::command] async fn greet(name: String) -> Result<String, String> { Ok(format!("Hello, {}!", name)) } #[tauri::command] async fn read_config(app: AppHandle) -> Result<Config, String> { let path = app.path().app_config_dir().unwrap().join("config.json"); let body = std::fs::read_to_string(path).map_err(|e| e.to_string())?; serde_json::from_str(&body).map_err(|e| e.to_string()) }
backend/main.ts — NEW
import { app } from "@tynd/core"; import { os, path, fs } from "@tynd/core/client"; export async function greet(name: string): Promise<string> { return `Hello, ${name}!`; } export async function readConfig(): Promise<Config> { const configDir = await os.configDir(); const p = await path.join(configDir ?? "", "myapp", "config.json"); return JSON.parse(await fs.readText(p)); } app.start({ window: { title: "My App", width: 1200, height: 800 }, });

Notice:

  • No Result<T, E> — throw exceptions; they reject the frontend promise.
  • No #[derive(Serialize, Deserialize)]T flows through typeof backend.
  • No AppHandle — Tynd OS APIs are free functions (os.configDir(), path.join(...), fs.readText(...)).

Port frontend calls

// OLD import { invoke } from "@tauri-apps/api/core"; const msg = await invoke<string>("greet", { name: "Alice" }); // NEW import { createBackend } from "@tynd/core/client"; import type * as backend from "../backend/main"; const api = createBackend<typeof backend>(); const msg = await api.greet("Alice");

The Tynd version is typed from typeof backend — no <string> annotation needed.

Port events

// OLD — backend app.emit("user-created", serde_json::json!({ "id": "1", "name": "Alice" }))?;
// NEW — backend/main.ts import { createEmitter } from "@tynd/core"; export const events = createEmitter<{ userCreated: { id: string; name: string }; }>(); events.emit("userCreated", { id: "1", name: "Alice" });
// OLD — frontend import { listen } from "@tauri-apps/api/event"; await listen<{ id: string; name: string }>("user-created", (e) => { /* e.payload.id */ }); // NEW — frontend api.on("userCreated", (user) => { /* user.id, user.name — typed */ });

Port plugin calls

Tauri pluginTynd equivalent
@tauri-apps/plugin-fsfs
@tauri-apps/plugin-httphttp (or fetch)
@tauri-apps/plugin-shellshell + process
@tauri-apps/plugin-dialogdialog
@tauri-apps/plugin-clipboard-managerclipboard
@tauri-apps/plugin-notificationnotification
@tauri-apps/plugin-osos
@tauri-apps/plugin-processprocess + app.exit
@tauri-apps/plugin-global-shortcutshortcuts
@tauri-apps/plugin-autostartautolaunch
@tauri-apps/plugin-deep-linksingleInstance.onOpenUrl + protocols in config
@tauri-apps/plugin-single-instancesingleInstance
@tauri-apps/plugin-updaterupdater — same manifest format
@tauri-apps/plugin-storestore
@tauri-apps/plugin-sqlsql (bundled SQLite)
@tauri-apps/plugin-websocketwebsocket (or native WebSocket)

Tynd doesn’t ship equivalents for: Stronghold, biometric, NFC, positioner, persisted-scope, log. For those, either use pure-JS libs or write your own wrapper on top of the available primitives.

Port window config

// OLD tauri.conf.json { "app": { "windows": [ { "title": "My App", "width": 1200, "height": 800, "center": true, "resizable": true } ] } }
// NEW tynd.config.ts (or app.start config) app.start({ window: { title: "My App", width: 1200, height: 800, center: true, resizable: true, }, });

Port menu + tray

Menus and trays are declared in app.start(config):

backend/main.ts
app.start({ menu: [ { type: "submenu", label: "File", items: [ { label: "New", id: "file.new", accelerator: "CmdOrCtrl+N" }, { label: "Open", id: "file.open", accelerator: "CmdOrCtrl+O" }, { type: "separator" }, { role: "quit" }, ], }, ], tray: { icon: import.meta.dir + "/../assets/tray.png", tooltip: "My App", menu: [ { label: "Show", id: "show" }, { label: "Quit", id: "quit" }, ], }, window: { /* ... */ }, });

Handle clicks in the frontend:

import { menu, tray } from "@tynd/core/client"; menu.onClick("file.new", () => createDocument()); tray.onMenu("quit", () => app.exit(0));

Delete src-tauri/

Once the Tynd app builds and runs, remove the Tauri sub-crate, the Rust toolchain from CI, and any Tauri-specific plugins from package.json.

Update CI

Replace tauri-apps/tauri-action with a direct tynd build --bundle step:

- run: bunx tynd build --bundle

See the Bundling guide for a full matrix example.

What you lose

  • Capability-based ACLs. Tauri’s capabilities/*.toml per-command / per-path / per-URL permissions have no Tynd equivalent. Do access control in backend logic.
  • Mobile (iOS + Android). Tynd is desktop-only.
  • Official plugin ecosystem. The 30+ Tauri plugins don’t have 1:1 Tynd equivalents. Check the table above; for missing ones, use pure-JS libs or implement on the Tynd primitives.
  • Cross-compilation. Tauri builds Windows binaries from Linux. Tynd requires the target host OS.
  • Delta updates. Tynd ships full-binary updates; Tauri supports binary-diff.

What you gain

  • TypeScript end-to-end. No Rust in your source tree.
  • Zero-codegen typed RPC. typeof backend — rename a function and the compiler catches every stale call.
  • Dual runtimes. lite (~6.5 MB) vs full (~44 MB) with a one-line config change. No Rust equivalent.
  • Simpler security model. Export = exposure. No ACL config to drift out of sync.
Last updated on