Naalya Handbook

Getting Started

From a fresh clone to a running stack — what runs where, the Taskfile, local infra in Docker, the apps on your host, and the type bus.

One codebase, three apps — server (apps/server), worker (apps/worker), audit (apps/audit). The thing to get straight before anything else is what runs where, because only half the stack is on your machine.

What runs where

PieceWhere, in development
PostgresHosted on Neon. There is no local Postgres container — you point DATABASE_URL at a Neon database.
RabbitMQHosted on CloudAMQP. Same — no local container.
RedisLocal Docker container. BullMQ queues and the Centrifugo engine.
CentrifugoLocal Docker container, the realtime broker. See How realtime works.
server / worker / auditYour terminal, in watch mode.

Docker does not give you a database

docker-compose.yaml defines Redis, Centrifugo, and the three apps — no Postgres, no RabbitMQ. If a guide (including this repo's README) tells you Docker brings up a database, it is out of date. Get connection strings from the team before you start.

Step 1: Toolchain

ToolTargetWhy
Node.jsLTS 22Dockerfile builds on node:22-alpine; dev scripts raise the heap to 8 GB.
pnpmlatestOne workspace, one install.
Docker + ComposelatestRedis and Centrifugo.
TasklatestThe project's front door. Optional but assumed below.
terminal
corepack enable
corepack prepare pnpm@latest --activate
brew install go-task    # or see taskfile.dev/installation

Every task is a thin alias over a pnpm script, so pnpm <script> always works too. Run task with no arguments to list everything.

Step 2: Install

terminal
task setup

That runs pnpm install and scaffolds an empty .env.local (it will never clobber an existing one). One install at the root covers all three apps and installs Husky git hooks.

Chowbea installs with it. chowbea-axios is a normal dependency in package.json, so there is no separate or global install on the API side — after task setup the chowbea-axios binary and its busHandler() are both available. (The Education Hub is the repo that historically used a global CLI.)

Step 3: Fill in .env.local

There is no .env.example. task setup leaves you a file with three comment lines; everything else you fill in.

Config is validated by a Zod schema (libs/shared/src/config/env.config.ts) at boot, and a missing key stops the process with Config validation error: … naming it.

37 keys are required, not 8

The schema marks most integrations as required — OPENAI_API_KEY, AI_GATEWAY_*, CLOUDFLARE_*, R2_*, MICROSOFT_*, UNIPILE_API_KEY, RESEND_*, SENTRY_*, DOCS_*, PESAPAL_BASE_URL, PAYMENT_CREDENTIALS_ENC_KEY included. They have neither .optional() nor .default(), so the app will not boot without them. Ask the team for a filled .env.local rather than assembling one key by key.

The four you can set yourself, because they point at your own machine:

.env.local (the local-only part)
PORT=8000
HOST=0.0.0.0
NODE_ENV=development
FRONTEND_URL=http://localhost:3000   # comma-separated for multiple CORS origins

REDIS_URL=redis://localhost:6379     # matches task infra
CENTRIFUGO_URL=http://localhost:8006 # matches task infra

REDIS_URL and CENTRIFUGO_URL already default to exactly these values, so you can omit them. DATABASE_URL (Neon) and RABBIT_MQ_URL (CloudAMQP) are shared credentials — never invent them, and never commit the file. Full inventory: Config & Environment.

Step 4: Start local infra

terminal
task infra          # Redis + Centrifugo, detached
task infra:status   # are they up?
task infra:logs     # tail both
task infra:down     # stop and remove

This is the fast loop: containers for the two things you don't want to install, everything else on your host where the debugger and file watcher work properly.

Centrifugo reads centrifugo.config.json from the repo root, which hardcodes the dev HMAC secret and API key that CENTRIFUGO_HMAC_SECRET / CENTRIFUGO_API_KEY default to — so they line up with no configuration. Its admin UI is at http://localhost:8006 (password admin).

Step 5: Run migrations

terminal
task migration:run

Schema is TypeORM migrations under database/migrations/, applied by hand — never auto-generated and never applied on boot.

You are migrating a shared database

DATABASE_URL points at hosted Neon, not a throwaway local container. Check which database you are pointed at before running or reverting anything. Workflow: The Database.

Step 6: Run the apps

terminal
task dev            # server + worker + audit together, watch mode

Colour-coded output: api blue, worker green, audit yellow. One at a time while iterating:

terminal
task dev:server     # apps/server — the HTTP API
task dev:worker     # apps/worker — jobs, and the Rover agent graph
task dev:audit      # apps/audit  — audit-log consumer
task email          # React Email preview server

Step 7: The type bus

The Education Hub generates its types from this repo. Two pipes — OpenAPI for REST, the type bus for everything Swagger cannot carry. Full picture: How Chowbea works.

terminal
task bus            # extract → chowbea.bus.json
task bus:watch      # re-extract on save — run this while working on shared types
task bus:check      # validate without writing (the CI gate)
task bus:diff       # compare against staging, fail on removed types

The server serves the manifest at /.well-known/chowbea.json, re-reading the file when it changes — so task bus:watch plus a running server gives the Hub live types with no restart. prebuild runs extract automatically, so a build never ships a stale manifest.

Step 8: Check it works

Everything mounts under the global prefix api/v1.

URLWhat
http://localhost:8000/api/v1The HTTP API
http://localhost:8000/docsScalar — interactive reference
http://localhost:8000/docs/swaggerSwagger UI
http://localhost:8000/docs/swagger/jsonRaw OpenAPI JSON
http://localhost:8000/.well-known/chowbea.jsonType bus manifest
http://localhost:8000/queuesBull Board — BullMQ dashboard
http://localhost:8006Centrifugo admin

Open Scalar and fire a request. A response means the apps, Neon, Redis, and RabbitMQ are all wired up.

/docs* is open in development; elsewhere it sits behind HTTP Basic Auth (DOCS_USERNAME / DOCS_PASSWORD).

Logging in

  • Seeding — in development, SchoolSeedService ensures a fixed demo school exists (the tenant users attach to). Skipped outside development.
  • Login — every address in SUPERADMIN_EMAILS is provisioned as a passwordless platform admin. Sign in by requesting an emailed one-time code.
  • Demo datapnpm seed:stress stamps extra data onto the demo school. Not needed on day one.

SUPERADMIN_EMAILS grants platform admin, above any school. For a school-scoped super admin, add your email to that school's SchoolConfig.superAdminEmails. See Auth & Permissions.

Everything in Docker

The alternative mode builds and runs the three apps as containers too, with ./apps and ./libs bind-mounted so watch mode still works:

terminal
task docker:dev         # foreground, builds on first run
task docker:dev:down

Slower to start and awkward to attach a debugger to. Use it to reproduce a container-only problem, not for daily work.

Taskfile reference

TaskDoes
taskList every task
task setupInstall deps, scaffold .env.local
task dev · task dev:server · task dev:worker · task dev:auditRun apps in watch mode
task infra · :status · :logs · :downRedis + Centrifugo
task docker:dev · :downFull stack in containers
task migration:generate NAME=add_widget · task migration:run · task migration:revertTypeORM migrations
task bus · :watch · :check · :diffType bus
task test · :watch · :cov · :e2eVitest
task lint · task format · task checkESLint, Prettier, and lint+test as a pre-push gate
task buildBuild all three apps
task docs · task docs:buildCompodoc on :8009
task sandbox:*Rover sandbox image on GHCR
task redeploy-railway ENVIRONMENT=staging · task sync-railway-envRailway infra

Where to go next

On this page