Thanks for your interest in contributing. RateLock is an API rate limiter as a service — a Redis-backed engine, a REST API, a dashboard, and drop-in SDKs. This guide covers how to get the project running, how the codebase is laid out, and what we look for in a pull request.
New here? Issues labelled good first issue are scoped to be completable
without deep knowledge of the whole system.
- Code of Conduct
- Ways to Contribute
- Getting Set Up
- Project Layout
- How the Hot Path Works
- Development Workflow
- Coding Conventions
- Working with the Database
- Testing Your Changes
- Commit Messages
- Opening a Pull Request
- Reporting Bugs
- Proposing Features
Be respectful and constructive. Assume good faith, critique the code rather than the person, and keep discussion focused on the technical merits. Harassment or personal attacks are not tolerated.
You don't need to write code to help:
- Report bugs with clear reproduction steps
- Improve documentation — the README, SDK docs, or in-code comments
- Write an SDK for a language we don't cover yet (Go, Ruby, PHP, Rust…)
- Improve the dashboard UI or accessibility
- Add tests — the project currently has no test suite, so this is high-impact
- Triage issues by reproducing reports and adding detail
- Docker and Docker Compose
- Node.js 24+ (for running services outside Docker)
- Python 3.10+ (only if working on the Python SDK)
git clone git@github.com:TheCodeHeist-Coder/ratelock.git
cd ratelock
# Optional: override defaults. Compose works without this file.
cp .env.example .env
docker compose up -d --buildOnce healthy:
| Service | URL |
|---|---|
| Dashboard | http://localhost:8080 |
| Backend API | http://localhost:4000 |
| Health check | http://localhost:4000/health |
Migrations are applied automatically when the backend container boots. Create an account from the dashboard's Sign up page, then add a project to get an API key.
Run Postgres and Redis in Docker, everything else on your machine so you get hot reload:
# Infrastructure only
docker compose up db redis -d
# Backend — terminal 1
cd backend
cp .env.example .env
npm install
npx prisma generate
npx prisma migrate dev
npm run dev # API on :4000
# Frontend — terminal 2
cd frontend
npm install
npm run dev # Vite on :5173, proxies /api → :4000Note on Redis: compose does not publish Redis to the host — the backend reaches it as
redis:6379over the compose network. For local backend dev,backend/.envpoints atlocalhost:6379, so either addports: ["6379:6379"]to theredisservice or run a local Redis.
ratelock/
├── backend/ Express + TypeScript API and rate-limit engine
│ ├── prisma/
│ │ ├── schema.prisma Data model (User, Project, Rule, Event, Alert)
│ │ └── migrations/
│ └── src/
│ ├── app.ts Express app, middleware, route mounting
│ ├── index.ts Server entry point
│ ├── routes/ Thin routers — no business logic here
│ ├── controller/ Request/response handling, validation
│ ├── services/ Business logic (rateLimitService, analyticsService)
│ ├── middleware/ auth (JWT), projectOwner (ownership checks)
│ ├── config/redis.ts ioredis client
│ ├── lib/prisma.ts Prisma client singleton
│ └── generated/ Prisma client output — never edit by hand
│
├── frontend/ React 19 + Vite + Tailwind v4 dashboard
│ └── src/
│ ├── screens/ Route-level pages
│ ├── components/ Shared UI
│ │ ├── charts/ AreaChart, Sparkline
│ │ └── dashboard/ DashboardLayout (sidebar shell)
│ ├── stores/ Zustand stores (authStore, projectStore)
│ ├── lib/api.ts Axios instance, JWT interceptors
│ └── index.css Tailwind theme tokens + component primitives
│
├── sdk/
│ ├── node/ @ratelock/express middleware
│ └── python/ ratelock package (FastAPI/Flask middleware)
│
├── nginx/ Serves the built frontend, proxies /api → backend
└── docker-compose.yml db, redis, backend, web
Keep the boundaries clean — this is the convention most PRs get feedback on:
route → controller → service → prisma / redis
- Routes only wire paths to controllers.
- Controllers parse input, call a service, shape the response.
- Services hold the business logic and own all Prisma/Redis access.
Don't call Prisma directly from a controller.
The most performance-sensitive code is the check endpoint that SDKs call on every inbound request to a protected API:
POST /api/rl/check
Headers:
X-RL-Key <project api key>
X-RL-Endpoint <path> (default "/")
X-RL-Method <method> (default "GET")
X-RL-IP <client ip> (optional)
It lives in backend/src/services/rateLimitService.ts,
reached via routes/rateLimit.ts → controller/rateLimitController.ts.
Because it runs on every request, treat it as latency-critical:
- Redis is the source of truth for counters — never add a Postgres read to this path
- Avoid extra round trips; batch Redis operations where you can
- Event writes for analytics must not block the allow/deny decision
- Benchmark before and after if you touch it
Everything under /api/v1/* (auth, projects, rules, alerts) is the control
plane and is not latency-critical in the same way.
-
Fork the repo and create a branch off
dev:git checkout dev git pull origin dev git checkout -b fix/sliding-window-off-by-one
Branch naming:
feat/…,fix/…,docs/…,refactor/…,test/… -
Make your change, keeping it focused. One concern per PR.
-
Verify it builds and passes lint (see Testing Your Changes).
-
Commit with a clear message, then push and open a PR against
dev.
Target
dev, notmain.maintracks released state.
- ES modules throughout. The backend is
"type": "module"— relative imports need the.jsextension even in.tsfiles:import { prisma } from '../lib/prisma.js'; // correct import { prisma } from '../lib/prisma'; // breaks at runtime
- Prefer explicit types on exported functions; let inference handle locals.
- Avoid
any. If you genuinely need an escape hatch, add a comment explaining why.
Write comments that explain why, not what. The existing code follows this — match it. Skip comments that restate the line below them.
// Redis is the source of truth here; a Postgres read would add ~15ms
// to a path that runs on every customer request.
const count = await redis.incr(key);- Tailwind v4 with the theme defined in
frontend/src/index.cssunder@theme. Use the semantic tokens (brand-*,ink-*,surface-*) rather than raw hex values so the palette stays changeable in one place. - Reuse the component primitives already defined in
index.css—.card,.btn-primary,.input,.badge,.panel-head,.segmented— instead of re-styling from scratch. - Charts live in
components/charts/.AreaChartandSparklineare generic; extend them rather than writing a one-off SVG. - The landing page keeps its neon-green identity; the dashboard and auth pages use the blue theme. Don't unify them without discussion.
- State goes in Zustand stores under
stores/, not in prop-drilled component state.
Follow the existing shapes. Errors go through utils/errot.ts's errorResponse
helper. (Yes, the filename has a typo — renaming it is a welcome standalone PR.)
Schema lives in backend/prisma/schema.prisma. After editing it:
cd backend
npm run generate # regenerate the Prisma client
npm run migrate # create + apply a migration (prompts for a name)Available scripts (backend/package.json):
| Script | Runs |
|---|---|
npm run build |
tsc -b |
npm run dev |
build, then start dist/index.js |
npm run start |
start dist/index.js |
npm run generate |
prisma generate |
npm run migrate |
prisma migrate dev |
npm run deploy |
prisma migrate deploy (production) |
The README's "Prisma Commands" section currently lists
db:generate,db:migrate,db:seedanddb:studio. Those scripts do not exist — use the table above. There is also no seed script yet; adding one is a genuinely useful contribution.
Never edit backend/src/generated/ — it is Prisma output and is
regenerated on every npm run generate.
Migrations are committed to the repo. Don't hand-edit an applied migration; create a new one.
There is no automated test suite yet. Until there is, verify manually and say what you checked in your PR description.
Both packages must build and lint cleanly:
# Backend
cd backend && npm run build
# Frontend
cd frontend && npm run build && npm run lintManual checks by area:
- Rate-limit engine — hit
POST /api/rl/checkwith a real API key and confirm allow/deny flips at the configured limit:curl -X POST http://localhost:4000/api/rl/check \ -H "X-RL-Key: <your project api key>" \ -H "X-RL-Endpoint: /api/test" \ -H "X-RL-Method: GET"
- Dashboard — click through the affected screens at both desktop and mobile widths; the app is expected to be responsive.
- Charts — confirm hover tooltips and the crosshair still track correctly, and that empty/no-data states render.
- Auth — sign up, log in, log out, and confirm protected routes redirect.
Adding a real test setup (Vitest or Jest) is high on the wish list — see Ways to Contribute.
Write in the imperative mood and describe the effect of the change:
fix: correct off-by-one in sliding window boundary
feat: add Go SDK with net/http middleware
docs: document the rate-limit check headers
refactor: move analytics aggregation into a service
Keep the subject under ~72 characters. If the change needs context, add a body explaining the reasoning.
Before you open it:
- Branch is off
devand targetsdev -
npm run buildpasses in bothbackend/andfrontend/ -
npm run lintpasses infrontend/ - No stray debug logging or commented-out code
- No secrets,
.envfiles, or credentials committed -
src/generated/is untouched - Docs updated if you changed behaviour, env vars, or the API
In the PR description, include:
- What changed and why
- How you verified it — the manual steps you ran
- Screenshots or a short clip for any UI change (before/after is ideal)
- Linked issue, e.g.
Closes #42
Small, focused PRs get reviewed faster. If you're planning something large, open an issue first so we can agree on the approach before you invest the time.
Open an issue with:
- What you expected versus what happened
- Exact reproduction steps
- Environment: OS, Node version, Docker or local setup
- Relevant logs (
docker compose logs backend) — redact API keys and tokens
For anything security-sensitive, please report it privately to the maintainer rather than opening a public issue.
Open an issue describing:
- The problem you're solving and who has it
- Your proposed approach
- Alternatives you considered
Worth discussing before building: changes to the rate-limit algorithms, the database schema, the public SDK surface, or the hot path.
If something in this guide is unclear or wrong, that's a bug in the docs — open an issue or send a PR fixing it.