Vault composition
Blind MEK store plus create/unlock UX on the hardened passkey stack.
Vault composition
Use this when the product needs a zero-access vault: the server stores only blind wraps; the MEK, mnemonic, and PRF secret stay in the browser.
Vault requires the passkey composition. Elevate is usually installed too so MEK/wrap writes and recovery control can demand a fresh step-up window.
Live: Vault lab · Chooser: Use cases · Ceremony guide: Vault
What you install
Everything from Passkey + step-up, plus:
| Package | Role |
|---|---|
@railman/auth-zero-access | Blind store plugin (zeroAccess) + E2E relay endpoints |
@railman/zero-vault | Product vault client (not a Better Auth plugin) |
PRF salt is browser-only via defineZeroAccessPasskeyCryptoConfig — never a server plugin option.
Server composition
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
import { twoFactor } from "better-auth/plugins";
import { enhancePasskey } from "@railman/auth-zero-access-passkey";
import { zeroAccess } from "@railman/auth-zero-access";
import { elevate } from "@railman/auth-elevate";
import {
AUTH_REQUIRE_PRESETS,
createElevateOpener,
loginFactors,
requireAccess,
} from "@railman/auth-login-factor";
const secret = process.env.BETTER_AUTH_SECRET!;
const passkeyStack = enhancePasskey(passkey, {
rpID: "example.com",
origin: ["https://example.com"],
assertCredentialAccess: "session",
});
export const auth = betterAuth({
secret,
session: { cookieCache: { enabled: false } },
plugins: [
...passkeyStack.plugins,
loginFactors(),
twoFactor(),
elevate({
deploymentMode: "single-instance",
passkeyCounterGuard: passkeyStack.controller,
passwordOnlyIfNoStronger: true,
elevatedTtlSec: 300,
maxElevatedSec: 3600,
}),
zeroAccess({
deploymentMode: "single-instance",
requireRecoveryExportAck: true,
// Optional separate Railman root:
// secret: process.env.ZERO_ACCESS_SECRET,
}),
],
});
export async function assertPrivileged(session: {
userId: string;
sessionToken: string;
loginFactor?: string | null;
elevateClaim?: string | null;
}) {
await requireAccess({
secret,
session,
map: AUTH_REQUIRE_PRESETS.privilegedAction,
elevateOpener: createElevateOpener({
secret,
userId: session.userId,
sessionToken: session.sessionToken,
ttlSec: 300,
maxElevatedSec: 3600,
}),
// enrolled: { passkey, totp, password }, // server lookup for password AMR
});
}zeroAccess() discovers the installed passkey guard by exact plugin identity. Omit secret to capture Better Auth's resolved secret; set a stable Railman root only when persistent-data lifecycle must diverge from sessions.
Lab/dev may omit assertAccess at standard (warning); protected writes still deny without a callback. Production sketch with createZeroAccessAssertAccess: Quick start.
Typechecked sources: dx-d-full-vault.ts, dx-client-vault.ts.
Client plugins + vault
import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "@better-auth/passkey/client";
import { elevateClient } from "@railman/auth-elevate/client";
import { zeroAccessClient } from "@railman/auth-zero-access/client";
import { defineZeroAccessPasskeyCryptoConfig } from "@railman/auth-zero-access-passkey/client";
import { createVaultClient } from "@railman/zero-vault";
export const authClient = createAuthClient({
plugins: [passkeyClient(), elevateClient(), zeroAccessClient()],
});
const cryptoConfig = defineZeroAccessPasskeyCryptoConfig({
salt: process.env.NEXT_PUBLIC_PRF_SALT!,
});
export const vault = createVaultClient({
userId,
passkeyOptions: cryptoConfig,
});Create / unlock / recovery UX: Vault guide. Low-level wrap helpers live under @railman/auth-zero-access/client — prefer createVaultClient for product paths.
HTTP
Blind store and recovery endpoints: API — Zero-access. Never send MEK, mnemonic, or PRF secrets in request bodies (M6).
Hard rules
- Elevate (L1) ≠ MEK unlock (L2) — keep the code paths separate
- Recovery phrase is for reset, not everyday unlock
- Authorize from the server session, not client-forged claims
Next
- Packages: Zero-access · Zero-vault
- Recovery flows: Recovery
- Add sealed messaging: Chat composition
- Multi-node: Multi-instance
Demo-only: deploymentMode: "single-instance". Do not ship process-local stores.