Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Backend quickstart

Run Ownfold as a standalone Node.js API with your choice of database and identity, with no frontend packages.

This guide creates a pure backend Ownfold coordination service. It renders no UI and installs no browser or React package.

What you will build

authenticated HTTP request


Express → @ownfold/node → @ownfold/fetch → @ownfold/server

                         SQLite | PostgreSQL | Drizzle | Prisma

The service exposes vault, recovery, device, pairing, and rotation endpoints. It stores encrypted envelopes and coordination metadata. It does not decrypt application records.

1. Install backend packages

pnpm add @ownfold/fetch@beta @ownfold/node@beta @ownfold/server@beta express
pnpm add -D @ownfold/cli@beta @types/express typescript

Requirements:

  • Node.js 22.13 or newer;
  • either a verified immutable user ID or an explicitly private single-user process; and
  • an explicit public origin such as https://api.example.com.

2. Choose and initialize a database

pnpm add @ownfold/sqlite@beta
pnpm ownfold init --database sqlite --identity custom
pnpm ownfold migrate --database-path ./ownfold.sqlite
import { DatabaseSync } from "node:sqlite"
import { SqliteVaultAdapter } from "@ownfold/sqlite"

export const database = new DatabaseSync("ownfold.sqlite")
export const vaultAdapter = new SqliteVaultAdapter(database)
export const closeDatabase = async () => database.close()
pnpm add @ownfold/postgres@beta pg
pnpm ownfold init --database postgres --identity custom
pnpm ownfold migrate --database-url "$DATABASE_URL"
import { nodePostgresVaultAdapter } from "@ownfold/postgres/node-postgres"
import { Pool } from "pg"

export const database = new Pool({ connectionString: process.env.DATABASE_URL })
export const vaultAdapter = nodePostgresVaultAdapter(database)
export const closeDatabase = async () => database.end()
pnpm add @ownfold/drizzle@beta drizzle-orm
pnpm ownfold init --database drizzle --identity custom
pnpm ownfold migrate --database-url "$DATABASE_URL"
import { drizzleVaultAdapter } from "@ownfold/drizzle"
import { db, pool } from "./drizzle"

export const vaultAdapter = drizzleVaultAdapter(db)
export const closeDatabase = async () => pool.end()
pnpm add @ownfold/prisma@beta @prisma/client
pnpm ownfold init --database prisma --identity custom
pnpm prisma migrate dev --name add-ownfold
pnpm prisma generate
import { prismaVaultAdapter } from "@ownfold/prisma"
import { prisma } from "./prisma"

export const vaultAdapter = prismaVaultAdapter(prisma)
export const closeDatabase = async () => prisma.$disconnect()

3. Choose identity

import { betterAuthUserResolver } from "@ownfold/better-auth"
import { auth } from "./better-auth"

export const getUserId = betterAuthUserResolver(auth)
import { authJsUserResolver } from "@ownfold/auth-js"
import { auth } from "./auth-js"

export const getUserId = authJsUserResolver(async () => auth())
import type { OwnfoldUserResolver } from "@ownfold/server"

export const getUserId: OwnfoldUserResolver = async ({ request }) => {
  const session = await sessions.verify(request.headers)
  return session?.user.id ?? null
}
import { singleUserResolver } from "@ownfold/server"

export const getUserId = singleUserResolver("local-owner")

This is safe only when the process itself is private and single-user. It does not authenticate an HTTP caller.

Do not trust x-user-id, query parameters, or owner IDs in JSON bodies.

4. Create the vault server

import { createVaultServer } from "@ownfold/server"
import { getUserId } from "./auth"
import { vaultAdapter } from "./database"

export const vaultServer = createVaultServer({
  adapter: vaultAdapter,
  getUserId,
  onAuditEvent: (event) => auditLog.write(event),
})

The same server instance works behind every Ownfold framework adapter.

5. Mount Express

import { createVaultFetchHandler } from "@ownfold/fetch"
import { createExpressVaultMiddleware } from "@ownfold/node"
import express from "express"
import { vaultServer } from "./vault"

const origin = process.env.PUBLIC_ORIGIN

if (origin === undefined || origin.length === 0) {
  throw new Error("PUBLIC_ORIGIN is required, for example https://api.example.com.")
}

export const app = express()

app.disable("x-powered-by")
app.use(
  "/api/ownfold",
  createExpressVaultMiddleware({
    handler: createVaultFetchHandler({ server: vaultServer }),
    origin,
    maxBodyBytes: 64 * 1024,
  }),
)

app.use(express.json({ limit: "64kb" }))
app.get("/health", (_request, response) => response.json({ ok: true }))

Mount Ownfold before express.json(). Its Node bridge streams and bounds the request body before constructing a Web Request; pre-consuming the stream breaks the handler.

6. Start and stop cleanly

import { app } from "./app"
import { closeDatabase } from "./database"

const server = app.listen(3000)

const shutdown = () => {
  server.close(() => void closeDatabase().then(() => {
    process.exitCode = 0
  }))
}

process.once("SIGINT", shutdown)
process.once("SIGTERM", shutdown)

7. Verify the API

After authenticating with your application’s normal session mechanism:

curl --fail-with-body \
  --header "Cookie: session=YOUR_SESSION" \
  http://localhost:3000/api/ownfold/vault

An authenticated user without a vault receives a successful response containing null. An invalid session receives a structured authentication error. The handler sets Cache-Control: no-store.

Add application ciphertext routes

Ownfold’s coordination endpoint does not own your documents, messages, or files. Add application routes and validate every encrypted write through VaultServer.validateEncryptedRecordWrite before inserting it. The private records guide provides the complete pattern.

Use another backend framework

The server and database setup stay the same. Replace only the HTTP bridge:

Runtime Guide
Raw Node.js or Express Node.js and Express
Fastify Fastify
Hono Hono
Elysia Elysia
tRPC tRPC
Web Request runtimes Fetch handler

Production checklist

  • Replace the illustrative session resolver with verified production authentication.
  • Use PostgreSQL when more than one process writes coordination state.
  • Restrict allowed origins for cookie-authenticated mutations.
  • Rate-limit recovery, pairing, device, and rotation operations.
  • Send audit events to durable storage without logging sensitive payloads.
  • Back up the coordination database and test restoration.
  • Run authorization and adapter compliance tests before deployment.

Continue to the backend SDK overview or the exact server API reference.

Last updated on August 4, 2026

Was this page helpful?