Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

tRPC integration

Mount Ownfold's v11 coordination router and adapt an inferred tRPC client without coupling application ciphertext to tRPC.

The tRPC adapter does not select a database or authentication provider. Choose both in the framework recipe builder; no frontend package is required.

@ownfold/trpc supplies a typed vault-lifecycle router and a browser transport. It targets tRPC v11. Encrypted application records remain in your application’s own routers and tables.

Runnable example: examples/trpc-sqlite.

Install

pnpm add @ownfold/server@beta @ownfold/trpc@beta @trpc/server

This is sufficient for a backend-only tRPC service. Add @trpc/client and @ownfold/browser only when a browser client will consume the router.

Create the router

import { createVaultTRPCRouter } from "@ownfold/trpc"
import { vaultServer } from "../vault-server"

export const vaultRouter = createVaultTRPCRouter({ server: vaultServer })

The router exposes vault, recovery, device, pairing, and rotation procedures. Each mutation input is parsed with Ownfold’s Standard Schema-compatible validators before server logic runs.

Add the request to context

Ownfold needs the original Web Request, because VaultServer resolves the authenticated user from its cookies or authorization headers.

import type { VaultTRPCContext } from "@ownfold/trpc"

export const createContext = ({ request }: { request: Request }): VaultTRPCContext => ({ request })

If your tRPC adapter provides a Node request, convert it once at the framework boundary while preserving headers, method, URL, and body semantics. Do not manufacture a user ID from client input.

Merge into the application router

export const appRouter = router({
  vault: vaultRouter,
  notes: notesRouter,
})

export type AppRouter = typeof appRouter

The Ownfold router creates its own v11 router instance with the required context shape. Mount it as a nested router under a stable key such as vault.

Browser transport

This section is optional and does not affect server setup.

import { createVaultClient } from "@ownfold/browser"
import { trpcVaultTransport } from "@ownfold/trpc/client"
import { trpcClient } from "./trpc-client"

export const vault = createVaultClient({
  transport: trpcVaultTransport(trpcClient.vault),
})

The structural client interface works with a normally inferred tRPC client. No generated code or manual cast should be required when the router is mounted under vault.

Procedure surface

Procedures Purpose
getVault, createVault Vault metadata lifecycle.
markRecoveryVerified, replaceRecoveryKit Recovery status and replacement coordination.
listDevices, registerDevice, touchDevice, revokeDevice Device lifecycle.
createPairing, getPairing, approvePairing, cancelPairing Encrypted pairing transport.
getRotation, beginRotation, updateRotationProgress, completeRotation Resumable root-key rotation metadata.

No procedure accepts userId. Authentication is resolved from ctx.request on every operation.

Error translation

The server router maps authentication failures to UNAUTHORIZED, conflicts to CONFLICT, explicit limits to TOO_MANY_REQUESTS, limiter failures to SERVICE_UNAVAILABLE, and storage failures to a scrubbed INTERNAL_SERVER_ERROR. Other validated client failures become BAD_REQUEST.

The browser transport catches tRPC client failures and returns TransportError with VAULT_TRPC_FAILED. The current transport deliberately does not expose a tRPC cause object, which could contain framework or server details. Refresh remote state before retrying revision-sensitive operations.

Application ciphertext stays separate

const encrypted = await vault.encryptJson({
  namespace: "notes",
  recordId: noteId,
  value: note,
})

if (encrypted.status === "error") return handleError(encrypted.error)

await trpcClient.notes.create.mutate({
  id: noteId,
  encryptedPayload: encrypted.value,
})

The notes procedure authorizes ownership and stores only the envelope. It must not import a root key or decrypt the payload. This separation lets applications use REST, GraphQL, or another protocol for records while still using tRPC for Ownfold coordination.

Protected procedures

You may place the mounted router behind application-wide authenticated middleware, but keep Ownfold’s own getUserId check enabled. The server resolver is the authoritative identity boundary and prevents a routing mistake from becoming cross-user vault access.

Testing

  • Create callers with authenticated and unauthenticated Request objects.
  • Verify invalid inputs fail before adapter calls.
  • Verify client-supplied ownership fields are rejected.
  • Assert transport mocks never receive plaintext, root keys, or recovery secrets.
  • Exercise conflict and rate-limit translation.
  • Run a full browser onboarding/recovery flow through the actual tRPC HTTP adapter.

Troubleshooting

Context type is incompatible

Ensure the context object has request: Request. If your application context has additional fields, return them alongside request; structural typing preserves the richer type.

The browser client is not assignable

Confirm the Ownfold router is mounted under the property passed to trpcVaultTransport and server and client both infer from the same exported AppRouter type. Avoid duplicating procedure types.

All calls return VAULT_TRPC_FAILED

Inspect the tRPC network request and server logs for session, context, and routing failures. The browser error is intentionally scrubbed. Confirm the original request reaches context creation.

Application data appears in the vault router

Move record CRUD back to the host router. Ownfold’s router should transport lifecycle metadata and encrypted key envelopes only.

Production checklist

  • Use tRPC v11 on both client and server.
  • Preserve the original request in context.
  • Resolve ownership only through VaultServer.getUserId.
  • Mount the router under a stable namespace.
  • Keep application encrypted-record procedures separate.
  • Disable request-body logging for ciphertext routes.
  • Test unauthenticated, stale-revision, and malformed-input paths.

Last updated on August 4, 2026

Was this page helpful?