Skip to main content

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

ToolVersion
Node.js20.x LTS (use nvm or volta)
pnpm9.x (corepack enable && corepack prepare pnpm@latest --activate)
Docker Desktop / colimalatest
docker composev2
gcloud CLIlatest (only needed for staging access)
psql client16
makeoptional 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:

ContainerPortPurpose
postgres-tenant5432Postgres 16 with ltree, pgcrypto, pg_trgm, pgaudit
redis-tenant6379Memorystore-compatible (Redis 7)
pubsub-emulator8085GCP Pub/Sub local emulator
iam-service-stub4001NestJS stub satisfying IdentityClient port
billing-service-stub4002NestJS stub satisfying BillingClient port
notification-service-stub4003Captures sendInviteEmail calls; exposes /inbox
ai-orchestrator-stub4004Returns deterministic AI responses
otel-collector4318Receives 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

EntityNotes
Platform super-adminusr_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 membershipsOwner 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 membershipsOwner Reza, FrontDesk Zahra
RolesAll 9 system roles seeded for both tenants; one custom role custom.night_auditor on Tenant A
InvitationsOne pending invite for each tenant to a fresh email
Feature flagsaiEnabled = 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

CommandPurpose
pnpm devRun NestJS in watch mode
pnpm testRun unit tests (Jest)
pnpm test:intRun integration suite (Testcontainers)
pnpm test:isolationRun the two-tenant simulator (REQUIRED before any PR)
pnpm test:fuzzABAC fuzz tests (fast-check)
pnpm lintESLint + Prettier
pnpm typechecktsc --noEmit
pnpm migrate:up / :downForward / rollback migration
pnpm migrate:create <name>Scaffold a new migration
pnpm seedRe-seed the dev DB (idempotent)
pnpm policy:lintValidate the role/permission registry against canonical resources
pnpm policy:bundleBuild the offline policy bundle for the desktop
pnpm openapi:emitRegenerate OpenAPI spec from Zod DTOs; commit alongside code
pnpm pact:verifyVerify 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 dev exposes a Swagger UI at http://localhost:3000/docs.
  • Pub/Sub inspector: make pubsub:list-topics, make pubsub:peek SUBSCRIPTION=…. Backed by the emulator.
  • Postgres explorer: pnpm db:psql opens a psql session with app.tenant_id settable; 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/inbox shows captured invitation emails (with the raw token visible for local dev only).

10. Common Pitfalls

SymptomLikely causeFix
42501: permission denied for table membershipsapp.tenant_id not set on the sessionUse the Tenancy test helper or SET app.tenant_id = 'tnt_…'
Seed script fails on first runPostgres extensions missingRecreate the container; infra/postgres-init.sql installs the extensions
Pub/Sub emulator topic not foundTopic auto-create disabledmake pubsub:bootstrap (creates all melmastoon.tenant.* topics)
MELMASTOON.AUTH.TENANT_MISMATCH in devJWT tid does not match X-Tenant-IdThe seed JWTs are tenant-scoped — pass the matching X-Tenant-Id
Two-tenant simulator passes locally but fails in CILocal DB had stale RLS offAlways 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)