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.

HeaderValueProtection
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS
X-Frame-OptionsSAMEORIGINClickjacking
X-Content-Type-OptionsnosniffMIME sniffing
Referrer-Policyno-referrerReferrer leaks
Cross-Origin-Opener-Policysame-originProcess isolation
Cross-Origin-Resource-Policysame-originCross-origin loads
Cross-Origin-Embedder-Policyrequire-corpCross-origin embeds
X-XSS-Protection0Disables 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

PresetLimitWindowUse case
authLimiter5 req60sLogin, signup, OTP
authStrictLimiter3 req60sForgot password
mutationLimiter20 req60sAll write actions (offers, profile, boost, etc.)

API route presets

PresetLimitWindowRoutes
apiLimiter30 req60s/checkout, /withdraw, /items
aiLimiter10 req60s/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.

EventLimitWindow
send_message3060s
typing1010s
heartbeat530s
conversation_focus2060s
conversation_blur2060s
join_conversation1060s

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

VariableRequiredPurpose
UPSTASH_REDIS_REST_URLOptionalRedis for rate limiting
UPSTASH_REDIS_REST_TOKENOptionalRedis auth token
ARCJET_KEYOptionalBot detection (starts with ajkey_)

Graceful degradation

FeatureWithout env varsWith env vars
Security headersAlways activeAlways active
Server action rate limitingSilently skipsActive
API route rate limitingSilently skipsActive
Socket rate limitingAlways active (in-memory)Always active
Arcjet bot detectionSilently skipsActive