Summary
The backend exposes OAuth callback routes, card-view endpoints, and profile mutation routes with no inbound rate limiting in place. Without it, the server is open to brute-force attacks on auth flows, credential stuffing via OAuth endpoints, and abuse of high-cost analytics write paths. Redis is already in the stack — this implements a Redis-backed, per-IP and per-user rate limiter applied as a Fastify plugin across all sensitive routes.
Contexts
The project uses Fastify and already provisions Redis (REDIS_URL in .env.example) primarily for QR session caching (issue #46). The ENCRYPTION_KEY and JWT_SECRET in .env.example confirm that auth token handling is security-sensitive. However, none of the backend routes currently apply any throttling — an attacker can hammer the OAuth callback or token refresh endpoints indefinitely. @fastify/rate-limit supports Redis as a backing store natively, making this a well-scoped addition that reuses existing infrastructure without introducing new dependencies.
Tasks
Install and register @fastify/rate-limit as a global Fastify plugin backed by the existing Redis connection
Define a tiered rate limit strategy: strict limits for auth-related routes, moderate limits for profile mutation routes, and relaxed limits for public read routes
Apply per-IP limiting on unauthenticated routes and per-user-ID limiting on authenticated routes (so legitimate users behind shared IPs are not incorrectly throttled)
Return standard Retry-After and X-RateLimit-* response headers on all rate-limited routes so clients can handle 429s gracefully
Add a custom error response shape for 429s that matches the project's existing error format
Write integration tests covering: requests under the limit pass, requests over the limit return 429 with correct headers, per-user and per-IP scoping behaves independently
Document the rate limit tiers and configuration in CONTRIBUTING.md
Acceptance Criteria
All auth-related routes return 429 Too Many Requests with a Retry-After header when the threshold is exceeded
Authenticated routes throttle per user ID, not per IP
Rate limit state is stored in Redis — a server restart does not reset counters
Integration tests for all three tiers pass in CI
No regression on existing route behaviour under normal load
Docs updated in CONTRIBUTING.md
Area
backend
Difficulty
advanced
Summary
The backend exposes OAuth callback routes, card-view endpoints, and profile mutation routes with no inbound rate limiting in place. Without it, the server is open to brute-force attacks on auth flows, credential stuffing via OAuth endpoints, and abuse of high-cost analytics write paths. Redis is already in the stack — this implements a Redis-backed, per-IP and per-user rate limiter applied as a Fastify plugin across all sensitive routes.
Contexts
The project uses Fastify and already provisions Redis (REDIS_URL in .env.example) primarily for QR session caching (issue #46). The ENCRYPTION_KEY and JWT_SECRET in .env.example confirm that auth token handling is security-sensitive. However, none of the backend routes currently apply any throttling — an attacker can hammer the OAuth callback or token refresh endpoints indefinitely. @fastify/rate-limit supports Redis as a backing store natively, making this a well-scoped addition that reuses existing infrastructure without introducing new dependencies.
Tasks
Install and register @fastify/rate-limit as a global Fastify plugin backed by the existing Redis connection
Define a tiered rate limit strategy: strict limits for auth-related routes, moderate limits for profile mutation routes, and relaxed limits for public read routes
Apply per-IP limiting on unauthenticated routes and per-user-ID limiting on authenticated routes (so legitimate users behind shared IPs are not incorrectly throttled)
Return standard Retry-After and X-RateLimit-* response headers on all rate-limited routes so clients can handle 429s gracefully
Add a custom error response shape for 429s that matches the project's existing error format
Write integration tests covering: requests under the limit pass, requests over the limit return 429 with correct headers, per-user and per-IP scoping behaves independently
Document the rate limit tiers and configuration in CONTRIBUTING.md
Acceptance Criteria
All auth-related routes return 429 Too Many Requests with a Retry-After header when the threshold is exceeded
Authenticated routes throttle per user ID, not per IP
Rate limit state is stored in Redis — a server restart does not reset counters
Integration tests for all three tiers pass in CI
No regression on existing route behaviour under normal load
Docs updated in CONTRIBUTING.md
Area
backend
Difficulty
advanced