LOCAL_DEV_SETUP — notification-service
Sibling: DEPLOYMENT_TOPOLOGY · TESTING_STRATEGY · API_CONTRACTS
Strategic anchors: 10-monorepo · 02 Enterprise Architecture §15 Developer Experience
This document gets a new engineer from "git clone" to "I sent a notification to my own phone via the WhatsApp sandbox" in under 30 minutes.
1. Prerequisites
| Tool | Version | Why |
|---|---|---|
| Node.js | >= 20.11 (LTS) | runtime |
| pnpm | >= 9.0 | monorepo package manager |
| Docker Desktop / Rancher Desktop | latest | local infra (Postgres, Redis, Pub/Sub emulator, GCS emulator) |
| Docker Compose v2 | bundled | pnpm dev:infra |
gcloud CLI | latest | optional — only for live-vendor sandbox tests |
mkcert | latest | optional — local TLS for WS testing |
direnv | latest | environment management |
psql | >= 16 | manual DB poking |
redis-cli | latest | optional |
Windows users: install via WSL2 (Ubuntu) for the smoothest experience; the team's CI mirrors Ubuntu 22.04.
2. Repository layout (monorepo)
melmastoon/
services/
notification-service/
src/
domain/
application/
infrastructure/ # adapters: Postgres, Pub/Sub, Memorystore, vendor SDKs
interfaces/ # REST/WS controllers, Pub/Sub subscribers, scheduler entrypoints
config/
tests/
openapi/
drizzle/
docker/
package.json
tsconfig.json
vitest.config.ts
Dockerfile
packages/
shared/event-envelope
shared/ids
shared/clock
shared/test-fixtures-notification
shared/openapi-codegen
shared/event-schemas
infra/
docker-compose.local.yml
terraform/notification-service
Per 10-monorepo rule, service code never imports another service's src/; cross-service code lives under packages/shared/.
3. First-time setup
git clone git@github.com:ghasi/melmastoon.git
cd melmastoon
direnv allow # loads .envrc with NPM_CONFIG_REGISTRY etc.
pnpm install # installs all workspace deps
pnpm -F notification-service... build:codegen # generate OpenAPI types + event schemas
Create services/notification-service/.env.local (gitignored):
NODE_ENV=development
LOG_LEVEL=debug
PORT=3007
WS_PORT=3008
# Postgres
DATABASE_URL=postgresql://melm:melm@localhost:5439/melmastoon_notification
DATABASE_RLS_ROLE=melmastoon_notification_app
# Memorystore (Redis)
REDIS_URL=redis://localhost:6389
# Pub/Sub emulator
PUBSUB_EMULATOR_HOST=localhost:8085
PUBSUB_PROJECT_ID=melmastoon-local
# GCS emulator
STORAGE_EMULATOR_HOST=http://localhost:4443
GCS_BUCKET=melmastoon-notifications-local
# Secret Manager (use file-based in dev)
SECRETS_BACKEND=file
SECRETS_FILE=./.dev-secrets.json
# AI orchestrator stub
AI_ORCHESTRATOR_URL=http://localhost:3099
AI_ORCHESTRATOR_TOKEN=local-dev-token
# Vendor stubs
SENDGRID_BASE_URL=http://localhost:4001
TWILIO_BASE_URL=http://localhost:4002
WHATSAPP_BASE_URL=http://localhost:4003
FCM_BASE_URL=http://localhost:4004
# Cross-service projection clients (run sibling services with `pnpm dev:siblings`)
RESERVATION_PROJECTION_URL=http://localhost:3001
BILLING_PROJECTION_URL=http://localhost:3002
LOCK_PROJECTION_URL=http://localhost:3009
TENANT_CONFIG_URL=http://localhost:3000
IAM_URL=http://localhost:3010
# Synthetic tenant + user
DEV_TENANT_ID=tnt_LOCAL000000000000000000
DEV_USER_ID=usr_LOCAL000000000000000000
DEV_PROPERTY_ID=ppt_LOCAL000000000000000000
4. Bring up local infra
pnpm dev:infra
This starts (infra/docker-compose.local.yml):
| Service | Port | Notes |
|---|---|---|
postgres | 5439 | Postgres 16 with extensions; auto-migrate via notification-migrate job |
redis | 6389 | redis:7-alpine |
pubsub-emulator | 8085 | gcloud beta emulator |
gcs-emulator (fsouza/fake-gcs-server) | 4443 | bucket melmastoon-notifications-local pre-created |
mailhog | 8025 (UI), 1025 (SMTP) | catches local sandbox SMTP for legacy adapters (rare) |
sendgrid-stub | 4001 | tiny Express app under services/notification-service/docker/stubs/sendgrid |
twilio-stub | 4002 | likewise |
whatsapp-stub | 4003 | likewise |
fcm-stub | 4004 | likewise |
ai-orchestrator-stub | 3099 | returns deterministic AI-drafted bodies + provenance |
jaeger | 16686 (UI) | tracing |
prometheus | 9090 | metrics |
grafana | 3009 (UI) | pre-provisioned dashboards |
loki | 3100 | logs |
Verify:
pnpm dev:infra:status
5. Run notification-service
pnpm -F notification-service dev
This starts the API + WS servers and all in-process workers (router, dispatchers, scheduler, outbox-relay, prober, cache-warmer) using concurrently. Hot-reload via tsx --watch.
Health:
curl http://localhost:3007/api/v1/internal/health
6. Seed data
pnpm -F notification-service seed:dev
This creates:
- 1 tenant (
tnt_LOCAL…), 1 property, 3 staff users (OWNER, FRONT_DESK, MARKETING_MANAGER), 3 guests with localesen-US,ps-AF,ur-PK. - Channel configs for all 6 channel kinds pointing at the local stubs.
- Platform-global templates for the 12 most common keys, all in
en-US, plus tenant override forreservation.confirmed.emailinps-AF. - A trigger-map import matching the platform default.
7. Send a notification end-to-end
7.1 Via API
curl -X POST http://localhost:3007/api/v1/notifications \
-H 'Authorization: Bearer dev:owner' \
-H 'X-Tenant-Id: tnt_LOCAL000000000000000000' \
-H 'Idempotency-Key: 01J4Z000000000000000000001' \
-H 'Content-Type: application/json' \
-d '{
"templateKey": "reservation.confirmed.email",
"category": "transactional",
"channel": "email",
"locale": "ps-AF",
"recipient": { "by": "guestId", "guestId": "gst_LOCAL_AHMED" },
"variables": {
"guestName": "احمد",
"reservationCode": "MELM-2026-04-001234",
"stayWindow": { "start": "2026-04-25", "end": "2026-04-28" },
"roomLabel": "Suite 304",
"balanceDue": { "amountMicro": "120000000", "currency": "USD" },
"propertyName": "هتل د کابل",
"checkInTime": "14:00",
"policyExcerptKey": "cancellation.standard"
}
}'
The local sendgrid-stub accepts the send and emits a synthetic delivered webhook back to us within 2 s. You should see:
- a row in
notifications(status='delivered'), - a
delivery_attempt(outcome='accepted'), - a webhook_inbound + event applied,
- events
notification.requested.v1,notification.dispatched.v1,notification.delivered.v1on the local Pub/Sub emulator (subscribe withgcloud pubsub subscriptions pullfor inspection).
7.2 Via consumed event
pnpm -F notification-service dev:emit -- reservation.confirmed.v1 fixtures/booking_ahmed.json
This publishes a fixture event to the local emulator; the router consumes it and enqueues 2 notifications (email + sms) per the trigger map.
7.3 In-app + WS
Open tools/dev-feed-viewer.html in a browser; it connects to ws://localhost:3008/api/v1/notifications/feed/stream with a dev token and prints incoming feed events. Send any in-app notification and watch it appear.
8. Vendor sandboxes (optional, for real-vendor tests)
For send-to-real-phone tests:
direnv exec . pnpm dev:vendors:real
Reads .env.vendors.local (gitignored) with real sandbox keys (Twilio test creds, Meta WhatsApp sandbox phone number, SendGrid sandbox API key with sink). Sends are categorised system. Recipients must be on the engineer allowlist.
9. Database tooling
pnpm -F notification-service db:migrate # run pending migrations
pnpm -F notification-service db:reset # drop + re-create local DB
pnpm -F notification-service db:gen # regenerate Drizzle schema types
pnpm -F notification-service db:psql # psql shell with role melmastoon_notification_app + app.tenant_id set to DEV_TENANT_ID
pnpm -F notification-service db:partman:run # force pg_partman maintenance
10. Tests
pnpm -F notification-service test # unit + application + contract (no Docker needed)
pnpm -F notification-service test:integration # Testcontainers-backed integration
pnpm -F notification-service test:flows # end-to-end domain flows
pnpm -F notification-service test:contracts # OpenAPI + event-schema drift + webhook fixtures
pnpm -F notification-service coverage # produces coverage report
Targeted runs:
pnpm -F notification-service test src/domain/notification
pnpm -F notification-service test:integration tests/integration/flows/booking-confirmation.test.ts
11. Lint, format, typecheck
pnpm -F notification-service lint
pnpm -F notification-service format
pnpm -F notification-service typecheck
Pre-commit hooks (Husky):
lint-stagedrunseslint --fix+prettier.pii-grepscans staged files for plaintext PII patterns in log statements.gitleaksscans for secrets.
12. Working with templates locally
pnpm -F notification-service templates:import path/to/templates.yaml # bulk import
pnpm -F notification-service templates:preview reservation.confirmed.email # render to terminal + open HTML in browser
pnpm -F notification-service templates:lint --tenant tnt_LOCAL… # call lint tool against the AI orchestrator stub
The preview tool supports --locale ps-AF to test RTL rendering; output is opened via xdg-open on Linux, open on macOS, Start-Process in PowerShell.
13. Observability locally
- Jaeger UI: http://localhost:16686
- Grafana: http://localhost:3009 (admin / admin)
- Dashboards: Notifications Overview, Per-Channel Funnel, Vendor Health, Templates, AI, Compliance
- Prometheus: http://localhost:9090
- Loki: query via Grafana Explore
14. Useful debug recipes
| Goal | Command |
|---|---|
| List unsent queued notifications | pnpm -F notification-service db:psql then select id, template_key, channel, status from notifications where status in ('queued','scheduled') order by queued_at desc limit 20; |
| Force re-dispatch | curl -X POST localhost:3007/api/v1/notifications/<id>/resend ... |
| Inspect outbox lag | select count(*), min(enqueued_at) from outbox where published_at is null; |
| Drop suppression for my email | update suppressions set released_at=now(), released_by='dev' where address_hash='sha256:…'; |
| Trigger AI fallback | set env AI_ORCHESTRATOR_FAIL=1 and restart |
| Replay a DLQ entry | curl -X POST localhost:3007/api/v1/internal/dlq/<id>/retry ... |
15. Common gotchas
- PowerShell quoting for
curlJSON: preferInvoke-RestMethodor wrap JSON in single quotes when in WSL. - Pub/Sub emulator does not support some prod features (schemas + ordering keys are simulated client-side); contract tests catch the rare differences.
- GCS emulator signed URLs use a fake signature; do not paste them into real GCS clients.
- Time zones: scheduler logic depends on per-tenant
Asia/Kabuletc. Local dev uses your machine TZ for display but stores ISO UTC; quiet-hours tests should setTZ=UTCbefore running. - WhatsApp template approval: locally always "approved" via the stub; in staging, real approval is required before sends succeed.
- AI personalisation: the orchestrator stub returns deterministic outputs to keep tests reproducible. Toggle
AI_ORCHESTRATOR_REAL=1to call the real orchestrator-dev for an integration check.
16. Resetting everything
pnpm dev:infra:down
docker volume rm melmastoon_postgres_data melmastoon_redis_data melmastoon_gcs_data
pnpm dev:infra
pnpm -F notification-service db:migrate && pnpm -F notification-service seed:dev
This is the safe nuclear option when state gets weird.
17. Where to look next
- Adding a new template? → API_CONTRACTS §4 + DOMAIN_MODEL Template aggregate.
- Adding a new channel adapter? → APPLICATION_LOGIC §1 Ports + SECURITY_MODEL §6 Vendor credentials.
- Changing an event payload? → EVENT_SCHEMAS §6 + bump
vNif breaking. - Touching the DB schema? → MIGRATION_PLAN + DATA_MODEL §7.