Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Node.js and Express

Mount Ownfold in native Node.js or Express with bounded request streaming, explicit origins, authentication, storage, and graceful shutdown.

@ownfold/node adapts Node’s IncomingMessage and ServerResponse to Ownfold’s canonical Web handler. It can be used directly with node:http or as Express middleware. No frontend package is required.

Choose SQLite, PostgreSQL, Drizzle, or Prisma and choose identity independently in the framework recipe builder. The Node bridge does not require a specific database or authentication provider.

Install

pnpm add @ownfold/fetch@beta @ownfold/node@beta @ownfold/server@beta express

Install an authentication integration and one database adapter separately.

Create the server core

import { createVaultServer } from "@ownfold/server"

export const vaultServer = createVaultServer({
  adapter,
  getUserId,
})

getUserId receives the Web Request created by the Node bridge. Cookies and authorization headers are preserved.

Express

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

const app = express()

app.use(
  "/api/ownfold",
  createExpressVaultMiddleware({
    handler: createVaultFetchHandler({ server: vaultServer }),
    origin: "https://app.example.com",
    maxBodyBytes: 64 * 1024,
  }),
)

app.use(express.json({ limit: "64kb" }))

Mount Ownfold before middleware that consumes the request stream. The bridge bounds chunked bodies while reading them and returns a structured error for invalid chunks, incomplete bodies, or size violations.

Native Node.js

import { createServer } from "node:http"
import { createVaultFetchHandler } from "@ownfold/fetch"
import { createNodeVaultHandler } from "@ownfold/node"

const handleVault = createNodeVaultHandler({
  handler: createVaultFetchHandler({ server: vaultServer }),
  origin: "https://api.example.com",
})

createServer(async (request, response) => {
  if (request.url?.startsWith("/api/ownfold") === true) {
    await handleVault(request, response)
    return
  }

  response.writeHead(404).end()
}).listen(3000)

The request URL passed to the canonical handler includes the original path. Keep the default /api/ownfold base path or configure the Fetch handler and mount point together.

Public origin

The origin option must be a URL origin only—scheme, host, and optional port. Do not include a path, query, or fragment.

origin: "https://app.example.com"

For validated multi-tenant hosts, use the callback form:

origin: ({ request }) => tenantOrigins.forRequest(request)

Never construct it directly from an untrusted Host or X-Forwarded-Host header. Configure trusted proxy handling in the application and resolve the tenant against an allowlist.

Authentication

Express applications may use Better Auth, Auth.js, bearer tokens, or any verified session loader. Ownership is always derived inside getUserId; it is never accepted from route parameters or JSON.

const vaultServer = createVaultServer({
  adapter,
  getUserId: async ({ request }) => {
    const session = await sessionStore.fromHeaders(request.headers)
    return session?.userId ?? null
  },
})

Error and response behavior

  • Adapter failures become scrubbed JSON errors.
  • Responses use Cache-Control: no-store.
  • Unsupported methods return 405 with Allow.
  • Unknown Ownfold paths return a structured 404.
  • The bridge catches unexpected Node-boundary failures and completes the response once.

Do not add a second error serializer after the middleware has written or ended the raw response.

Graceful shutdown

Stop accepting requests, wait for active handlers, and close the database pool or SQLite connection:

const listener = app.listen(3000)

const shutdown = () => {
  listener.close(async () => {
    await database.close()
    process.exitCode = 0
  })
}

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

Testing

  • Call the real HTTP listener with authenticated and unauthenticated requests.
  • Send oversized and chunked bodies.
  • Verify proxy headers cannot select an arbitrary origin.
  • Confirm ownership fields in request bodies are rejected.
  • Run the selected database adapter compliance suite.

The repository includes a complete Express + Better Auth + SQLite example.

Troubleshooting

Requests hang

Check that express.json() or another body parser is not mounted before Ownfold. The raw request stream must still be readable.

Every request is unauthorized

Confirm cookies or authorization headers survive proxying and that getUserId receives the public request expected by the session library.

URLs resolve against the wrong host

Set origin to the externally visible application origin. Do not use an internal container address unless clients also use that address.

Production checklist

  • Configure an explicit public origin.
  • Mount before body-consuming middleware.
  • Set a bounded body size.
  • Verify proxy trust and forwarded headers.
  • Use secure session cookies or verified bearer tokens.
  • Add server rate-limit and audit hooks.
  • Close listeners and database resources on shutdown.

Last updated on August 4, 2026

Was this page helpful?