Skip to content

Typed IPC contracts

Rust is the source of truth for application commands. ipc_builder() in src-tauri/src/lib.rs supplies both Tauri’s runtime handler and tauri-specta’s exporter. Add commands to this registry with #[tauri::command] and #[specta::specta]; use commands from src/lib/generated/ipc.ts in the frontend, not handwritten invoke<T> calls. Tauri plugin APIs keep their upstream wrappers.

Generation and verification

Terminal window
pnpm generate:ipc
pnpm check:ipc
pnpm check
cargo test --locked --manifest-path src-tauri/Cargo.toml --lib
pnpm test

Generation uses the headless export_bindings Cargo example, the committed lockfile, and the project’s Biome formatter. It does not launch a webview, initialize telemetry, or read user settings/sessions. Commit src/lib/generated/ipc.ts alongside Rust contract changes. check:ipc generates into a temporary directory and compares the formatted bytes without overwriting the tracked file. The Checks workflow runs this check and TypeScript compile-time checks on macOS. The Tests workflow runs native tests and the application suite on macOS. Both workflows run only on pull requests, with the existing repository/actor restrictions. A supported native Tauri build environment is required even for generation.

The migration uses Tauri 2.11.5+, tauri-specta and Specta =2.0.0-rc.25, and specta-typescript =0.0.12. The previous Tauri 2.9.x Specta integration uses attributes removed in Specta rc.25. Keep the prerelease family pinned together and regenerate and test when upgrading; do not independently update one prerelease dependency. On macOS with a non-Apple cc on PATH, use Apple’s Clang for Cargo’s linker and C/C++ build scripts (CC=/usr/bin/clang, CXX=/usr/bin/clang++, and, on Apple Silicon, CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/clang).

Session boundary

src-tauri/src/sessions/commands.rs dispatches scoped operations directly into the typed response enum defined in sessions/ipc.rs. Read pages, append/recovery/delete receipts, recording status, and asset references share their plain native result types with IPC. There are no duplicate DTOs or field mappings for these values, and no intermediate JSON serialization/deserialization. The sanitized native Error is also shared, exported under the existing TypeScript name StorageError.

Separate DTOs remain for scoped registrations and filesystem path conversion, including discovery diagnostics; non-UTF-8 paths still reject rather than being lossily converted. Shared output types derive Serialize and Specta’s Type, not Deserialize. Actual request types retain deserialization and runtime validation. Filesystem handles, locks, and writer capabilities remain native-only; deriving Type does not register a command or authorize access.

The IPC request is a generated tagged union, not a JSON-string envelope. Responses are tagged by the same operation and carry a concrete result. SessionRepository keeps application decoding, error sanitization, paging, and recording lifecycle abstractions. Its injectable transport infers results from the operation and checks the returned discriminant; callers cannot choose an arbitrary response type.

Record inputs (header, entries, started, finished, interrupted) remain bounded JSON strings. Rust’s existing record framing, depth checks, batch limits, ancestry checks, path authorization, and media validation still apply. The aggregate request limit is retained after Tauri deserialization. Record outputs intentionally remain their original serde_json::Value trees: TypeScript’s existing session schemas own that extensible vocabulary, and the repository decodes records at runtime. The Rust JsonValue enum describes only the generated TypeScript schema; records are not converted into that second representation. Assets and non-record receipts have explicit generated DTOs. Optional application conveniences (such as omitting MediaDescription.capture) are normalized before IPC; generated native nullable fields remain explicit.

Rust integer offsets are exported as JavaScript numbers, matching existing IPC. Native media validation retains its safe-integer and size limits; generated types cannot validate integer ranges, finite numbers, filesystem permissions, provider content, or session invariants. They supplement, never replace, runtime validation. Command rejections still throw; SessionStorageError continues to sanitize unknown native errors rather than treating TypeScript types as trustworthy runtime data.

The native integration bridge uses the same typed dispatcher as Tauri and serializes responses only at the test transport boundary. Tests cover success/error serialization, malformed requests, record and aggregate limits, nullable fields, recording recovery, media, and existing persistence behavior. src/lib/sessions/ipc.type-test.ts uses @ts-expect-error to prevent request/return contracts from silently weakening. Storage layout and persistence responsibilities are unchanged.