Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Better Auth

Integrate Better Auth with stable vault ownership, session resolution, framework mounting, account lifecycle, resets, tests, and failure handling.

@ownfold/better-auth adapts a Better Auth server instance to Ownfold’s one authentication contract: resolve the immutable authenticated user ID from a Web Request.

It does not configure Better Auth, add authentication routes, share databases, manage providers, or store encryption secrets.

Install packages

pnpm add @ownfold/better-auth@beta @ownfold/server@beta better-auth

Configure and mount Better Auth first using its framework integration. Ownfold requires only the server instance’s auth.api.getSession({ headers }) method.

Configure the resolver

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

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

The adapter is structural. The complete accepted shape is:

PropType
api{ readonly getSession: (input: { readonly headers: Headers }) => Promise<unknown> }
Type{ readonly getSession: (input: { readonly headers: Headers }) => Promise<unknown> }

For every Ownfold request, the resolver forwards the real Headers object to Better Auth, awaits the session, and returns session.user.id only when it is a non-empty string. Missing or malformed sessions become anonymous; VaultServer converts that to AuthenticationFailedError.

Request flow

Browser request
      │ cookies / authorization headers

Ownfold framework handler
      │ original Web Request

betterAuthUserResolver(auth)
      │ auth.api.getSession({ headers })

stable session.user.id or null


VaultServer → application-owned adapter

No Better Auth password, OAuth token, refresh token, provider account, session record, or MFA secret crosses into Ownfold storage.

Framework examples

const vaultServer = createVaultServer({
  adapter,
  getUserId: betterAuthUserResolver(auth),
})

const ownfold = createHonoVaultHandler({ server: vaultServer })
app.all("/api/ownfold/*", (context) => ownfold(context))
const vaultServer = createVaultServer({
  adapter,
  getUserId: betterAuthUserResolver(auth),
})

app.all(
  "/api/ownfold/*",
  createFastifyVaultHandler({
    server: vaultServer,
    origin: "https://app.example.com",
  }),
)
const handler = createVaultFetchHandler({
  server: createVaultServer({
    adapter,
    getUserId: betterAuthUserResolver(auth),
  }),
})

Mount Better Auth’s own handler under its configured base path, commonly /api/auth. Mount Ownfold separately under /api/ownfold. Do not proxy Ownfold operations through Better Auth plugin data or reuse an auth route body parser that drops cookies/headers.

Stable identity rule

Ownfold binds every vault, device, pairing, and rotation row to session.user.id. That value must be:

  • unique across all users who share the database;
  • immutable for the lifetime of encrypted data;
  • identical in every application instance and region;
  • preserved across sign-in methods and account linking;
  • the same value exposed to browser encryption as ownerId.

Do not substitute email, phone number, display name, provider username, organization membership, or the provider’s external subject unless that field is your deliberate immutable application user ID.

Account linking

When Better Auth links multiple provider accounts, they must resolve to one existing application user ID. If linking creates a different user row, the new session addresses a different Ownfold vault. Resolve duplicate-account behavior before enabling linking in production.

User-ID migration

Changing IDs is a data migration, not a session callback edit. Migrate Ownfold coordination rows and application record ownership together while the old and new identities are known and the user can still authenticate. Preserve ciphertext and its authenticated ownerId expectations; record envelopes may require a deliberate migration if the owner context itself changes.

Password reset and account recovery

Better Auth account recovery restores the ability to create a valid application session. It does not reconstruct an Ownfold root key.

After a password reset, the user still needs either:

  • an enrolled, non-revoked browser device; or
  • the Recovery Kit plus its independent recovery secret.

Never automatically use the login password as the Recovery Kit secret. Never send the recovery secret to Better Auth, store it in an auth field, copy it into a reset token, or claim that an admin can restore encrypted data.

Account deletion

Decide the host application’s policy before deleting the Better Auth user:

Confirm destructive intent

Explain whether ciphertext, Ownfold coordination metadata, and backups will be deleted or retained under policy.

Complete vault-side cleanup or export

Perform authorized deletion/export while the session still resolves the stable user ID.

Delete authentication state

Remove sessions, provider accounts, and the Better Auth user only after dependent cleanup reaches a known result.

Deleting authentication first can orphan encrypted data under an identity no route can safely claim. Never add an unauthenticated cleanup endpoint that accepts the old user ID.

Sessions, caches, and revocation

The resolver trusts Better Auth’s getSession result. Configure Better Auth session expiry, revocation, cookie security, and any cookie cache according to your threat model. A cookie-presence check is insufficient for Ownfold mutations; the server must validate the session.

If Better Auth caches sessions, test the maximum delay between account/session revocation and Ownfold rejection. Device revocation and auth-session revocation are separate controls:

  • auth revocation prevents the session from reaching Ownfold metadata;
  • device revocation prevents a local device identity from continuing normal vault access.

Use both when responding to a stolen authenticated device.

Runtime compatibility

@ownfold/better-auth itself uses only Web Headers and structural typing. The selected Better Auth database adapter, plugins, and framework integration determine whether a runtime is Node-only or edge-compatible. Validate the entire dependency graph before choosing Hono Workers, Bun, or another runtime.

Testing

Test the resolver and full mounted route with real session cookies:

it("rejects an anonymous Ownfold request", async () => {
  const response = await app.request("/api/ownfold/vault")
  expect(response.status).toBe(401)
})

it("isolates vaults by Better Auth user ID", async () => {
  const alice = await signedInRequest("alice-id")
  const bob = await signedInRequest("bob-id")

  await createVaultAs(alice)
  expect(await getVaultAs(bob)).toBeNull()
})

Also cover expired/revoked sessions, a session without user, missing/empty/non-string IDs, account linking, cookie forwarding behind the production proxy, and cross-origin credential rules.

Troubleshooting

Better Auth works, but Ownfold always returns 401

Verify the Ownfold request includes the same session cookie/header, the framework forwards the original headers, and auth.api.getSession returns user.id for that request.

The resolver returns null for a valid-looking session

Inspect the server-side session shape. The adapter deliberately accepts only a non-empty string at session.user.id; it never falls back to email or name.

A user sees an empty vault after account linking

The linked session likely resolves a different application user ID. Repair the auth identity mapping; do not copy ciphertext or disable ownership checks ad hoc.

Password reset succeeded but the vault is unavailable

Authentication recovery and encryption recovery are independent. Restore with a Recovery Kit or approve the browser from another enrolled device.

Production examples

  • examples/fastify-better-auth-sqlite uses real Better Auth email/password sessions and SQLite.
  • examples/hono-better-auth-postgres uses Better Auth, Hono, and PostgreSQL.

Neither example contains a demo identity bypass or shares encryption secrets with Better Auth.

Last updated on August 4, 2026

Was this page helpful?