Auth and API-key custody engine — issuance, validation, and revocation for multi-tenant services. Built to reason clearly about token lifecycle, replay protection, and safe key revocation under concurrent access, not to be a CRUD-auth starter kit.
Stack: NestJS · PostgreSQL · Prisma 7 (driver adapter) · Redis · BullMQ · JWT · bcrypt · Docker Compose · Nginx
Most "auth boilerplate" repos stop at register/login. The interesting engineering problems in an API-key system show up after that: how you rotate refresh tokens without opening a replay window, how you revoke a key without leaving a stale-cache path to bypass it, and how your schema behaves when a user is deleted but their usage history needs to survive. This repo is where I work through those problems directly, without leaning on an off-the-shelf auth-as-a-service provider — the point is to own the decisions, not to configure someone else's.
Access tokens are short-lived JWTs. Refresh tokens are opaque, bcrypt-hashed before storage, and delivered via an httpOnly cookie. On every refresh, the old token is invalidated and a new one issued (rotation) — if a hashed refresh token is presented that's already been rotated out, that's treated as a replay signal and the session is revoked rather than silently re-issued. This trades a small amount of complexity (tracking token lineage) for closing the window where a leaked refresh token can be reused indefinitely.
UsageLog rows reference the ApiKey that generated them. On key deletion, the FK is set to SetNull rather than Cascade — usage history is treated as an audit trail that should outlive the key it was generated by. This is a deliberate tradeoff: it means usage queries need to handle a nullable key reference, but it prevents deleting a key from silently erasing billing/usage evidence.
Currently working through DELETE /api-key/:id. Given the SetNull design above, a hard delete on ApiKey is schema-safe (usage logs survive via SetNull), but a soft-delete (revokedAt timestamp, key excluded from active-key queries) preserves the ability to show "this key existed and was revoked on X" in an audit UI, and makes accidental revocation reversible. Leaning toward soft-delete for auditability, at the cost of every active-key query needing a revokedAt IS NULL filter.
Migrated PrismaService to the Prisma 7 driver adapter pattern rather than staying on the legacy engine binary. This decouples the query engine from a bundled native binary, which matters for the Docker image size and for avoiding platform-specific engine mismatches between local dev and the containerized build.
.env (local) and .env.docker (containerized) are kept separate and loaded via Compose's env_file:, with Joi-based validation wired into ConfigModule so a missing or malformed env var fails at boot, not at first request.
(Benchmarks — validation latency, throughput under load — are on the roadmap once the revoke endpoint lands; not included here to avoid citing numbers that haven't been measured yet.)
- Auth module — register, login (access + refresh JWT pair), bcrypt-hashed refresh tokens, httpOnly cookies, rotation with replay protection, logout with session revocation
- Global
JwtAuthGuardwith a@Public()opt-out decorator (Reflector.createDecorator) - API key module — create (prefixed key via
crypto.randomBytes, bcrypt hash storage, maskedkeyPreviewfor display), list (user-scoped, active-only, newest-first, Prismaselect-level filtering) - Prisma schema —
User,RefreshToken,ApiKey,UsageLogwith hashed fields and indexed FKs - Full Docker Compose dev environment — Postgres, Redis, Nginx, healthcheck-gated startup, multi-stage Dockerfile with Prisma client generation at a custom output path
DELETE /api-key/:idrevoke endpoint (see tradeoff #3 above)- Rate limiting via Redis + BullMQ
- Usage logging middleware
cp .env.example .env
docker compose up --buildPostgres, Redis, and Nginx come up behind healthchecks before the API container starts. Prisma migrations run on boot.
Jest for unit tests, supertest for E2E. Endpoint testing during development via Postman; DB state verified via Prisma Studio / raw SQL where a UI would hide the actual query behavior.