# result-rpc > Typed RPC for React. Errors accumulate along the call path and discharge along the component tree. # The client Source: https://result-rpc.com/concepts/client ```ts import { createBrowserClient, batchFetchTransport } from "result-rpc/client"; import { appContract } from "../shared/contract"; export const client = createBrowserClient({ contract: appContract, transport: batchFetchTransport({ url: "/rpc" }), }); const result = await client.doc.byId({ id: "doc_123" }); ``` Calls issued in the same microtask share one HTTP request. Every batch item keeps its own status, decoder, rich-value envelope, and tagged Result. Use `fetchTransport` when batching is not wanted; the client API is unchanged. Because the batch itself is one successful protocol exchange, its outer HTTP status is 200; an item's domain status (for example 409) remains in that item and is reported by the server hooks, but browser network panels show the outer batch status. `batchFetchTransport` also implements the streaming path used by subscriptions. Use this same client instance beneath `ResultRpcProvider`; a separate fetch client is neither required nor compatible with that runtime's cache and ownership context. One deliberate carve-out from "every call resolves `Result`": input that the procedure's **own input codec** rejects throws a `TypeError` at the call site and never reaches the wire. That is a programmer error — the types prevent it for structural codecs, and for runtime-validating codecs (`wire.standard`) your form validates the human first (see [Forms and the wire](/guides/forms/)). Errors-as-values is a contract about _outcomes of operations_, not a net under code the compiler already rejects. The direct client is the honest base: it always resolves the complete union. Narrowing is a property of where a call is _rendered_, and the direct client is not rendered anywhere, so it never subtracts anything. Every failure in the union is already a reified `TaggedError`: definition `.is()` guards, `instanceof Error`, `toJSON`, and direct `yield*` all work after transport. ```ts Result< Doc, | Unauthorized | DocNotFound | ServerInternal | Offline | NetworkFailure | Timeout | HttpFailure | ProtocolViolation | DecodeFailure >; ``` Handle the union with an ordinary switch (`result.error satisfies never` in the default arm keeps it exhaustive), or build a reusable projection — a message catalog, a metrics mapper — once, from the same definition map middleware and shells use: ```ts import { errorCatalog } from "result-rpc"; const message = errorCatalog( { DocNotFound, Unauthorized }, { "doc/not-found": (e) => `Doc ${e.data.docId} is gone`, "auth/unauthorized": () => "Sign in to continue", }, ); ``` Adding a definition to the map breaks every catalog missing the new tag. For inline one-offs, `matchError(result.error, { ...handlers })` gives the same exhaustiveness on a single value. At an `unknown` boundary, use the catalog's exact guard before projecting: ```ts if (message.is(caught)) return message(caught); ``` The full composition surface — `gen` with `yield*`, `tryPromise`, `all`, the transform family — is covered in [Result composition](/concepts/results/). ## Cancellation is not an operation error Query cancellation updates lifecycle state without producing an `Err`, consuming a retry, or entering an error boundary. Direct calls accept an `AbortSignal`: ```ts const controller = new AbortController(); const pending = client.doc.byId({ id: "doc_123" }, { signal: controller.signal }); controller.abort(); ``` Internally result-rpc uses a tagged `control/cancelled` sentinel so cancellation does not depend on platform-specific `AbortError` identity. It is control flow and is excluded from the recoverable error union. Its sibling, `control/claimed`, is what a shell-claimed mutation rejects with — the same family, one `catch` at the call site, and `isCancelled`/`isClaimed` to tell the two events apart when the UX differs. ## Server-side calls stay in-process ```ts import { createServerClient } from "result-rpc/server"; import { appRouter } from "./router"; const serverClient = createServerClient(appRouter, { context, }); const result = await serverClient.doc.byId({ id: "doc_123" }); ``` The server client still runs middleware and input/output codecs, but it does not manufacture browser failures that cannot happen in-process. Use `createParityClient` from `result-rpc/testing` when a test should cross the serializer, protocol envelope, fetch handler, and browser decoder. --- # The client boundary Source: https://result-rpc.com/concepts/client-boundary Read this page before you wire a client. It is the one place where a wrong import is a **security bug**, not a style choice. ## result-rpc is not tRPC here tRPC ships **a type** to the client. `AppRouter` is imported `import type`, erased at build, and the browser gets nothing but inference. You can point tRPC's client at your server router because only its shape survives. result-rpc ships **a value** to the client — a real runtime built from your contract, with codecs that encode inputs and decode outputs. That value has to exist in the browser. So the question "what ends up in my bundle?" has a real answer. `createBrowserClient` therefore accepts contracts only. The rule is one sentence: > **Build the browser client from a `contract()`, and define that contract in a > module that never imports handler or server code.** Do that and your bundle contains codecs and error definitions — nothing else. You still control imports: merely importing a server module from a browser entry can make it reachable before any call runs. ## What a contract carries, and what a router carries Two different values, two different payloads: - **`contract()`** — procedure _shapes_ only: input codec, output codec, declared error definitions, `.affects()`/`.writes()` invalidation maps. No handlers, no middleware. This is the browser-safe surface, and the client only ever reads these fields. - **`router()`** (and any implemented procedure) — _everything the contract has, plus the handler function and its middleware chain_, which close over your database, your services, your environment. Server-only. The browser client is constructed from the contract. Fetch handlers and server clients are constructed from the implemented router. ## Why the split matters A handler that closes over a secret: ```ts // server.ts const server = serverRpc.context() const getUser = server.implement(getUserContract).handler(async ({ input }) => { const key = process.env.STRIPE_SECRET_KEY! // closed over by the handler const row = await db.query.users.findFirst(...) return ok(row) }) export const appRouter = server.router({ users: { get: getUser } }) ``` A client entry imports the standalone contract: ```ts // client.ts import { appContract } from "./contract"; createBrowserClient({ contract: appContract, transport }); // bundle: STRIPE_SECRET_KEY → 0 hits. db driver → 0 hits. ``` The layout is the protection. A top-level `server.router(...)` or `.handler(...)` call is potentially side-effecting, so a browser entry that imports the server module may retain its graph. Keep the import direction clean. ## The safe layout The dependency arrow points **from server to contract**, never the reverse: ``` contract.ts ── imports: result-rpc, wire codecs, models, error defs ▲ (and `import type` from server is fine — types are erased) │ server.ts ── imports contract.ts, implements handlers, builds the router client.ts ── imports contract.ts, builds the browser client ``` ```ts // contract.ts — no server imports; safe to reach from anywhere import { rpc, wire } from "result-rpc" import type { AppContext } from "./server" // type-only: erased at build export const getUserContract = rpc.context() .procedure().input(...).output(...).query() export const appContract = rpc.context().contract({ users: { get: getUserContract }, }) ``` ```ts // server.ts — imports the contract to implement it import { serverRpc } from "result-rpc/server" import { getUserContract } from "./contract" const server = serverRpc.context() const getUser = server.implement(getUserContract).handler(...) export const appRouter = server.router({ users: { get: getUser } }) ``` ```ts // client.ts — imports the contract only import { appContract } from "./contract"; export const client = createBrowserClient({ contract: appContract, transport }); ``` Three cheap rules keep this honest: 1. **Contracts import no runtime server code.** `import type` is fine; a value import from a server module is the footgun. 2. **`.affects()` targets are contract references**, never implemented procedures — an implemented procedure passed as a target drags its handler into the contract graph. 3. **Never import the router into anything a browser bundles.** Colocating "for convenience" ships handlers; there is no convenience worth a leaked secret. ## Isomorphic loaders are a second leak vector The rule above is about _what you import_. Some frameworks add a second hazard: code that **looks** server-side but runs in both places. TanStack Router/Start **loaders are isomorphic** — they run on the server during SSR and in the browser on client-side navigation. A loader that imports your database module directly will typecheck, work perfectly in dev SSR, and then ship your database driver (and whatever it closes over) to the browser the first time a user navigates client-side. Nothing warns you, because nothing is technically wrong: you asked for that module in code that runs on the client. There is no `'use client'` directive protecting you here, the way RSC frameworks protect a server component. The fix is to put an explicit server wall inside the loader: ```ts // ☠️ the loader imports the server module directly — ships the db on client nav import { db } from "./db"; export const Route = createFileRoute("/")({ loader: async () => db.query.spots.findMany(), }); // ✅ the server wall is a server function; the loader only calls it const loadSpots = createServerFn().handler(async () => { const { db } = await import("./db"); // server-only, never bundled return runtime.dehydrate(); }); export const Route = createFileRoute("/")({ loader: () => loadSpots() }); ``` The general test applies to any framework: **for every module a client bundle can reach, ask what it transitively imports.** RSC's `'use client'`/server-component split answers that question for you; isomorphic loaders make it your job. ## Monorepos and pre-built `packages/api` If you ship a built `packages/api` (emitted `.d.ts` + `.js`), consumers get the clean boundary automatically — in **both** dimensions: - **Runtime**: they import `appContract` from the package's built output; the handler code lives in a server entry the client build never touches. - **Types**: client callables are projected from the contract's associated `record`; the root context never appears in their input, output, or error types. TypeScript still has to resolve whatever declarations the shared contract itself imports, so keep that module's context shape browser-safe (or put the public context interface in its own type-only module). Runtime bundlers erase those imports, but a declaration graph is still a graph. The split protects the runtime bundle completely; keeping the shared contract's type imports narrow does the corresponding job for editor and `tsc` performance. ## What is safe to expose The contract _is_ your public API surface, and everything in it reaches the client by design — that is correct and not a leak: - **Field names and shapes** in your codecs (the client must decode them). - **Error tags and their data shapes** for declared, `visibility: "public"` errors. The compiler rejects private (`visibility: "private"`) definitions from procedure, middleware, and layer error maps. They are server-side composition currency; the runtime also sanitizes an unsafe cast or JavaScript bypass to `server/internal` at the wire. - **`.affects()`/`.writes()` maps** — these run _on the client_ (cache invalidation), so they are meant to ship. Keep them pure input→input transforms; never close a secret into one. If a fact would be dangerous in the browser, it does not belong in the contract — it belongs behind a handler. `visibility: "private"` controls the error algebra; it is not a bundler directive. Keep private definitions and the libraries they adapt in a server-only module. Importing a mixed server error module from the contract can still pull that module's runtime dependency graph into the browser even though TypeScript correctly excludes its private errors from `.errors(...)`. --- # Services and request context Source: https://result-rpc.com/concepts/context A procedure sees one `context`, but two different things feed it, with different lifetimes and failure rules: | | Services | Request middleware | | -------------------------- | ------------------------------------------- | --------------------------------------- | | Examples | database pool, worker bindings, API clients | session, viewer, organization | | Lifetime | process | request | | Shape | dependency graph | ordered chain | | Can fail with a wire error | no — a broken service is a broken process | yes — failures join the operation union | | Owned by | `defineService` / `resolveServices` | middleware | ## Services If you have read about Effect: this is its service/dependency-injection idea at its useful core — declare what each resource needs, resolve the graph once, memoize by identity — without fibers, without `Effect.gen`, without a runtime. It is a small feature, not a religion. ```ts import { defineService, resolveServices } from "result-rpc"; const Db = defineService("db", { create: () => createPool(env.DATABASE_URL), }); const Mailer = defineService("mailer", { needs: { db: Db }, create: ({ db }) => createMailer(db), }); const services = await resolveServices({ db: Db, mailer: Mailer }); ``` The graph is resolved once at process start and memoized by definition reference — a service two others depend on is constructed exactly once. The sharp edge, stated plainly: identity is by reference, so store definitions in module constants; two `defineService` calls are two services. The resolved record becomes the root context that every request closes over: ```ts export const handleRpc = createFetchHandler({ router: appRouter, createContext: ({ request }) => ({ ...services, request }), }); ``` Nothing pulls services per call — the auth middleware reads `context.db` because the root context guarantees it, and swapping the whole record for a test double is one argument to `createContext`. ## Middleware composes by requirement, not by ordering The footgun: middleware order as tribal knowledge — `session` must run before `requireViewer`, enforced by a comment. Here a middleware declares what it runs after; the dependency's output becomes its input, the dependency's errors join the union, and any `.use()` site pulls the whole chain in dependency order: `next` takes only this middleware's declared contribution. result-rpc merges it into the established context, so a middleware cannot accidentally discard upstream fields and refinements need no whole-context spread. ```ts import { serverRpc } from "result-rpc/server"; const server = serverRpc.context(); const session = server .middleware<{ viewer: User | null }>() .use(async ({ context, next }) => next({ context: { viewer: await userFromCookie(context) } })); const requireViewer = server .middleware<{ viewer: User }>() .after(session) // handler sees viewer: User | null .errors({ Unauthorized }) .use(({ context, errors, next }) => context.viewer === null ? err(errors.Unauthorized()) : next({ context: { viewer: context.viewer } }), ); ``` A mutation then demands exactly one thing: ```ts export const renameDoc = server .procedure() .input(RenameInput) .output(DocCodec) .use(requireViewer) // session comes along, in order .mutation(({ context, input }) => // context.viewer: User context.db.docs.rename(input, context.viewer), ); ``` `.use(session)` followed by `.use(requireViewer)` still runs `session` once — composition is deduplicated by reference identity, the same rule as services (module constants, not inline builds). A middleware whose input demands context the procedure cannot supply is a type error, so requirements are checked, not hoped for. ## Setting response headers, and logging someone in Writing a response header is a **declared capability**. A procedure calls `.headers()` and receives `context.headers`, a `Headers` to append to — a session cookie on login, a `cache-control`, a rate-limit hint. A procedure that does not declare it has no `context.headers` at all. ```ts const login = server .procedure() .headers() .input(wire.object({ email: wire.string, password: wire.string })) .output(wire.object({ userId: wire.string })) .errors({ BadCredentials }) .mutation(async ({ input, context, errors }) => { const user = await context.db.verify(input.email, input.password); if (!user) return err(errors.BadCredentials({})); context.headers.append( "set-cookie", `session=${await mintToken(user)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=604800`, ); return ok({ userId: user.id }); }); ``` Note the mutation returns a `Result` like anything else — bad credentials are a declared failure, not an exception. Reading cookies needs nothing new; `createContext` has the request: ```ts createContext: ({ request }) => ({ db, session: parseCookie(request.headers.get("cookie"))?.session, }); ``` A middleware that rotates a session cookie declares the same way, and then every procedure using it must declare `.headers()` too — the same rule that makes a middleware's errors part of its procedures' declared unions: ```ts const rotateSession = server .middleware() .headers() .use(({ context, next }) => { context.headers.append("set-cookie", `session=${refresh(context)}; HttpOnly; Path=/`); return next({ context: {} }); }); ``` ### Why declare it instead of just writing it The declaration is recorded in the contract, which means a transport knows _before dispatch_ that this call's response headers cannot be sent early. That matters because batching and streaming pull in opposite directions. A streamed batch sends its headers first and its results as they arrive — which means a `set-cookie` written by a handler that has not finished yet arrives after the headers are already on the wire, and is silently dropped. tRPC has exactly this hazard: `ctx.resHeaders` works under `httpBatchLink` and silently stops working under `httpBatchStreamLink`, with no error and no warning. The usual workaround is to move the cookie into `responseMeta`, a hook that runs before the procedure has produced a result. Declaring the capability makes the conflict statically visible instead of silent, and it is why the flag is part of the [contract digest](/concepts/deploys/) — a client and server that disagreed about it would reintroduce the same dropped cookie. Two consequences follow: **A batch shares one response.** Several procedures answered in one HTTP request share its headers, so their `set-cookie`s combine rather than overwrite. That is usually what you want; it does mean two logins in one batch set two cookies. **A subscription cannot write response headers at all** — a compile error, whether you declare `.headers()` directly or acquire it by `.use()`-ing a middleware that declares it. Its response is on the wire before the stream, and therefore before any of its middleware or handler code runs, so there is no moment at which a write could land. Set the header in the request that opens the stream instead. **The capability survives `.use()`.** A middleware replaces the context, but `headers` is re-applied from the capability rather than inherited from whatever the middleware passed on — so `.headers().use(mw)` and `.use(mw).headers()` behave identically. Builder order is not something you have to remember here, which is the same promise middleware composition makes above. The response is otherwise the protocol's. Status is derived from the failing error's declared `httpStatus` rather than chosen by a handler — that is what lets a client tell a real result-rpc failure from an intermediary's 502 — and the body is always the Result envelope. --- # Contract and procedures Source: https://result-rpc.com/concepts/contract ```ts import { rpc, wire, type InputOf, type ProcedureError, type ProcedureOutput } from "result-rpc"; import { DocNotFound, Unauthorized } from "./errors"; interface AppContext { docs: DocRepository; auth: AuthService; } export const app = rpc.context(); const DocCodec = wire.object({ id: wire.string, title: wire.string, savedAt: wire.date, }); type Doc = InputOf; export const getDocContract = app .procedure() .input(wire.object({ id: wire.string })) .output(DocCodec) .errors({ Unauthorized, DocNotFound }) .query(); export const appContract = app.contract({ doc: { byId: getDocContract, }, }); type DocOutput = ProcedureOutput; type GetDocFailure = ProcedureError; ``` One honest difference from tRPC: tRPC ships the router's _type_ to the client (`import type AppRouter`), and in exchange the client can neither decode rich values nor validate anything. result-rpc ships a small _value_ — the contract: codecs, tags, and policies, no middleware or handler code, safe in any browser bundle. It is the one place this library costs you a file tRPC doesn't, and it is what pays for `Date`/`Map`/`BigInt` over the wire and codecs on both sides. Browser clients are built from this contract. Implemented routers belong to `createFetchHandler` and `createServerClient`. `ProcedureOutput` and `ProcedureError` are the direct helpers when code names one procedure. For forms, loaders, test fixtures, or adapters spanning a whole application, `RouterInputs`, `RouterOutputs`, and `RouterErrors` preserve the router's nested shape so a path can be indexed without rebuilding a codec or union. All six helpers work on the shared contract; the nested helpers also work on an implemented router in server-only code. ## Implement the contract on the server ```ts import { err, ok } from "result-rpc"; import { serverRpc } from "result-rpc/server"; import { getDocContract, type AppContext } from "./contract"; const server = serverRpc.context(); export const getDoc = server .implement(getDocContract) .use(authenticated) .handler(async ({ input, errors, context }) => { const doc = await context.docs.find(input.id); if (!doc) return err(errors.DocNotFound({ docId: input.id })); return ok(doc); }); ``` The handler must return the declared Result: ```ts Result; ``` Returning a different tag is a type error. Smuggling an undeclared or malformed tag at runtime does not make it public: result-rpc logs the defect and emits a sanitized `server/internal` value. ## Middleware participates in the same union The tRPC footgun here is quiet: middleware that throws `TRPCError({ code: "UNAUTHORIZED" })` adds a failure mode the procedure's type never mentions. Here, middleware declares what it contributes, and the contribution lands in the union: ```ts import { err } from "result-rpc"; import { serverRpc } from "result-rpc/server"; import type { AppContext } from "./contract"; import { Unauthorized } from "./errors"; const server = serverRpc.context(); const authenticated = server .middleware<{ user: User }>() .errors({ Unauthorized }) .use(async ({ context, errors, next }) => { const user = await context.auth.user(); if (!user) { return err(errors.Unauthorized()); } return next({ context: { user }, }); }); export const getDoc = server.implement(getDocContract).use(authenticated).handler(/* ... */); ``` The procedure now returns: ```ts Result; ``` Builders are immutable, so a base forks freely — the `protectedProcedure` pattern is one line: ```ts const protectedProcedure = server.procedure().use(authenticated); const renameDoc = protectedProcedure .input(RenameInput) .output(DocCodec) .errors({ DocNotFound, DocLocked }) // only its own domain errors; .mutation(/* ... */); // the auth union rides in with the base ``` Middleware definitions also join the handler's `errors` bag, so a handler can return a middleware-contributed error without re-importing it — but choose deliberately there: `errors.Unauthorized()` from an ownership check would hand a 403-shaped outcome to whatever shell owns the auth union (whose reaction is a sign-in redirect). Not-the-owner is its own domain error. In contract-first code, middleware errors must already be present in the shared contract; `server.implement(...).use(...)` rejects an undeclared contribution. The code-first convenience form unions middleware definitions automatically. Duplicate tags with different definitions are rejected rather than silently overridden. ## Create the router and server ```ts import { createFetchHandler, serverRpc } from "result-rpc/server"; import type { AppContext } from "./contract"; import { getDoc } from "./doc"; const server = serverRpc.context(); export const appRouter = server.router({ doc: { byId: getDoc, }, }); export type AppRouter = typeof appRouter; // Nested inference helpers mirror the router's shape — works on contracts too: // type Inputs = RouterInputs; Inputs["doc"]["byId"] → { id: string } // type Outputs = RouterOutputs; Outputs["doc"]["byId"] → Doc // type Errors = RouterErrors; Errors["doc"]["byId"] → declared union export const handleRpc = createFetchHandler({ router: appRouter, endpoint: "/rpc", createContext: ({ request }) => ({ request, auth, docs, }), onInternalError: ({ incidentId, phase, cause, procedurePath }) => { logger.error({ incidentId, phase, cause, procedurePath }); }, }); ``` `onError` is the observability tap: it fires for every declared error that crosses the wire — domain errors, bad requests, sanitized internals — with the error value, its policy (severity, retry, status), and the procedure path, so one hook feeds metrics and logging: ```ts onError: ({ error, policy, procedurePath, httpStatus }) => { metrics.count(error._tag, { severity: policy?.severity }); }; ``` Malformed input is the client's fault, not an incident: it becomes a public `server/bad-request` (400) carrying path-and-message issues — never values — while `onInternalError` stays reserved for genuine defects. Unknown exceptions receive an incident ID and are passed to `onInternalError` when configured. The client receives only: ```ts { _tag: "server/internal", data: { incidentId: "inc_..." }, } ``` Exception messages, stacks, causes, queries, and response bodies are not reflected over the wire. --- # Deploys and stale clients Source: https://result-rpc.com/concepts/deploys Every deploy opens a compatibility window: new server, old tabs. In most stacks the window is invisible — a stale client's failures are indistinguishable from bugs (bad requests, decode failures), Sentry counts every deploy as an incident spike, and the "fix" is a user who happens to press reload. Closed unions make the window _more_ acute, not less: a stale client cannot even decode an error tag added after it was built. result-rpc makes the window a detected, owned state: 1. The server stamps every response with a digest of its contract — procedure paths, kinds, and every error tag with its policy (`x-result-rpc-contract`). A router and the contract it implements digest identically; nothing to configure. 2. The client compares the stamp to its own digest. The first mismatch emits a `skew` ClientEvent — observability sees the drift before anything fails. 3. When a request **fails** with a contract-shaped tag (`server/bad-request`, `client/decode-failure`, `client/protocol-violation`, `client/http-failure`) _while the digests differ_, the failure is reclassified as `client/stale`, carrying the original tag. Matching digests change nothing — a real defect stays a defect, and successful calls are never touched. The stamp is required protocol evidence, not optional metadata. A missing or empty `x-result-rpc-contract` is a `client/protocol-violation` in unary, batched, and streaming responses. The built-in transports preserve it. A custom `ClientTransport` must return the server stamp as `response.contract`, and a proxy must forward or expose the header; otherwise the client fails closed instead of silently disabling skew detection. And `client/stale` has a built-in owner: the boundary's `StaleShell` claims it, holds the affected operations, and reacts — by default with a page reload, because the reload fetches the current client, which _is_ the fix. Override it to taste: ```tsx const { BoundaryProvider } = boundaryShells({ onStale: () => toast("A new version is available", { action: reload }), }); ``` The automatic digest is structural: built-in codecs contribute their complete nested shape, constraints, literals, model projections, and error-data schemas. Adopted Standard Schemas and guarded serializable values contribute their application-owned stable schema `id`. A field-level contract change therefore changes the digest. You may instead make a build stamp the effective version on both sides: ```ts createFetchHandler({ router, contractVersion: BUILD_SHA, ... }) createBrowserClient({ contract, contractVersion: BUILD_SHA, ... }) ``` Detection is failure-gated, so matching successful calls are never reclassified. `contractVersion` _replaces_ the structural digest entirely — it does not combine with it. Matching version strings on both sides suppress stale reclassification even when the underlying shapes differ, which is exactly the escape hatch a deliberately loose client (a canary, a test double, a tolerant reader during an expand window) needs. The whole mid-deploy arc is pinned as a runnable test in `examples/07-tracker`: a deliberately _stale-shaped_ client — the old deploy, no schema preflight — sends a bad request across the real wire, the server's input decode rejects it, and `server/bad-request` comes back projected onto form fields with `fieldIssues`, asserted at exactly one wire call. The failure mode every production app has during a rollout, and the one no framework's docs can usually demonstrate. Deploys then stay boring the same way database migrations do: **expand, then contract**. Ship additive changes first (new procedures, new tags — old clients never call what they don't know about), and make removals and reshapes a later deploy, after the previous client generation has drained. When a stale tab does cross the window, it reloads once instead of mis-reporting a bug. This is the same discipline [onwardpg](https://github.com/jokull/onwardpg) enforces for the database tier — expand while old code is live, contract after it drains — applied one level up, between the server and the browsers it left behind. --- # Entities Source: https://result-rpc.com/concepts/entities Update your profile picture. The avatar in the header, the byline on every cached doc, and the member row in settings all show the new picture **immediately** — no query invalidated, no refetch issued, nothing written at any call site: ```ts export const User = defineModel("user", { key: "id", shape: { id: wire.string, name: wire.string, avatarUrl: wire.url }, }) const setAvatar = server.procedure() .input(wire.object({ key: wire.string })) // a bucket reference; bytes are out of band .output(UserCard) // ← returns WHO changed .mutation(...) ``` The mutation returned a `user` entity; the cache knows every query whose result contains `user:u_1`; each one is patched in place — automatic invalidation and automatic updates by model + id. On the client, the full extent of the wiring is the mutation call itself: ```tsx