Skoolsewa - School Management System Docs

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.yaml

Prerequisites

  • Node.js and pnpm 10 (corepack enable picks up the pinned version)
  • PostgreSQL 18, running locally
  • Docker, for Redis and MongoDB

Local setup

1. Install dependencies

pnpm install

2. 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 -d

MONGO_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:

VariableWhat it is
DATABASE_URLThe development database, e.g. postgresql://skoolsewa:…@127.0.0.1:5432/skoolsewa
TEST_DATABASE_URLThe test database. Its name must end in _test, and it must differ from DATABASE_URL
MONGODB_URLMust use the same credentials as the root .env
REDIS_URLMust use the same credentials as the root .env
JWT_PRIVATE_KEY_BASE64, JWT_PUBLIC_KEY_BASE64An RS256 key pair — generate one below
COOKIE_SECRETAny long random string
PORTThe API port; the development environment uses 5002
SWAGGER_ENABLEDtrue to serve the OpenAPI documents; defaults to false

Generate the JWT key pair:

pnpm --filter @skoolsewa/api generate:jwt-keys

It 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 structure

To 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:migrate

5. Run the API

pnpm dev:server

With PORT=5002:

  • Liveness and readiness: http://localhost:5002/health/live and /health/ready — outside the /api prefix
  • Every other route: under http://localhost:5002/api/…, with no version segment
  • Admin OpenAPI document: http://localhost:5002/api/api-docs (when SWAGGER_ENABLED=true)
  • Mobile and audience OpenAPI document: http://localhost:5002/api/mobile-docs (when SWAGGER_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:4000

pnpm 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

CommandWhat it does
pnpm devEvery app in development mode, through Turborepo
pnpm dev:serverThe API only
pnpm buildBuild every package and the API
pnpm build:docsBuild this documentation site — not part of pnpm build
pnpm testUnit and integration tests
pnpm --filter @skoolsewa/api test:structureRoute and guard fingerprint tests — pnpm test does not run them
pnpm check-typesType-check every package — delete every *.tsbuildinfo first, or a stale one hides errors
pnpm checkBiome lint and format, writing fixes
pnpm check:ciBiome without writing, plus the rule audits — what CI runs
pnpm rulesError-code, module-size, module-shape, job-id and env-orphan audits
pnpm db:generate -- --name=<name>Generate a migration from the schema
pnpm db:migrateApply pending migrations to DATABASE_URL
pnpm db:migrate:downRevert applied migrations — see Down Migrations
pnpm db:seedSeed permissions, roles, the superadmin, reference lists and school structure
pnpm --filter @skoolsewa/api seed:demo-schoolBuild the demonstration school through the running API - see Demo School Seed
pnpm db:studioOpen Drizzle Studio
pnpm --filter @skoolsewa/api permissions:syncReconcile 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-types

A 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