Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Fetch transport

Mount Ownfold's portable Web Request handler, configure its validated browser transport, and secure origins, credentials, limits, and errors.

@ownfold/fetch is the canonical HTTP protocol used by the Next.js, Hono, Fastify, Node, Elysia, and TanStack Start adapters. It uses standard Request and Response objects and has no framework or database dependency.

Choose the database and identity independently in the framework recipe builder. The fetch handler works with every combination and requires no frontend.

Install

pnpm add @ownfold/browser @ownfold/fetch @ownfold/server

Server handler

import { createVaultFetchHandler } from "@ownfold/fetch"

export const handleVaultRequest = createVaultFetchHandler({
  server: vaultServer,
  basePath: "/api/ownfold",
  isOriginAllowed: ({ request }) => {
    if (request.headers.get("sec-fetch-site") === "cross-site") return false
    const origin = request.headers.get("origin")
    return origin === null || origin === "https://app.example.com"
  },
})
PropType
serverVaultServer
TypeVaultServer
basePath?string
Typestring
isOriginAllowed?(input: { readonly request: Request }) => boolean | Promise<boolean>
Type(input: { readonly request: Request }) => boolean | Promise<boolean>

Pass the original Request through unchanged so the server’s authentication resolver receives the host application’s cookies and authorization headers. Framework adapters should delegate all Ownfold paths to this handler instead of recreating validation or error mapping.

Origin checks apply to state-changing requests. Configure an explicit production policy whenever the endpoint can be reached cross-origin; CORS response headers alone do not prevent forged writes.

Browser transport

import { createVaultClient } from "@ownfold/browser"
import { createFetchVaultTransport } from "@ownfold/fetch"

export const vault = createVaultClient({
  transport: createFetchVaultTransport({
    baseURL: "/api/ownfold",
    credentials: "same-origin",
  }),
})
PropType
baseURL?string
Typestring
credentials?RequestCredentials
TypeRequestCredentials
fetch?typeof globalThis.fetch
Typetypeof globalThis.fetch
headers?HeadersInit | (() => HeadersInit | Promise<HeadersInit>)
TypeHeadersInit | (() => HeadersInit | Promise<HeadersInit>)

Use credentials: "include" only for an intentional cross-origin cookie deployment with a strict origin allowlist and correctly scoped cookies. A headers callback can obtain a fresh host-issued access token for each request:

const transport = createFetchVaultTransport({
  baseURL: "https://api.example.com/api/ownfold",
  credentials: "omit",
  headers: async () => ({ authorization: `Bearer ${await getAccessToken()}` }),
})

Recovery secrets and root keys must never be placed in headers.

Protocol guarantees

The default base path is /api/ownfold. JSON request and response bodies are bounded to 65,536 bytes. Requests with invalid JSON, the wrong content type, unsupported methods, unknown routes, or malformed inputs fail before reaching storage.

Successful responses use:

{ ok: true, data: value }

Failures use:

{ ok: false, error: { code, message } }

The browser transport validates returned vault, device, pairing, and rotation values. A malformed success response becomes INVALID_VAULT_RESPONSE, and local key state remains unchanged.

Route surface

Path Methods Purpose
/vault GET, POST Read or create vault metadata.
/recovery/verify POST Mark locally verified recovery setup.
/recovery/replace POST Commit a replacement Recovery Kit ID.
/devices GET, POST List or register devices.
/devices/touch, /devices/revoke POST Activity and revocation.
/pairings POST Create a pairing offer.
/pairings/status GET Poll an offer by request ID.
/pairings/approve, /pairings/cancel POST Resolve pairing.
/rotation GET, POST Read or begin rotation.
/rotation/progress, /rotation/complete POST Checkpoint or atomically cut over.

No route accepts a user ID. Ownership always comes from the authenticated request.

Custom fetch implementation

Tests, desktop shells, and instrumented runtimes can inject fetch:

const transport = createFetchVaultTransport({
  fetch: async (input, init) => {
    assertNoPlaintext(init?.body)
    return applicationFetch(input, init)
  },
})

Instrumentation must not log request bodies. They contain encrypted envelopes and metadata even though they do not contain plaintext.

Error behavior

Network failures return TransportError with VAULT_NETWORK_FAILED. Non-success HTTP responses preserve the server’s safe error code when the response shape is valid. Invalid or oversized responses return INVALID_VAULT_RESPONSE.

Retry only after reconciling current state. Creation uses stable identities and storage-level idempotency, but revision-checked mutations require a fresh read after a conflict.

Production checklist

  • Mount the entire handler under one exact base path.
  • Preserve the original request headers and cookies.
  • Configure a strict origin policy for browser writes.
  • Keep the 65,536-byte protocol limit at the outer proxy and framework layers.
  • Do not enable body logging, plaintext analytics, or request replay capture.
  • Use HTTPS in production.
  • Return cache-prevention headers for authenticated lifecycle responses at the platform edge.
  • Test malformed JSON, oversized bodies, absent sessions, stale revisions, and offline recovery.

Troubleshooting

Every operation returns unauthorized

The framework bridge may be constructing a new request without cookies or authorization headers. Pass the original headers and confirm the browser credential mode matches the host session.

ORIGIN_NOT_ALLOWED

The request origin does not match the configured policy. Compare normalized scheme, host, and port; do not solve this by accepting every origin in production.

INVALID_VAULT_RESPONSE

A proxy, error page, or incompatible server returned a body outside the Ownfold protocol. Inspect status and content type server-side while keeping the response body out of user telemetry.

Last updated on August 4, 2026

Was this page helpful?