Session-based auth, database-backed via the native Drizzle adapter. This app is passwordless — OAuth (Google) + magic link + OTP, with passkeys for phishing-resistant login and TOTP as step-up only. See the better-auth skill and blueprint/auth.md.
Why was it chosen?
- Native Drizzle adapter with auto-schema generation.
- Built-in passkeys and TOTP — both load-bearing for this app's auth model.
Known limitations
Rate limiting:
- Requires
enabled: trueand IP forwarding in SvelteKit hooks (see blueprint/auth.md) - In-memory storage (default) problematic in serverless/multi-instance
- Use database/Redis adapter for production
- Only applies to client-initiated requests (not server-side calls)
Session management:
- Stateless sessions cannot be easily revoked (requires redeploy)
- Most plugins require a database
Ecosystem:
- Created May 2024 (~8 months old)
- Smaller provider ecosystem than Auth.js's years of contributions
- Growing but still early-stage community
SvelteKit-specific:
svelteKitHandlerdoesn't auto-populateevent.locals.user(manual implementation needed)- Cloudflare Workers requires explicit
/api/auth/[...betterauth]route /api/auth/*is fully owned bysvelteKitHandler's catch-all. Any custom+server.tsroutes placed under this prefix are silently swallowed — Better Auth handles the request and returns a 404 before SvelteKit's router sees it. Custom API routes that happen to start with/api/auth/(e.g. grant requests) must be moved to a different prefix (e.g./api/grant-requests).
twoFactor gates credential sign-in only:
- The
twoFactorplugin's challenge fires only on/sign-in/email|username|phone-number. A passwordless app (OAuth + magic link + OTP) has no credential sign-in, so a TOTP login challenge never triggers — by upstream design (1.6.3 broadened the gating, 1.6.4 reverted it). This app uses passkeys for phishing-resistant login and TOTP purely as a step-up factor.allowPasswordless: truelets credential-less users still enroll TOTP. Disabling TOTP / regenerating backup codes are gated upstream on a password we don't have (#9248); we substitute a step-up-freshness check in ahooks.beforemiddleware. See blueprint/auth.md.
Passkeys break on shared-domain previews:
*.vercel.appis on the Public Suffix List, so a Vercel preview URL can never satisfy the productionrpID. The passkey plugin is excluded entirely on previews (VERCEL_ENV === 'preview') — endpoints 404, UI hidden via an exportedpasskeysEnabledflag — rather than shipping a half-working ceremony surface.
Plugin endpoint shapes (1.6.19):
- Passkey registration is two calls —
GET /passkey/generate-register-optionsthenPOST /passkey/verify-registration(no/passkey/add-passkeyendpoint). The clientaddPasskey()wraps both. @better-auth/passkeyis a peer that must matchbetter-authversion-for-version; both are pinned exact.- Pinned at
1.6.19(bumped from1.6.17) for the upstream OTP-replay and session-cookie-splitting fixes.
Secure cookies set explicitly:
advanced.useSecureCookies = NODE_ENV === 'production'rather than relying on Better Auth inferring it from the baseURL scheme. The scheme-inference path is fragile behind a proxy (the app seeshttpinternally while servinghttps); pinning it toNODE_ENVis deterministic.
Related
- ../data/drizzle.md - Database integration
- ../data/postgres.md - Session storage