Step-up only
Password and TOTP sudo without passkeys, vault, or chat.
Step-up only
Use this when you want GitHub-style sudo / re-auth on an existing Better Auth session — delete account, change billing, open an admin panel — and you do not need WebAuthn, a vault, or sealed chat.
Elevate never unlocks a MEK. There is no PRF or recovery phrase on this path.
Live: Elevate lab · Chooser: Use cases
What you install
| Package | Role |
|---|---|
@railman/auth-login-factor | Signed L0 session stamps + requireAccess maps |
@railman/auth-elevate | Step-up verify + short elevate claim on the session |
better-auth/plugins twoFactor | Optional TOTP enroll/storage (elevate verifies) |
Skip @better-auth/passkey, @railman/auth-zero-access-passkey, zeroAccess, zero-vault, and zero-e2e.
Server composition
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
import { elevate } from "@railman/auth-elevate";
import { loginFactors } from "@railman/auth-login-factor";
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET!,
session: { cookieCache: { enabled: false } },
plugins: [
loginFactors(),
twoFactor(),
elevate({
deploymentMode: "single-instance",
passwordOnlyIfNoStronger: true,
elevatedTtlSec: 300,
maxElevatedSec: 3600,
}),
],
});Omit passkeyCounterGuard (or set allowPasskey: false) so passkey step-up stays off.
Typechecked source: docs-site/verified-examples/dx-a-step-up-only.ts (pnpm docs:verify-examples).
Gate a privileged route
import { requireElevate } from "@railman/auth-elevate";
await requireElevate(session.session.elevateClaim, {
secret: process.env.BETTER_AUTH_SECRET!,
userId: session.user.id,
sessionToken: session.session.token,
ttlSec: 300,
});For maps that also check elevate AMR and distinct-from-login rules, use requireAccess + createElevateOpener — see Login factor and API — privilege helpers.
Client flow
GET /elevate/methods— which factors are availablePOST /elevate/verifywith{ method: "password", password }or{ method: "totp", totpCode }- Session row carries the signed claim; UI may show
elevatedAt/expiresAtonly
HTTP table: API — Elevate.
Hard rules
- Elevate (L1) ≠ vault unlock (L2)
- Authorize from the server session, not client-forged claims
- Keep
session.cookieCachedisabled when using login-factor stamps
Next
- Deeper elevate semantics: Elevate / sudo
- Package getting started: Elevate · Login factor
- Add passkeys later: Passkey + step-up
- Production checklist: Checklist
Demo-only: deploymentMode: "single-instance". Shared stores for multi-node: Multi-instance.