Rate Limiting Library
All token-gate APIs share a Cloudflare Durable Object that tracks request volume, SIWE nonces, and quotas. The server-side helpers in dapp/app/lib/rateLimit/rateLimit.server.ts wrap that worker so every request—whether it lands on Vercel, a local dev server, or another host—pulls from the exact same counters.
The library exposes:
isRateLimitEnabled()– true only whenNEXT_PUBLIC_ENABLE_STATE_WORKER=true, the state worker creds exist, and the worker client can initialize.checkRateLimitWithNonce()– applies a per-endpoint limiter, an optional global limiter, and opportunistically returns the cached nonce for SIWE flows.- Helpers for extracting client identifiers and translating limiter metadata into HTTP headers (
X-RateLimit-*,Retry-After).
Architecture Overview
- Client identifier – derived from
x-forwarded-for(production) or the request socket/IP (everywhere else) viagetIdentifier. - Per-endpoint limiter – configuration-driven (
RATE_LIMITER_CONFIGS) and enforced by the Durable Object (state:ratelimit:checkaction). - Global limiter – optional 100 req/hour bucket for most endpoints (token-status polling is exempt).
- Nonce lookup – the same worker stores SIWE nonces; if one already exists for the identifier it is injected into the limiter response to avoid redundant writes.
Configuration
NEXT_PUBLIC_ENABLE_STATE_WORKER=true
STATE_WORKER_URL=https://ritoswap-state-worker.worker.dev/state
STATE_WORKER_API_KEY=your_shared_secretDisable the flag locally to bypass rate limits entirely while iterating. In production, the worker must be reachable before any route can perform SIWE or rate-limited operations.
Limiter Catalog
| Limiter | Limit | Window | Prefix | Purpose |
|---|---|---|---|---|
nonce | 10 | 60s | rl:nonce: | SIWE nonce generation attempts |
gateAccess | 5 | 60s | rl:gate-access: | Main gate unlock endpoint |
formSubmissionGate | 3 | 60s | rl:form-submission: | Single-use message submission endpoint |
tokenStatus | 60 | 60s | rl:token-status: | High-frequency polling for UI updates (no global limiter) |
global | 100 | 3600s | rl:global: | Catch-all protection shared across most routes |
The configuration lives in RATE_LIMITER_CONFIGS and feeds both the worker payload (limit, windowSeconds) and the doc site.
Request Lifecycle
1. Identifier Extraction
getIdentifier(req) prioritizes x-forwarded-for, falls back to x-real-ip, and finally req.ip. Development requests default to 127.0.0.1 so the limiter remains stable between reloads.
2. Per-Route Check
applyLimiter(type, identifier) calls getStateClient().checkRateLimit(...). Failures (network issues, worker downtime) are logged and treated as success: true to avoid blocking the app.
3. Optional Global Check
For everything except tokenStatus, a second limiter (global) is evaluated to cap total throughput per identifier.
4. Nonce Lookup
When SIWE is enabled, the helper also tries getStateClient().getNonce(identifier) so /api/nonce can reuse the cached value without performing another worker round trip.
5. Response Headers
Successful checks compute Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining. Routes include those headers on 429 responses (and optionally on successful responses).
Surfacing to APIs
Routes typically integrate like this:
const result = await checkRateLimitWithNonce(request, 'gateAccess');
if (!result.success) {
return rateLimitResponse(result, 'Rate limit exceeded');
}
// result.nonce may contain a SIWE nonce for reuse/api/gate-accesspasses'gateAccess'and leavesincludeGlobalattrue./api/token-status/[tokenId]uses'tokenStatus'and explicitly setsincludeGlobal=falseto keep polling responsive./api/form-submission-gateuses'formSubmissionGate'before parsing any user content, protecting the most expensive code path.
Testing & Debugging
dapp/app/lib/rateLimit/__tests__/rateLimit.server.test.ts verifies:
- Identifier extraction fallbacks
- Global limiter bypass for
tokenStatus - Error resilience when the worker is unreachable
- Header/metadata formatting for 429 responses
For manual diagnostics, /api/debug/status responds in development with booleans indicating whether the state worker URL and API key are configured.
Because all rate limiting, nonce storage, and quota tracking shares the same Durable Object, never expose its STATE_SERVICE_AUTH_TOKEN publicly. The worker authenticates every request coming from the Next.js app.