All requests pass through src/proxy.ts (Next.js Edge Middleware) which handles:
Applied to every response:
| Header | Value |
|---|---|
X-Frame-Options |
SAMEORIGIN |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Content-Security-Policy |
See below |
default-src 'self'
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://challenges.cloudflare.com
https://js.stripe.com
https://dahlia.stripe.com
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob: https:
font-src 'self' data:
connect-src 'self'
https://challenges.cloudflare.com
https://api.resend.com
https://cdn.jsdelivr.net
https://api.stripe.com
https://api.modrinth.com
frame-src
https://challenges.cloudflare.com
https://js.stripe.com
https://hooks.stripe.com
object-src 'none'
base-uri 'self'
Extra headers added to /admin/*:
| Header | Value |
|---|---|
X-Robots-Tag |
noindex, nofollow |
Cache-Control |
no-store |
Backed by Redis sliding window algorithm (src/lib/rate-limit.ts).
No hard 429 errors — excessive requests receive an artificial delay instead:
| Endpoint | Limit | Window |
|---|---|---|
sign-in |
10 requests | 5 minutes |
sign-up |
8 requests | 5 minutes |
forget-password |
5 requests | 5 minutes |
reset-password |
8 requests | 5 minutes |
send-verification-email |
5 requests | 5 minutes |
When limit is exceeded:
- Remaining > 0 → 1 second delay added
- Remaining = 0 → 3 second delay added
- Max delay: 5 seconds
This prevents brute force without showing errors to legitimate users with typos.
No rate limiting on /admin routes (admin users only).
If Redis is unavailable, rate limiting fails open — requests are allowed through. This prevents Redis downtime from taking down the app.
Handled by better-auth:
- Email/password with verification
- 2FA (TOTP)
- Session-based (HTTP-only cookies)
- Admin impersonation
Cloudflare Turnstile on sign-up and contact forms. Verified server-side via TURNSTILE_SECRET_KEY.
Stripe webhooks are verified using STRIPE_WEBHOOK_SECRET (HMAC signature). Requests without a valid signature return 400.
Cron endpoint (/api/cron/cleanup) requires Authorization: Bearer <CRON_SECRET> header.