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.

The limiters

Defined once in packages/rate-limit/limiters.ts, each with its scope and the reason it exists. That table is the source of truth; this is a snapshot.

LimiterLimitKeyed on
auth5 / 60semail or IP
authStrict3 / 60semail or IP
api30 / 60sIP
chat5 / 60ssender user id
helpFeedback20 / 60sIP
ai10 / 60suser id
searchAsk10 / 60sIP
upload60 / 60suser id
mutation20 / 60suser id

authStrict is tighter than auth because each of those actions sends mail to an address the caller chose. chat is an anti-spam bound on blasting sellers, not a typing-speed budget.

Action clients and inline rate limiting

Server actions use clients from @repo/lib/action-clients for auth, with rate limiting applied inline per action:

  • publicActionClient — no auth (read-only work: shipping quotes, postal lookup)
  • authActionClient — session check; { session, userId } in context
  • staffActionClient(capability) — authActionClient plus the staff table in @repo/supabase/staff (admin: every capability; moderator: members, listings, community, dispute triage); writes one user_activity_logs row per successful act, against the subject the action names with ctx.audit
  • staffReadActionClient(capability) — the same gate with no ledger row, for the few staff reads not yet on a route handler

Rate limiting is applied at the top of each action with the right limiter from @repo/rate-limit/limiters, so the limit is visible where the action is, not hidden in a wrapper.

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
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