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:
api{
readonly getSession: (input: { readonly headers: Headers }) => Promise<unknown>
}
{
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.
Production examples
examples/fastify-better-auth-sqliteuses real Better Auth email/password sessions and SQLite.examples/hono-better-auth-postgresuses Better Auth, Hono, and PostgreSQL.
Neither example contains a demo identity bypass or shares encryption secrets with Better Auth.