Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Authentication API reference

Exact Better Auth, Auth.js, custom, and single-user resolver contracts, session shapes, failure behavior, and identity requirements.

Ownfold does not authenticate users. VaultServer asks the host application for one immutable user ID on every operation. Authentication packages adapt existing session APIs to that resolver.

Resolver contract

import type { OwnfoldUserResolver } from "@ownfold/server"

const getUserId: OwnfoldUserResolver = async ({ request }) => {
  const session = await authenticateApplicationRequest(request)
  return session?.accountId ?? null
}

The server treats null as unauthenticated. It rejects empty or oversized IDs and converts resolver exceptions into a safe authentication failure before touching the database.

Better Auth

pnpm add @ownfold/better-auth@beta

betterAuthUserResolver(auth)

import { betterAuthUserResolver } from "@ownfold/better-auth"
import { createVaultServer } from "@ownfold/server"
import { auth } from "@/lib/auth"

export const vaultServer = createVaultServer({
  adapter,
  getUserId: betterAuthUserResolver(auth),
})
PropType
api{ readonly getSession: (input: { readonly headers: Headers }) => Promise<unknown> }
Type{ readonly getSession: (input: { readonly headers: Headers }) => Promise<unknown> }

The adapter calls auth.api.getSession({ headers: request.headers }) for each server operation. It reads only session.user.id. Missing session, missing user, non-string ID, or empty ID returns null. The entire session and provider tokens never enter an Ownfold contract.

The interface is structural: a compatible Better Auth wrapper may be passed without importing a specific concrete auth type.

Auth.js

pnpm add @ownfold/auth-js@beta

authJsUserResolver(loadSession)

import { authJsUserResolver } from "@ownfold/auth-js"
import { createVaultServer } from "@ownfold/server"
import { auth } from "@/auth"

export const vaultServer = createVaultServer({
  adapter,
  getUserId: authJsUserResolver(async () => auth()),
})

The loader receives the current standard Request, allowing framework-specific Auth.js APIs to read headers or cookies. Its return value is untrusted unknown; the resolver structurally reads only a non-empty session.user.id string.

getUserId: authJsUserResolver(async (request) =>
  authFromHeaders(request.headers),
)

Custom resolver

Use a custom resolver when the host session shape does not match either adapter.

import { createVaultServer, type OwnfoldUserResolver } from "@ownfold/server"

const getUserId: OwnfoldUserResolver = async ({ request }) => {
  const result = await sessions.verify(request.headers.get("authorization"))
  if (result.status === "error") return null
  return result.value.subject
}

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

Verify signatures, expiry, revocation, audience, and tenant scope before returning the subject. Do not decode an unsigned token and treat its sub as authenticated.

No authentication / single user

singleUserResolver(userId)

import { createVaultServer, singleUserResolver } from "@ownfold/server"

export const vaultServer = createVaultServer({
  adapter,
  getUserId: singleUserResolver("local-owner"),
})

Use this only when the process and data are private to one trusted operator, such as a local desktop service bound to a protected loopback or socket boundary. It is not anonymous multi-user mode. Every request receives the same owner ID, so exposing the handler to untrusted clients exposes that owner’s lifecycle operations.

singleUserResolver trims its configured ID and rejects an empty value when created. Empty configuration is a startup error, not a per-request fallback; the server applies its normal user-ID validation to the returned value.

Identity requirements

The returned ID must be:

  • immutable for the lifetime of the account’s encrypted data;
  • unique within the application’s database and authorization domain;
  • the same value used as encrypted-record ownerId; and
  • unavailable to the browser as a field it can substitute for another account.

Email addresses, usernames, display names, device IDs, vault IDs, and provider access tokens are poor ownership identifiers. If a legacy account identifier must change, perform an explicit data and encryption-context migration; do not silently begin returning a new value.

Request and session behavior

Situation Resolver result Server behavior
Valid active session Immutable user ID Continue to rate limit and validation.
Missing/expired session null AUTHENTICATION_REQUIRED; no adapter call.
Empty or malformed ID null from official adapters Authentication rejection.
Session provider throws Rejected promise Safe authentication failure and rejected audit event.
Browser body contains userId Ignored/rejected by strict parser Ownership still comes from the resolver.

Multi-tenant applications

If the same account may own separate vaults in multiple tenants, return a stable compound subject owned by the server, such as an internal membership ID. Do not concatenate unvalidated browser tenant input. The application’s session/authorization layer must first prove membership in the tenant represented by that subject.

Testing resolvers

Test at least valid, anonymous, expired, malformed-session, and provider-failure cases. Assert the adapter is not called after an authentication rejection and the remote response never includes raw session details.

See Vault server, Errors, and the complete export index.

Last updated on August 4, 2026

Was this page helpful?