Security hardening
Rate limiting, security headers, and bot detection
Overview
The application implements multiple security layers that degrade gracefully — everything works in local dev without env vars.
Security headers (Nosecone)
Applied to every response via middleware (apps/web/proxy.ts). Always active, no env vars required.
| Header | Value | Protection |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Forces HTTPS |
X-Frame-Options | SAMEORIGIN | Clickjacking |
X-Content-Type-Options | nosniff | MIME sniffing |
Referrer-Policy | no-referrer | Referrer leaks |
Cross-Origin-Opener-Policy | same-origin | Process isolation |
Cross-Origin-Resource-Policy | same-origin | Cross-origin loads |
Cross-Origin-Embedder-Policy | require-corp | Cross-origin embeds |
X-XSS-Protection | 0 | Disables buggy legacy filter |
CSP is disabled (needs per-app configuration).
Rate limiting (Upstash Redis)
Requires UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN. Without them, all rate limiting silently skips.
Server action presets
| Preset | Limit | Window | Use case |
|---|---|---|---|
authLimiter | 5 req | 60s | Login, signup, OTP |
authStrictLimiter | 3 req | 60s | Forgot password |
mutationLimiter | 20 req | 60s | All write actions (offers, profile, boost, etc.) |
API route presets
| Preset | Limit | Window | Routes |
|---|---|---|---|
apiLimiter | 30 req | 60s | /checkout, /withdraw, /items |
aiLimiter | 10 req | 60s | /ai/analyze-image, /ai/generate-* |
Action clients and inline rate limiting
Server actions use clients from @repo/features/action-clients for auth, with rate limiting applied inline per action:
publicActionClient— no auth, no rate limiting (read-only: shipping quotes, CEP lookup)authActionClient— session check,{ session, userId }in context
Rate limiting is inlined at the top of each action using the appropriate limiter from @repo/rate-limit/limiters. This makes limits visible per action and customizable without extra abstraction.
Socket server rate limiting (in-memory)
The socket server uses an in-memory sliding window limiter. Single Fly.io instance means no Redis needed.
| Event | Limit | Window |
|---|---|---|
send_message | 30 | 60s |
typing | 10 | 10s |
heartbeat | 5 | 30s |
conversation_focus | 20 | 60s |
conversation_blur | 20 | 60s |
join_conversation | 10 | 60s |
On rate limit exceeded, the server emits { type: "RATE_LIMITED", message: "Too many requests" } and drops the event.
Arcjet bot detection (dormant)
Wired into /api/v1/checkout and /api/v1/withdraw. Activates when ARCJET_KEY env var is set. Adds Shield (common attack patterns) + bot detection.
Environment variables for deployment
| Variable | Required | Purpose |
|---|---|---|
UPSTASH_REDIS_REST_URL | Optional | Redis for rate limiting |
UPSTASH_REDIS_REST_TOKEN | Optional | Redis auth token |
ARCJET_KEY | Optional | Bot detection (starts with ajkey_) |
Graceful degradation
| Feature | Without env vars | With env vars |
|---|---|---|
| Security headers | Always active | Always active |
| Server action rate limiting | Silently skips | Active |
| API route rate limiting | Silently skips | Active |
| Socket rate limiting | Always active (in-memory) | Always active |
| Arcjet bot detection | Silently skips | Active |