Technical Introduction
The Skoolsewa backend — a NestJS API over PostgreSQL, MongoDB, Redis and BullMQ in a Turborepo and pnpm monorepo — and how to run it locally.
Skoolsewa is the backend for a school management platform: people, classes, subjects, timetables,
attendance, leave, notices, assignments, examinations and results, notifications, bulk import and
export, and backup and restore. The admin panel that consumes it lives in a separate repository,
skoolsewa-frontend.
Stack
Runtime
- NestJS 11 — the API, in
apps/api - PostgreSQL 18 with Drizzle ORM — every business table, owned by
packages/db - MongoDB — the audit log only, owned by
packages/mongodb - Redis — caching, permission caches and the BullMQ backing store, via
packages/redis - BullMQ — background jobs, always enqueued through the transactional outbox; queue names and payloads live in
packages/jobs - Passport with RS256 JWTs — authentication
Tooling
- Turborepo and pnpm workspaces (
pnpm@10.33.0) - Biome — linting and formatting
- Jest — unit and integration tests, run against a real PostgreSQL test database
- Fumadocs — this documentation site, in
apps/fumadocs
Repository layout
skoolsewa-backend/
├── apps/
│ ├── api/ # NestJS API
│ └── fumadocs/ # This documentation site (port 4000)
├── packages/
│ ├── db/ # Drizzle schema, migrations, down migrations, seeds
│ ├── jobs/ # BullMQ queue names and payload types
│ ├── mongodb/ # Mongoose schema for the audit log
│ ├── redis/ # Redis client, key and TTL registry
│ ├── email/ # Email delivery
│ ├── otp/ # One-time passwords
│ ├── sms/ # SMS delivery
│ ├── firebase/ # Push delivery
│ └── storage/ # Upload storage (local filesystem or S3-compatible)
├── docker-compose.yml # Redis and MongoDB for local development
├── turbo.json
└── pnpm-workspace.yamlPrerequisites
- Node.js and pnpm 10 (
corepack enablepicks up the pinned version) - PostgreSQL 18, running locally
- Docker, for Redis and MongoDB
Local setup
1. Install dependencies
pnpm install2. Start Redis and MongoDB
The root docker-compose.yml runs Redis on port 6382 and MongoDB on port 27020, reading their
credentials from the root .env (template: sample.env).
docker compose up -dMONGO_INITDB_ROOT_USERNAME only applies when the MongoDB volume is first created. Changing it
against an existing volume leaves the container reporting healthy while every connection fails
authentication.
3. Configure the API
Copy apps/api/.env.example to apps/api/.env and fill it in. The values that must be set before
the API boots:
| Variable | What it is |
|---|---|
DATABASE_URL | The development database, e.g. postgresql://skoolsewa:…@127.0.0.1:5432/skoolsewa |
TEST_DATABASE_URL | The test database. Its name must end in _test, and it must differ from DATABASE_URL |
MONGODB_URL | Must use the same credentials as the root .env |
REDIS_URL | Must use the same credentials as the root .env |
JWT_PRIVATE_KEY_BASE64, JWT_PUBLIC_KEY_BASE64 | An RS256 key pair — generate one below |
COOKIE_SECRET | Any long random string |
PORT | The API port; the development environment uses 5002 |
SWAGGER_ENABLED | true to serve the OpenAPI documents; defaults to false |
Generate the JWT key pair:
pnpm --filter @skoolsewa/api generate:jwt-keysIt prints both variables, and writes a sample configuration to apps/api/.env.generated unless that
file already exists. Copy the two keys into apps/api/.env.
4. Create the schema and seed
pnpm db:migrate # apply every migration to DATABASE_URL
pnpm db:seed # permissions, roles, one superadmin, reference lists and school structureTo fill a development database with a populated demonstration school, start the API and run
pnpm --filter @skoolsewa/api seed:demo-school - see Demo School Seed.
The superadmin is SUPERADMIN_EMAIL (default superadmin@skoolsewa.local) with
SUPERADMIN_PASSWORD. Outside production, an unset password falls back to a development-only
default and the seed warns about it; in production the seed refuses to run without one.
The test database is migrated separately — pnpm db:migrate only touches DATABASE_URL:
cd packages/db && DATABASE_URL="$TEST_DATABASE_URL" pnpm run db:migrate5. Run the API
pnpm dev:serverWith PORT=5002:
- Liveness and readiness:
http://localhost:5002/health/liveand/health/ready— outside the/apiprefix - Every other route: under
http://localhost:5002/api/…, with no version segment - Admin OpenAPI document:
http://localhost:5002/api/api-docs(whenSWAGGER_ENABLED=true) - Mobile and audience OpenAPI document:
http://localhost:5002/api/mobile-docs(whenSWAGGER_ENABLED=true) - Drizzle Studio:
pnpm db:studio, then local.drizzle.studio
6. Run this documentation site
pnpm build:docs
pnpm --filter @skoolsewa/fumadocs start:docs # http://localhost:4000pnpm dev:docs also works, but its development server can exhaust the Node heap on the larger
pages; the production build is the reliable way to read every page.
Commands
| Command | What it does |
|---|---|
pnpm dev | Every app in development mode, through Turborepo |
pnpm dev:server | The API only |
pnpm build | Build every package and the API |
pnpm build:docs | Build this documentation site — not part of pnpm build |
pnpm test | Unit and integration tests |
pnpm --filter @skoolsewa/api test:structure | Route and guard fingerprint tests — pnpm test does not run them |
pnpm check-types | Type-check every package — delete every *.tsbuildinfo first, or a stale one hides errors |
pnpm check | Biome lint and format, writing fixes |
pnpm check:ci | Biome without writing, plus the rule audits — what CI runs |
pnpm rules | Error-code, module-size, module-shape, job-id and env-orphan audits |
pnpm db:generate -- --name=<name> | Generate a migration from the schema |
pnpm db:migrate | Apply pending migrations to DATABASE_URL |
pnpm db:migrate:down | Revert applied migrations — see Down Migrations |
pnpm db:seed | Seed permissions, roles, the superadmin, reference lists and school structure |
pnpm --filter @skoolsewa/api seed:demo-school | Build the demonstration school through the running API - see Demo School Seed |
pnpm db:studio | Open Drizzle Studio |
pnpm --filter @skoolsewa/api permissions:sync | Reconcile the permission catalogue and role grants after adding a permission |
After pnpm db:generate, format the generated packages/db/src/migrations/meta/*.json with Biome —
drizzle writes spaces and this repository uses tabs, so check:ci otherwise fails on formatting
alone.
Troubleshooting
A request hangs, or the server says "Ready" but nothing answers
A server from an earlier run may still hold the port, accepting connections and never answering. Find the owner before debugging the code:
ss -ltnp | grep ':5002 'Stopping a pnpm dev process does not stop the nest child that holds the port. Kill the process
group, then confirm the port is free.
check-types passes but the API will not build
tsc -b is incremental. Delete the build info and run it again:
find apps packages -name '*.tsbuildinfo' -not -path '*/node_modules/*' -delete && pnpm check-typesA test passes locally and fails in CI on a constraint
CI migrates the test database from empty. Apply new migrations to your local test database too, with the command in step 4.
MongoDB authentication fails although the container is healthy
The credentials in the root .env, apps/api/.env and the existing volume disagree. Restore the
original credential, or recreate the volume.
Further reading
- The module pages under Developer Resources — each module has a feature, backend and API page
- NestJS documentation
- Drizzle ORM documentation