docs/references/ipc/ipc-schema-guide.md
One file per domain under src/shared/ipc/schemas/, each split into two blocks:
| Block | Form | Direction | Trust |
|---|---|---|---|
Request (*RequestSchemas) | zod values (defineRoute) | renderer→main | untrusted → parsed |
Event (*EventSchemas) | pure types | main→renderer | trusted → not parsed |
A domain with no events simply omits the Event block. schemas/ipcSchemas.ts composes the per-domain pieces:
export const ipcRequestSchemas = { ...windowRequestSchemas, ...appRequestSchemas } satisfies Record<string, RouteDef>
export type IpcRequestSchemas = typeof ipcRequestSchemas
export type IpcRoute = keyof IpcRequestSchemas
export type IpcEventSchemas = WindowEventSchemas & AppEventSchemas
export type IpcEventName = keyof IpcEventSchemas
| Element | Rule | Example |
|---|---|---|
| Route name | dot snake_case namespace[.subdomain…].action | file.read_doc, window.main.set_minimum_size, ai.agent.task.create |
| Event name | dot snake_case namespace[.subdomain…].event | window.maximized_changed, ai.stream.chunk |
| Payload fields | JS camelCase (snake constrains only the route/event string) | { minWidth: number } |
| Request value/type | *RequestSchemas / IpcRequestSchemas / IpcRoute | windowRequestSchemas |
| Event contract/type | *EventSchemas / IpcEventSchemas / IpcEventName | WindowEventSchemas |
Any depth ≥ 2 segments is allowed. Add a subdomain once a namespace's routes fall into distinct groups — put the resource path first and the verb last (ai.agent.task.create, not ai.create_agent_task), and never fake a level with an underscore (ai.stream.open, not ai.stream_open). Group by domain, not by owning service: ai.tool.get_result and ai.tool.respond_approval share a subtree while delegating to different services. Existing subdomain trees: ai.*, mcp.{server,tool,package}, window.{main,sub}, system.{mac,shell}, app.{updater,data_reset,user_data_relocation}, channel.{feishu,wechat}, export.{obsidian,word}.
The dot structure is a naming convention, not type syntax — IpcRoute is the strong-typed union keyof IpcRequestSchemas; an undeclared route is a compile error. Reuse Preference's data-schema-key/valid-key ESLint rule for the snake-case keys.
ESLint enforcement: the
data-schema-key/valid-keyrule'sfilesglob includessrc/shared/ipc/schemas/**/*.ts, so every route/event key in this directory is lint-enforced and any new domain file is covered automatically. zod data-field names are exempt — keys inside az.*(...)object literal (e.g.z.object({ 'content-type': ... })) are skipped, so only the route/event strings are constrained. This relies on zod being imported asz(the repo convention).
| Type | Meaning |
|---|---|
defineRoute({ input, output }) | declare one route; identity at runtime, captures the zod schemas |
InputFor<R> / OutputFor<R> | parsed input / output type for a global route R |
EventPayload<E> | payload type for a global event E |
IpcHandlersFor<S> | exhaustive, closed handler map for a schema set S |
IpcContext | { senderId: WindowId | null } — handler's controlled second argument |
InputFor/OutputFor/EventPayload/IpcRoute/IpcEventName are bound to the global registry, so they resolve to never until at least one domain is migrated. The reusable inference (IpcHandlersFor<S>) is generic and verifiable against any schema set today.
zod schemas are runtime values.
IpcRouter) imports ipcRequestSchemas as a value to parse.import type from the @shared/ipc/schemas/* modules and @shared/ipc/types only. A value import would pull the entire zod schema set into the renderer bundle. This is enforced by an ESLint rule (@typescript-eslint/no-restricted-imports with allowTypeImports, scoped to src/renderer/** in eslint.config.mjs) that flags any value import under @shared/ipc/schemas. IpcError is the one exception — it is a value import, but plain TS with no zod dependency, so it is bundle-safe.Validation is always on: the router parses every request route. There is no skip-validation knob (add a field later only if profiling proves a hot route needs it).
Schemas are thin structural contracts, not behavior — do not add a per-domain schemas/__tests__. Asserting zod primitives (e.g. z.boolean() rejects a string, or z.infer yields boolean) only tests zod. Correctness is already locked by compile-time IpcHandlersFor exhaustiveness, z.infer driving handler/renderer types, and the one framework type test (src/shared/ipc/__tests__/schema.types.test.ts). Test the handler instead — see ipc-usage.md.