| Yup Validation |
Email format, password min length |
Frontend |
| Submit Cooldown |
2s lock + ref guard against rapid-fire spam |
Frontend |
| Next.js Middleware |
Checks cookie presence, redirects unauthenticated users to /login |
Frontend SSR
|
| CORS |
Blocks requests from unknown origins |
Backend |
| Rate Limiting |
100 req/min in production (prevents brute force) |
Backend |
| Helmet |
Security headers: XSS protection, clickjacking prevention, CSP in prod |
Backend |
| Better Auth Validation |
Email format, password min 10 / max 128, duplicate check |
Backend |
| Password Hashing |
scrypt (Better Auth default) — plaintext never stored |
Backend |
| HttpOnly Cookie |
JavaScript cannot access session token (XSS protection) |
Cookie |
| CSRF Protection |
Multi-layered: SameSite=Lax cookies, Origin/Referer header validation,
and Fetch Metadata (Sec-Fetch-Site/Mode/Dest) checks — all handled automatically by
Better Auth |
Cookie +
Backend |
| Secure Flag |
Set automatically by Better Auth in production — cookie is only sent over HTTPS, preventing token
interception on unencrypted connections |
Cookie |
| credentials: "include" |
Apollo Client config — ensures session cookies are attached to GraphQL requests sent from the SSR
layer to the backend |
Frontend SSR
|
| preExecution Hook |
Throws if no valid session — blocks resolver execution entirely |
Backend GQL
|
| errorFormatter |
Sanitizes error details in production (no stack traces leaked) |
Backend GQL |
| Query Depth Limit |
Max 12 levels of nesting (prevents DoS via deep queries) |
Backend GQL |
| cookieCache |
5 min session cache — performance vs. instant revocation tradeoff |
Backend |
| Frontend CSP |
Content-Security-Policy in next.config.js — restricts script/connect/frame sources. Dynamic
connect-src includes GRAPHQL_URL and AUTH_URL origins
|
Frontend |
| X-Frame-Options |
Set to DENY — prevents the app from being embedded in iframes on other domains
(clickjacking protection) |
Frontend |
| X-Content-Type-Options |
Set to nosniff — prevents browsers from MIME-type sniffing, blocking attacks that
serve executable content disguised as other file types |
Frontend |
| HSTS |
Strict-Transport-Security with max-age 2 years, includeSubDomains, preload —
forces HTTPS, prevents SSL stripping and downgrade attacks |
Frontend |
| Permissions-Policy |
Disables camera, microphone, and geolocation APIs
— reduces attack surface by blocking access to unnecessary browser capabilities |
Frontend |
| X-XSS-Protection |
Set to 0 (disabled) as recommended by OWASP — legacy browser XSS auditor was
removed from modern browsers and could itself introduce vulnerabilities |
Frontend |
| Referrer-Policy |
strict-origin-when-cross-origin — sends full URL for same-origin requests, only
origin for cross-origin, nothing on HTTPS→HTTP downgrade |
Frontend |
| Cache-Control |
no-store, no-cache on all /api/auth/*
responses — prevents proxies and browsers from caching session tokens or auth data |
Backend |
| Env Validation |
Fail-fast on startup — validateEnv() throws if DATABASE_URL or BETTER_AUTH_SECRET
missing |
Backend |
| Protected Layout |
Server-side getSession() fetch in (protected)/layout.tsx — validates session
against backend, not just cookie presence |
Frontend SSR
|
| Error Code Mapping |
~40 Better Auth error codes grouped into i18n keys via useAuthErrorMessage. Multiple
codes map to the same message (e.g. USER_NOT_FOUND, CREDENTIAL_ACCOUNT_NOT_FOUND →
INVALID_EMAIL_OR_PASSWORD) to prevent user enumeration |
Frontend i18n
|