Authentication & Admin Architecture
Deep dive into how this template handles Better Auth sessions, OAuth, RBAC, and business data modeling for credits and quota.
Authentication & Admin
This document is the source of truth for how authentication and admin authorization work in this template.
It covers:
- The exact Better Auth model used here
- How email/password and Google OAuth flows persist sessions
- Why
users,accounts,sessions, andverificationsare separate - What is auth data vs what is business data (credits, quotas, permits)
- How admin RBAC is implemented in this codebase
1) Authentication Model Used in This Template
This project uses cookie-based, server-managed sessions backed by Postgres. It is not JWT-first auth by default.
Why:
- Better Auth is configured with Drizzle + Postgres adapter in
src/lib/auth.ts - Session storage maps to the
sessionstable insrc/db/schema.ts nextCookies()plugin is enabled- No JWT plugin is enabled for primary auth
Practical implication:
- Browser stores an HttpOnly auth cookie (session token)
- Server validates session token against
sessions - Session row points to the user identity
JWT support exists in Better Auth, but it is opt-in and does not replace the core DB session model used here.
2) Core Auth Tables and Responsibilities
Better Auth intentionally splits identity, credentials, and login state.
users (identity profile)
Represents who the person is in your product.
Typical fields:
- Display profile (
nickname,avatar_url) - Role (
role) - Verification/status flags
- Created/updated timestamps
In this template, additional mapped fields include:
uuid(generated on create)role
This table should stay relatively stable.
accounts (authentication methods)
Represents how they can sign in.
- One user can have multiple accounts/providers
- Stores provider identity and provider tokens
Typical fields:
provider_idaccount_id- Password hash (email/password)
- OAuth access token / refresh token
- OAuth token expiration timestamps
This table is credential-focused and security-sensitive.
sessions (login state)
Represents active login sessions for your app.
Typical fields:
user_idtokenexpires_atip_addressuser_agent
Properties:
- Session expiration controls whether user stays logged in
- Revoking sessions is done by deleting/invalidating rows
- Sliding refresh behavior is controlled by session policy
verifications (one-time flows)
Used for temporary tokens in flows such as:
- Password reset
- Email verification
- Magic link / OTP (if enabled)
Normal sign-in/sign-out does not continuously write here, so seeing very few rows is expected.
3) End-to-End Auth Procedure in This Codebase
3.1 Email + Password Sign-Up
- Client submits sign-up form
- Better Auth creates a
usersrow databaseHooks.user.create.beforeinsrc/lib/auth.tsensuresuuidexists- Better Auth creates an
accountsrow for email/password - Better Auth creates a
sessionsrow - Session cookie is set in response
databaseHooks.user.create.aftertriggers welcome email async
Result: user is signed in immediately.
3.2 Email + Password Sign-In
- Client submits credentials
- Better Auth validates password hash in
accounts - Better Auth creates/refreshes session in
sessions - Session cookie is set/updated
3.3 Google OAuth Sign-In
- User clicks Google sign-in
- OAuth redirect + callback flow executes
- Google returns account identity (
sub) and profile claims - Better Auth finds or creates user
- Better Auth creates/updates
accountsrow for providergoogle - Better Auth creates session row
- Session cookie is set
4) Important OAuth Identity Rule
Google sign-in does not require a Gmail address.
- Users may use Gmail, Outlook, Yahoo, or custom domain email tied to a Google account
- Identity should be treated as provider account identity (
provider_id,account_id/sub), not email domain
Never treat email domain as proof of provider identity.
5) Session Expiration vs OAuth Token Expiration
These are separate lifecycles.
App session lifecycle (sessions)
- Controls login state in your app
- If session expires,
getSession()returns null - User must sign in again
Provider token lifecycle (accounts)
- Controls your backend's ability to call provider APIs
- Access tokens expire quickly
- Refresh tokens may be used to get new access tokens
Critical point:
- Better Auth session expiry does not auto-log users back in via Google refresh token
- Refresh token usage is for provider API calls, not app authentication
6) Why Sessions and OAuth Tokens Must Stay Separate
This separation is required for predictable security:
- OAuth refresh token is not app identity proof
- Session revocation must remain meaningful
- Explicit re-authentication should be possible
- Security/audit boundaries stay clear
Auto-recreating app sessions from provider refresh tokens would weaken revocation and auditability.
7) Authorization (RBAC) in This Template
Authentication answers "who are you". Authorization answers "what can you do".
Roles are DB-driven in users.role:
useradmin_roadmin_rw
Role checks are implemented in src/lib/authz.ts:
requireAdminRead()-> allowsadmin_roandadmin_rwrequireAdminWrite()-> allowsadmin_rw
Admin layout guard: src/app/(admin)/layout.tsx.
8) Current Admin Surfaces and APIs
Admin pages
- Dashboard:
src/app/(admin)/admin/page.tsx - Users list:
src/app/(admin)/admin/users/page.tsx - User detail/quota/credits:
src/app/(admin)/admin/users/[uuid]/page.tsx - Orders list/status view:
src/app/(admin)/admin/orders/page.tsx - Feedbacks:
src/app/(admin)/admin/feedbacks/page.tsx - Reservations:
src/app/(admin)/admin/reservations/page.tsx - Affiliates:
src/app/(admin)/admin/affiliates/page.tsx
Admin API routes
GET /api/admin/users-> paginated users listGET /api/admin/users/[uuid]-> user detail + monthly usage + credits summaryPATCH /api/admin/users/[uuid]-> updatemonthlyCreditsQuotaGET /api/admin/users/[uuid]/credits-> credit summary + ledgerPOST /api/admin/credits/grant-> grant creditsPOST /api/admin/credits/adjust-> positive/negative credit adjustmentsGET /api/admin/orders-> paginated orders with status filter (all|paid|created|deleted)
9) Cookie Sessions vs JWT (What You Actually Have)
Current setup:
- Session cookie contains a session token
- Server checks token against
sessionstable - User resolved from DB-backed session
So this is DB-backed session auth, not pure stateless JWT auth.
Pure JWT model (not current default):
- Token itself carries claims
- Server verifies signature each request
- Usually no DB lookup required for basic validation
10) Why BETTER_AUTH_SECRET Is Still Required
Even when session tokens are random, secret is still needed for cryptographic signing/verification flows.
Better Auth resolves secret from:
- Explicit
secretoption in config BETTER_AUTH_SECRETAUTH_SECRET- Internal fallback (dev convenience, not production-safe)
So app may still run without env secret, but that is not a safe production posture.
Use a strong stable secret per environment. Changing secret invalidates existing signed auth artifacts and can log users out.
11) Business Data Modeling: What Belongs Outside Auth Tables
Do not overload auth tables with high-churn business counters.
Keep auth tables auth-focused
users: identity/profile/roleaccounts: login providers + credentials/tokenssessions: login stateverifications: one-time verification tokens
Put product/billing state in domain tables
Recommended pattern:
- Current-state table (fast reads)
- Append-only ledger/events (audit/rebuild)
- Entitlements table (feature permits)
Example structures:
-
user_balanceuser_id(PK)credits_balancequota_limitquota_usedplan_idbilling_period_startbilling_period_endupdated_at
-
usage_eventsiduser_idtypeamountcost_creditsmetadata(JSON)created_at
-
entitlementsuser_idkeyvaluesource(plan/admin/promo)expires_at
This keeps auth stable while business logic stays auditable.
12) Quota, Credits, and Transaction Safety
When spending credits/quota:
- Use DB transactions
- Check available balance/quota
- Insert usage ledger event
- Apply balance update conditionally
This avoids race conditions and negative balances under concurrency.
In this template:
- Task usage is tracked in
tasks - Credit ledger is tracked in
credits - Monthly quota (
users.monthly_credits_quota) is enforced in task orchestration before credit spend
13) Forward Compatibility: Organizations
If you expect team accounts:
- Add
organizations - Move balance/quota/entitlements to org level
- Treat personal accounts as single-member orgs
Designing for org-capable ownership early avoids painful refactors later.
14) Environment and Setup Checklist
Minimum auth-related env:
BETTER_AUTH_URLNEXT_PUBLIC_AUTH_BASE_URLBETTER_AUTH_SECRET(strong random)GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(if Google enabled)
If schema changed, run migrations:
pnpm drizzle-kit generate --config src/db/config.ts
pnpm drizzle-kit migrate --config src/db/config.tsFinal Summary
- This project uses cookie-based DB sessions (not JWT-first)
- OAuth identity is provider-account based, not email-domain based
- Session lifecycle and provider-token lifecycle are separate
- Auth tables should stay auth-only
- Credits/quota/permits should live in domain tables with ledger support
- Admin RBAC is DB role-based and guarded server-side
This architecture gives strong security boundaries, good operational control, and clear scaling paths.
Database Setup
Configure Postgres and Drizzle ORM — the backbone for auth, billing, storage, tasks, and app tables.
Stripe Setup
Use Stripe Checkout for payments and finalize via webhooks to grant credits. Configure environment variables, create sessions, handle callbacks, and process signed webhook events.