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.sqliteimport { 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 generateimport { 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.