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.POSTMount 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.