A NestJS + PostgreSQL backend for a deterministic engagement loop:
register/login -> daily check-in -> state updates -> rewards -> buy/use tools
The project emphasizes correctness under concurrency using transactions, unique constraints, and explicit invariants.
# 1) Install deps
npm install
# 2) Configure env
cp .env.example .env
cp .env.docker.example .env.docker
# 3) Start DB + API containers (API runs migrate deploy on startup)
docker compose up -d --buildAPI: http://localhost:3000
Swagger: http://localhost:3000/api
Production API: https://stateful-engagement-backend.onrender.com
Production Swagger: https://stateful-engagement-backend.onrender.com/api#/
- Open
http://localhost:3000/api. - Call
POST /auth/register(orPOST /auth/login) with email + password. - Copy
accessTokenfrom response. - Click Authorize in Swagger UI and paste:
Bearer <accessToken>. - Call protected endpoints (
/me,/checkins,/rewards,/tools/inventory, ...).
Host-only API workflow (optional):
# Start only database in Docker
docker compose up -d db
# Run migrations from host and start API from host
npx prisma migrate dev
npm run start:dev- TypeScript
- NestJS 11
- PostgreSQL
- Prisma ORM
- JWT auth + bcrypt
- Jest + Supertest
- GitHub Actions CI
User: auth identityUserState(1:1):energy,fatigue,loyalty,streak,levelDailyCheckIn: unique per(userId, UTC date)Reward: reward catalog definitionsAppliedReward: once-ever reward trackingToolDefinition: tool catalogUserTool: user inventory rows with quantity
- At most one check-in per user per UTC day
- State-changing operations are transactional
- Rewards are applied once-ever per user
- Inventory has a max unique tool-type capacity
- Tool use is atomic (inventory + state mutation)
- State values are clamped by shared state rules
- Database checks enforce
energy/fatigueranges and non-negative values for key counters
Public:
POST /auth/registerPOST /auth/loginGET /tools
Protected (JWT bearer):
GET /mePOST /checkinsGET /rewardsGET /tools/inventoryPOST /tools/inventory/buy/:toolIdPOST /tools/:toolId/use
Register:
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@x.com","password":"pass1234"}'Login:
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@x.com","password":"pass1234"}'Use returned token as:
Authorization: Bearer <token>
Unit tests:
npm testE2E tests:
cp .env.test.example .env.test
npm run test:e2e:setup
npm run test:e2eWhat e2e covers:
- Check-in conflicts (
409) for duplicates - Reward idempotence (once-ever semantics)
- Tool buy/use flows end-to-end
- Concurrency safety for check-in/tool endpoints
- Auth race safety and rate-limiting behavior
- DB-level check-constraint enforcement
Lint:
npm run lintNotes:
- Lint is type-aware and runs Prisma generate first (
prelint) to avoid stale Prisma types. - Test files intentionally allow limited unsafe patterns where Supertest response typing is impractical.
- TS/ESLint settings are strict in key areas but intentionally pragmatic (
noImplicitAnyis not fully strict).
CI workflow: .github/workflows/ci.yml
On push/PR to main, it runs:
npm cinpx prisma generatenpm run lintnpm run buildnpm run test:e2e:setupnpm testnpm run test:e2e
CI uses a temporary PostgreSQL service (engagement_test).
src/
auth/
checkins/
common/
config/
me/
prisma/
rewards/
tools/
users/
app.module.ts
main.ts
test/
app.e2e-spec.ts
checkins.e2e-spec.ts
rewards.once-ever.e2e-spec.ts
tools.e2e-spec.ts
helpers/
prisma/
schema.prisma
migrations/
seed.ts
Required env vars:
DATABASE_URLJWT_SECRETJWT_EXPIRES_INNODE_ENV=productionCORS_ORIGIN(comma-separated allowlist, required in production)- Optional:
ENABLE_DEV_SIMULATION=trueenables DEV-only endpointPOST /me/dev/grant-loyalty/:amountfor local testing - Optional:
RUN_SEED_ON_BOOT=trueto runprisma db seedduring container startup
Logging/request correlation:
- Structured JSON logs are enabled via
nestjs-pino. - Every request gets/propagates
x-request-idand returns it in response headers. - Error payloads include
requestIdfor easier trace correlation.
Run migrations in production:
npx prisma migrate deployDo not use prisma migrate reset in production.
- Frontend UI
- Admin dashboard
- Background jobs/schedulers
- Realtime/WebSockets
Educational / portfolio use.