ADR-014: Typed IPC bridge — `ipc/` wrappers + shared TypeScript contracts
ADR-014: Typed IPC bridge — ipc/ wrappers + shared TypeScript contracts
Status
Accepted
Context
All frontend data enters through the IPC bridge (frontend-architecture.md §1.1:
no +server.js, no SSR, no build-time data). The surface is two halves —
Tauri commands (frontend → Rust, api-specs/tauri-commands.md) and Tauri
events (Rust → frontend, api-specs/tauri-events.md) — plus a one-time
bootstrap that wires events into the state layer.
The backend speaks snake_case Rust (serde); the frontend speaks TypeScript.
Payload shapes are canonical in data-models.md (TypeScript-first spec) and
the event/command API specs. Without a deliberate typing discipline, invoke
calls and listen handlers degrade into untyped any plumbing, and the TS
mirror drifts from the canonical spec.
ADR-005 chose Tauri events as the inter-component channel; ADR-007’s “shared TypeScript types” mitigation names the goal but not the mechanism.
Decision
The IPC bridge is the only code that touches @tauri-apps/api, in three
files under src/lib/ipc/:
commands.ts— one exported object per command group, matchingtauri-commands.mdexactly. Wrapper parameters are camelCase; Tauri v2 maps top-level camelCase argument keys to snake_case Rust parameters automatically, while nested object fields pass through for strict serde deserialization.events.ts— one typed subscription function per event intauri-events.md; each returns thelistenunlisten function for teardown.bootstrap.ts— a one-time boot sequence subscribing events and feeding them into the state layer (*.svelte.tsmodules, per ADR-013).
src/lib/contracts/types.ts is the executable mirror of data-models.md
plus the event/command payload types (PlaybackState, QueueState,
SearchResultBatch, PluginStatus, PluginInfo). Contract types mirror the
canonical snake_case field names verbatim; the code review step in CI diffs
types.ts against data-models.md and tauri-events.md to catch drift.
Alternatives considered
- Untyped
invoke/listen(string names,anypayloads). Zero ceremony, but every call site re-derives shapes, typos in command/event names surface at runtime, and the frontend has no compile-time view of the contract. Rejected — the contract surface is exactly where type safety pays off. - Runtime validation (zod) of every payload. Adds a validation layer and a dependency; the payloads already cross a serde boundary on the Rust side (strict deserialization). v1 uses compile-time typing + CI diff review instead.
- Auto-generation from Rust (
ts-rs/ OpenAPI-style schema). The single-source-of-truth ideal. Deferred to v2: requires restructuring the Rust type definitions and a codegen step in the build; frontend-architecture §16.4 records this and says an ADR would accompany that change.
Consequences
Positive:
- The frontend↔Rust contract is typed end-to-end; command/event payload shapes are checked at compile time and caught at review if the canonical spec changes.
- One import surface (
@rymflux/ipc) — components and state modules never touch@tauri-apps/apidirectly, keeping the vendored dependency confined. - The wrapper layer is the single place command argument mapping (camelCase → snake_case) happens; nested payload fields pass through for strict serde.
- A clean migration path to codegen in v2: the mirror
types.tsis replaced by generated output, andcommands.ts/events.tssignatures are unchanged.
Negative:
- Hand-synced mirror can drift despite the CI diff review; the review is human-dependent until v2 tooling exists.
- Wrapper + contract boilerplate per command/event group is repetitive to
author (one
commands.tsobject and oneevents.tsfunction per spec entry). - camelCase↔snake_case mapping is implicit (Tauri v2 convention); a nested field that must be renamed is easy to get wrong without the spec open.
References
frontend-architecture.md§4 (typed IPC bridge), §4.1–4.4, §16.4 (type parity tooling, v2)api-specs/tauri-commands.md,api-specs/tauri-events.md,api-specs/data-models.md- ADR-005 (inter-component communication — Tauri events)
- ADR-007 (search manager — shared TypeScript types mitigation)
- ADR-013 (state layer — the consumer of
bootstrap.ts)