Authentication
Configure and extend Better Auth without creating a second session authority.
Authentication
Better Auth 1.6 is the only authentication and session implementation. The pre-release schema has
one canonical identity, users.id; domain and wire code may call it user_id, but there is no
second stored application-user key.
Included flows
| Flow | Contract |
|---|---|
| Email/password signup | 8–128 characters, Bun password hashing, verification required, no immediate auto sign-in |
| Email verification | sent on signup/sign-in, successful verification signs the user in |
| Password sign-in | only a valid verified identity receives a session |
| Password reset | reset email through the typed mailer; successful reset revokes existing sessions |
| Magic link | 15-minute expiry, token stored hashed, successful token is single-use |
| Google OAuth | enabled only when both server credentials exist; missing config fails closed |
| Account linking | explicit linking only; implicit email-based linking is disabled |
| Session | secure Better Auth cookie, 7-day expiry, 1-day update age |
| Sign-out | Better Auth session revocation plus browser query/client-state clearing |
Google is the only included social provider. Adding providers to match a competitor is not a release requirement.
Request path
The Hono app mounts Better Auth directly:
browser authClient
→ /auth/*
→ Better Auth handler
→ Better Auth tables in PostgresProtected product routes use:
HttpOnly session cookie
→ sessionAuth
→ auth.api.getSession(headers)
→ c.var.user_id = session.user.id
→ ownership-scoped repository queryThe browser uses credentials: 'include' for both Better Auth and the typed Hono RPC client. It
does not store an access token in local storage. Do not add custom access/refresh JWTs, a second
cookie, a second password implementation, or another session table.
Database ownership
dbmate owns these Better Auth-compatible tables:
users;auth_sessions;auth_accounts;auth_verifications;auth_rate_limits.
Better Auth uses its supported pg adapter while application repositories use bun:sql. Better
Auth CLI output may be inspected to discover a schema change, but the reviewed change must become a
dbmate migration. Never run a second migration authority against a buyer database.
Sessions and accounts reference users.id with cascading database deletes. The application
soft-deletes the user, then removes live sessions and reusable account credentials transactionally.
Authorization contract
Authentication proves identity; it does not prove resource ownership, role, or billing entitlement.
For a private resource:
- mount
sessionAuth; - read identity only through
requireUserId(c); - pass that ID into service and repository methods;
- include the ownership predicate in the read/write SQL;
- use a non-disclosing 404 for absent and wrong-owner private records;
- apply billing entitlement separately when the feature requires it.
Do not accept a user_id, owner ID, role, or entitlement from request input. Consult the optional
Add an authenticated route playbook for a maintained
implementation and verification route.
Email behavior
Verification, reset, and magic-link callbacks use the typed mailer. In development/test without
Resend, the complete message and action URL are logged so the flow is deterministic. In production,
RESEND_API_KEY is required and the log fallback cannot start.
Treat local logs as sensitive: they contain short-lived authentication URLs. Do not send console breadcrumbs to error monitoring, publish logs, or enable the fallback in a shared/production environment.
Origins, cookies, and proxy IPs
FRONTEND_URL, optional MARKETING_SITE_URL, and CORS_ORIGIN define trusted browser origins.
Hono permits credentials only for that configured set. Better Auth uses the same trusted origins.
Set TRUSTED_PROXY_PROFILE to direct, railway, dokploy, aws-alb, gcp-cloud-run, or
azure-container-apps. The Bun boundary ignores forwarding headers in direct mode. For managed
ingress profiles it overwrites the internal client-IP header from the provider's expected hop shape.
Prevent direct access around the selected ingress.
Better Auth and application endpoint rate limits are database-backed. Atomic fixed-window updates share each application budget across replicas.
Account deletion and billing
Deleting an application account cannot silently cancel a remote provider subscription. The server
therefore returns 409 CONFLICT while the latest local subscription is active, trialing, past due,
or canceled with paid time remaining.
The buyer must cancel through the selected provider's Billing portal and wait until the paid period ends. Only then can the application soft-delete the user and remove sessions/accounts in one transaction. Deletion is a terminal authentication state: Better Auth rejects every later session creation for that user, and a database trigger closes the status-check/session-insert race. A fresh magic link therefore cannot restore a deleted account. Product-specific data retention or immediate statutory erasure requirements may require a separate reviewed cancellation-and-deletion workflow before launch.
Verification
The real Postgres HTTP suite proves:
- password signup, verification, authenticated session resolution, and logout;
- password reset and revocation of existing sessions;
- magic-link signup and single-use replay rejection;
- a fresh magic link cannot recreate a deleted user's session;
- optional Google behavior fails closed when credentials are absent;
- unauthenticated requests return 401;
- cross-owner private records use non-disclosing 404 behavior;
- account deletion cannot orphan an active provider subscription.
Run:
RUN_DB_INTEGRATION_TESTS=1 \
DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
bun test --cwd apps/serverAuth changes require this suite plus a production-like browser test of cookies, redirects, email delivery, and exact deployed origins. Unit tests alone are insufficient.
Common failures
| Failure | Inspect |
|---|---|
| Sign-in loops to login | web/API origins, cookie, credentials: include, and Better Auth base URL |
| Verification/reset email absent | local server log or Resend sender/domain/provider error |
| Magic link already invalid | expiry, exact origin, or expected single-use replay protection |
| Google button/action fails | both Google credentials and SERVER_URL/auth/callback/google registration |
| Requests all share one rate bucket | selected TRUSTED_PROXY_PROFILE and provider forwarding shape |
| Delete account returns 409 | cancel Billing, then wait for the recorded paid period to end |
Do not weaken verification, cookie policy, trusted origins, account-linking policy, proxy trust, or rate limits to make these failures disappear.