Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Browser quickstart

Build a browser E2EE flow with a headless client, application-owned UI, and ciphertext persistence.

This path uses browser end-to-end encryption. For an API-only service with no frontend packages, follow the backend quickstart instead.

Create the server module

import { createVaultServer } from "@ownfold/server"

export const vaultServer = createVaultServer({
  adapter,
  getUserId: async ({ request }) => {
    const session = await loadSession(request)
    return session?.user.id ?? null
  },
})

Identity must come from the authenticated request. Never accept a user ID from the vault request body, query string, or another client-controlled value.

Mount a framework adapter

import { createNextVaultHandlers } from "@ownfold/next"
import { vaultServer } from "@/lib/vault-server"

const handlers = createNextVaultHandlers({ server: vaultServer })
export const GET = handlers.GET
export const POST = handlers.POST

Mount this at app/api/ownfold/[...ownfold]/route.ts. Fastify, Hono, Elysia, Express, TanStack Start, tRPC, and generic fetch adapters expose the same server operations.

Create the browser client

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

export const vault = createVaultClient({
  transport: createFetchVaultTransport({ baseURL: "/api/ownfold" }),
  deviceLabel: "Web browser",
})

The transport carries coordination metadata and encrypted device envelopes. It never receives a root key, Recovery Kit secret, or plaintext application record.

Compose application-owned onboarding

"use client"

import { RecoveryKitFile } from "@ownfold/browser"
import { useRecoveryKit, useVault, useVaultStatus, VaultProvider } from "@ownfold/react"
import { vault } from "@/lib/vault-client"

function EncryptionSettings() {
  const client = useVault()
  const state = useVaultStatus()
  const recovery = useRecoveryKit()

  const start = async (secret: string) => {
    const draft = await client.createVault()
    if (draft.status === "error") return showError(draft.error)
    const kit = await recovery.create(secret)
    if (kit.status === "error") return showError(kit.error)
    RecoveryKitFile.download(kit.value)
  }

  return (
    <section>
      <h2>Private data</h2>
      <p>Encryption state: {state.status}</p>
      {state.status === "not-created" && (
        <button type="button" onClick={() => void start(readSecret())}>
          Enable encryption
        </button>
      )}
    </section>
  )
}

export default function Page() {
  return <VaultProvider client={vault}><EncryptionSettings /></VaultProvider>
}

Ownfold supplies the state and actions, not the markup. The host chooses names, descriptions, visible actions, file inputs, confirmation steps, and visual design. Re-import the downloaded kit and call recovery.verify(contents, secret) before treating onboarding as complete.

Encrypt before persistence

const encrypted = await vault.encryptJson({
  namespace: "notes",
  recordId: noteId,
  ownerId: authenticatedUserId,
  value: { title, body },
})

if (encrypted.status === "error") return showError(encrypted.error)
await saveNote({ id: noteId, encryptedPayload: encrypted.value })

On the write endpoint, call vaultServer.validateEncryptedRecordWrite() with the independently known namespace and record ID before inserting the envelope. Reject extra plaintext fields.

Decrypt after retrieval

const stored = await loadNote(noteId)
const decrypted = await vault.decryptJson({
  namespace: "notes",
  recordId: noteId,
  ownerId: authenticatedUserId,
  payload: stored.encryptedPayload,
})

Context mismatch, corrupted metadata, ciphertext modification, wrong keys, and unsupported versions return typed errors. Never replace failure with plaintext or empty content.

Verify the seam

Use a transport spy and assert that record content, Recovery Kit passwords, and raw keys never appear in any request. Continue with basic usage, the complete browser client reference, and private records.

Last updated on August 4, 2026

Was this page helpful?