Redis
Rate limiting for write endpoints — optional but recommended in production.
Redis is used for rate limiting only. If not configured, rate limiting is disabled (every request passes).
Setup options
Local dev (docker-compose)
Bundled in docker-compose.yml — runs on localhost:6379, no password:
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0bun run db:start brings Postgres + Redis up together.
Upstash (free tier)
- upstash.com → Create Database
- Copy the TCP endpoint (not the REST URL — Launchy uses
ioredis):
REDIS_HOST=YOUR_ENDPOINT.upstash.io
REDIS_PORT=6379
REDIS_PASSWORD=...Self-hosted VPS
Standard Redis install with password auth. Bind to 0.0.0.0 only behind a firewall that allows only your app's IP.
What's rate-limited
Every rate-limit call lives in lib/rate-limit.ts. Active limits shipped in the template:
| Route | Key | Limit |
|---|---|---|
POST /api/upload | upload:{userId} | 20 / hour |
POST /api/submissions | submissions:{userId} | submissions_per_day setting / day |
POST /api/sponsor/checkout | sponsor-checkout:{ip} | 10 / hour |
POST /api/sponsor/upload | sponsor-upload:{ip} | 10 / hour |
POST /api/template-checkout (sales site only) | template-checkout:{ip} | 10 / hour |
| Vote server action | per user | votes_per_window / votes_window_seconds |
| Comment server action | per user | comments_per_window / comments_window_seconds |
Vote and comment limits come from settings (lib/settings-shared.ts) — adjustable in /admin/settings.
Important: there is no Redis lock on the weekly-batch cron. Idempotency is enforced by checking the DB for an existing batch row before creating one. Cron authentication uses a timing-safe comparison on
CRON_SECRET.
How the limit works
Fixed-window counter with a time-bucketed key:
const windowKey = `rl:${key}:${Math.floor(Date.now() / 1000 / windowSeconds)}`
await redis.multi()
.incr(windowKey)
.expire(windowKey, windowSeconds)
.exec()Simple and sufficient. For strict sliding windows, switch to @upstash/ratelimit.
Dev override
In dev (NODE_ENV !== "production"), the rate limit helper bypasses checks entirely. This is hardcoded at the top of lib/rate-limit.ts.
Inspecting live
All rate-limit keys are prefixed with rl:.
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASSWORD
KEYS "rl:vote:*" # active per-user vote counters
TTL "rl:vote:user_abc:12345" # time until reset (12345 = time bucket)
DEL "rl:vote:user_abc:12345" # manually reset oneThe numeric suffix is the time bucket (floor(nowSeconds / windowSeconds)), so the key rolls over automatically at the end of each window.
Memory footprint
Rate-limit keys are tiny (a single integer with TTL). Launchy's Redis usage stays under 1 MB for any reasonable user base — free tier of any provider is plenty.
Common issues
Connection refused — Redis not running (docker ps) or wrong host/port.
"WRONGPASS" — REDIS_PASSWORD set on client but server has no auth, or vice versa.
Legitimate users getting rate-limited — Raise the limits in the relevant settings (votes_per_window, submissions_per_day, etc.), or edit lib/rate-limit.ts for the ones hardcoded there.