Core of a SaaS metering + billing system: ingest usage events → aggregate into hourly windows → tiered-price into monthly invoices → a signed payment webhook, with a customer dashboard and an internal ops console.
Stack: Django + DRF + Postgres · React + Vite + TypeScript · a Postgres-advisory-locked job runner. The reasoning behind every non-obvious decision is in DESIGN.md (read that first — it counts as much as the code).
docker compose up --buildThat's it — no config step. up migrates the DB and auto-seeds realistic demo data on first
run (it's idempotent, so repeated ups are safe; first run takes ~1 minute — watch the seed
logs), then starts the API, worker, and frontend.
- Frontend: http://localhost:5173 — customer dashboard at
/, ops console at/ops - API: http://localhost:8000 (health:
/health)
Demo logins (also printed in the seed logs):
- Ops console:
opsadmin/opsadmin - Customer dashboard: any seeded email (e.g.
acme0@example.com) /demo1234 - Anomaly-test accounts (
anomaly-usage-spike@…,-per-key@…,-spend@…,-drop@…) appear in the ops Anomalies panel; Acme is left with an unpaid May invoice to test the payment module.
Config comes from the environment with local-dev defaults baked into docker-compose.yml, so
nothing real is committed; override anything via a .env (copy .env.example) or real env vars.
Reseed from scratch with make down-v && docker compose up.
make test # full pytest suite against Postgres (RLS active)78 tests focused on the correctness boundaries the brief calls out — idempotency, concurrency (real threaded races), tenant isolation, reconciliation/late events, and money — not getters.
Customer API (/v1, scoped to the authenticated customer)
POST /v1/events— batched, idempotent ingestion (API-key auth)GET /v1/usage— page-number paginated (jump-to-page), filter by date range + api key;GET /v1/usage/summaryGET /v1/invoices,GET /v1/invoices/{id}POST /v1/auth/login,GET /v1/me
Ops API (/ops, staff auth, audited, cross-tenant)
GET /ops/customers,GET /ops/customers/{id}(usage + invoices + anomaly)POST /ops/customers/{id}/credits(requiresIdempotency-Key)PATCH /ops/invoices/{id}/line-items/{id}(override + audit trail)
Webhook
POST /webhooks/payments— HMAC-signed, replay-safe, marks invoices paid
Background jobs (the worker service; advisory-locked, idempotent)
aggregate_usage,issue_invoices,reconcile— run on a loop byrun_scheduler, and individually runnable:docker compose run --rm migrate python manage.py <command>
- Tenant isolation at the database via Postgres Row-Level Security and an app-layer scoped manager — a customer guessing another's invoice id gets a 404.
- Idempotency everywhere it matters:
ON CONFLICTonrequest_id(ingestion) andprovider_event_id(webhook),Idempotency-Keyon credits, recompute-based aggregator. - Money in integer minor units (cents), sub-cent rates in micro-dollars, banker's rounding.
- Immutable audit log (DB trigger + revoked grants) for every credit/override.
- No secrets in the repo — webhook secret, API-key pepper, DB creds are env-based.
backend/ Django project (apps: customers, usage, billing, audit, webhooks, jobs, ops, common)
frontend/ Vite + React + TS SPA (src/app = customer, src/ops = ops console)
docker/ Postgres role bootstrap
DESIGN.md the written reasoning (data model, idempotency, scaling, threat model, trade-offs)
The brief's stack is Django + DRF + Postgres + React, and that's what this uses — but the choice is also deliberate: the properties being graded (tenant scoping at the right layer, constraints over comments, idempotent jobs, integer money) are idiomatic in Postgres, so the database does the heavy lifting and the application stays thin. Where it would break first at production scale, and the migration path, are spelled out in DESIGN.md §4.