tenant-service — LOCAL_DEV_SETUP
Cross-cutting dev environment lives in the root README and the platform standards. This file is the per-service quickstart.
1. Prerequisites
| Tool | Version |
|---|---|
| Node.js | 20.x LTS (use nvm or volta) |
| pnpm | 9.x (corepack enable && corepack prepare pnpm@latest --activate) |
| Docker Desktop / colima | latest |
docker compose | v2 |
gcloud CLI | latest (only needed for staging access) |
psql client | 16 |
make | optional but recommended |
OS: macOS 13+, Ubuntu 22.04+, Windows 11 with WSL2 (Ubuntu).
2. Clone and Bootstrap
git clone https://github.com/ghasi-tech/melmastoon.git
cd melmastoon/services/tenant-service
pnpm install # workspace-aware
cp .env.example .env
3. Local Dependencies (docker compose)
infra/docker-compose.dev.yml brings up everything needed:
make dev:up # or: docker compose -f infra/docker-compose.dev.yml up -d
Stack:
| Container | Port | Purpose |
|---|---|---|
postgres-tenant | 5432 | Postgres 16 with ltree, pgcrypto, pg_trgm, pgaudit |
redis-tenant | 6379 | Memorystore-compatible (Redis 7) |
pubsub-emulator | 8085 | GCP Pub/Sub local emulator |
iam-service-stub | 4001 | NestJS stub satisfying IdentityClient port |
billing-service-stub | 4002 | NestJS stub satisfying BillingClient port |
notification-service-stub | 4003 | Captures sendInviteEmail calls; exposes /inbox |
ai-orchestrator-stub | 4004 | Returns deterministic AI responses |
otel-collector | 4318 | Receives OTel; exports to local Tempo + Loki + Prometheus |
Stubs live in services/tenant-service/test/stubs/ and ship with the repo.
4. Environment (.env.example)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://tenant:tenant@localhost:5432/tenant
REDIS_URL=redis://localhost:6379
PUBSUB_PROJECT_ID=melmastoon-dev
PUBSUB_EMULATOR_HOST=localhost:8085
IAM_SERVICE_BASE_URL=http://localhost:4001
BILLING_SERVICE_BASE_URL=http://localhost:4002
NOTIFICATION_SERVICE_BASE_URL=http://localhost:4003
AI_ORCHESTRATOR_BASE_URL=http://localhost:4004
JWT_JWKS_URL=http://localhost:4001/.well-known/jwks.json
JWT_ISSUER=https://iam.melmastoon.local
JWT_AUDIENCE=tenant-service
REGION=local
LOG_LEVEL=debug
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
Secrets in production come from Secret Manager; in dev they are plain in .env and never committed.
5. Migrate and Seed
pnpm migrate:up # node-pg-migrate, applies migrations/*.sql
pnpm seed # runs scripts/seed.dev.ts
The seed script provisions a deterministic dev fixture so every developer sees the same world. See §6.
6. Seed Fixture
| Entity | Notes |
|---|---|
| Platform super-admin | usr_super_admin_dev_0001 — JWT helper script returns a valid token for this user |
Tenant A — Asia Hotel Chain (tnt_dev_chain_0001, slug asia-hotel-chain) | 3 properties: Hotel Asia Kabul (ppt_dev_kabul_0001), Hotel Asia Mazar (ppt_dev_mazar_0001), Hotel Asia Herat (ppt_dev_herat_0001); org tree chain → region(Kabul, Mazar, Herat) → property |
| Tenant A memberships | Owner Daud, GM Sara, FrontDesk Ali (Kabul scope), Housekeeping Lead Maryam (Mazar scope) |
Tenant B — Pamir Guesthouse (tnt_dev_single_0001, slug pamir-guesthouse) | 1 property Pamir Guesthouse, single-property tenant; org root collapsed to property kind |
| Tenant B memberships | Owner Reza, FrontDesk Zahra |
| Roles | All 9 system roles seeded for both tenants; one custom role custom.night_auditor on Tenant A |
| Invitations | One pending invite for each tenant to a fresh email |
| Feature flags | aiEnabled = true on Tenant A; aiEnabled = false on Tenant B |
The seed script also prints a JWT for each member, ready to paste into Authorization: Bearer …:
$ pnpm seed
Seeded successfully.
JWTs (15-min TTL):
super_admin → eyJhbGciOiJSUzI1NiIs...
daud (owner A) → eyJhbGciOiJSUzI1NiIs...
sara (gm A) → eyJhbGciOiJSUzI1NiIs...
ali (frontdesk) → eyJhbGciOiJSUzI1NiIs...
reza (owner B) → eyJhbGciOiJSUzI1NiIs...
7. Useful Commands
| Command | Purpose |
|---|---|
pnpm dev | Run NestJS in watch mode |
pnpm test | Run unit tests (Jest) |
pnpm test:int | Run integration suite (Testcontainers) |
pnpm test:isolation | Run the two-tenant simulator (REQUIRED before any PR) |
pnpm test:fuzz | ABAC fuzz tests (fast-check) |
pnpm lint | ESLint + Prettier |
pnpm typecheck | tsc --noEmit |
pnpm migrate:up / :down | Forward / rollback migration |
pnpm migrate:create <name> | Scaffold a new migration |
pnpm seed | Re-seed the dev DB (idempotent) |
pnpm policy:lint | Validate the role/permission registry against canonical resources |
pnpm policy:bundle | Build the offline policy bundle for the desktop |
pnpm openapi:emit | Regenerate OpenAPI spec from Zod DTOs; commit alongside code |
pnpm pact:verify | Verify provider pacts (consumer pacts hosted in bff-backoffice-service) |
8. Two-Tenant Isolation Test (mandatory)
Before any PR:
pnpm test:isolation
This runs the suite documented in TESTING_STRATEGY §4.1. Failure blocks merge.
9. Debugging Tools
- HTTP inspector:
pnpm devexposes a Swagger UI athttp://localhost:3000/docs. - Pub/Sub inspector:
make pubsub:list-topics,make pubsub:peek SUBSCRIPTION=…. Backed by the emulator. - Postgres explorer:
pnpm db:psqlopens apsqlsession withapp.tenant_idsettable; the prompt warns you when RLS is in effect. - Trace viewer: local Tempo on
http://localhost:3200; the seed ID is included in every log line. - Notification inbox:
http://localhost:4003/inboxshows captured invitation emails (with the raw token visible for local dev only).
10. Common Pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
42501: permission denied for table memberships | app.tenant_id not set on the session | Use the Tenancy test helper or SET app.tenant_id = 'tnt_…' |
| Seed script fails on first run | Postgres extensions missing | Recreate the container; infra/postgres-init.sql installs the extensions |
| Pub/Sub emulator topic not found | Topic auto-create disabled | make pubsub:bootstrap (creates all melmastoon.tenant.* topics) |
MELMASTOON.AUTH.TENANT_MISMATCH in dev | JWT tid does not match X-Tenant-Id | The seed JWTs are tenant-scoped — pass the matching X-Tenant-Id |
| Two-tenant simulator passes locally but fails in CI | Local DB had stale RLS off | Always run pnpm db:reset && pnpm migrate:up && pnpm seed before a clean run |
11. Tear Down
make dev:down # stops and removes containers
make dev:nuke # also drops volumes (you will lose seeded data)