iam-service — Local Dev Setup
Get a working iam-service running locally in under 10 minutes. All external GCP dependencies (Cloud SQL, Memorystore, KMS, Pub/Sub) are emulated.
1. Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node.js | 20 LTS | nvm recommended |
| pnpm | 9.x | corepack enable |
| Docker | 24+ | for compose stack |
| Docker Compose | v2 | bundled with modern Docker |
| GNU Make | any | optional convenience |
| Git | 2.40+ | hooks via Husky |
gcloud CLI | latest | for production-like flows; optional locally |
Optional: mkcert for trusted local TLS certs, httpie or curl, jq.
2. Repository Layout (this service)
services/iam-service/
├── src/
│ ├── domain/ # pure TS aggregates, value objects, events
│ ├── application/ # use cases, ports, command/query handlers
│ ├── infrastructure/ # adapters: Postgres, Redis, KMS, Pub/Sub, IdP
│ ├── presentation/ # http (fastify), schemas, openapi
│ └── main.ts # composition root
├── test/
│ ├── unit/
│ ├── integration/
│ ├── contract/ # pact
│ └── e2e/ # playwright stack
├── db/
│ ├── migrations/ # node-pg-migrate
│ └── seeds/
├── openapi/
├── asyncapi/
├── docker-compose.dev.yml
├── docker-compose.e2e.yml
├── Dockerfile
├── package.json
├── tsconfig.json
└── README.md
3. Quick Start
git clone git@github.com:melmastoon/platform.git
cd platform/services/iam-service
cp .env.example .env.local
pnpm install
pnpm compose:up # spins up postgres, redis, fake-pubsub, fake-kms, mock-idp, mailhog
pnpm db:migrate
pnpm db:seed
pnpm dev # iam-service on http://localhost:4001
Check it's alive:
curl http://localhost:4001/health/ready
# → {"status":"ok","db":"ok","redis":"ok","kms":"ok"}
curl http://localhost:4001/.well-known/jwks.json | jq .
4. Compose Stack
docker-compose.dev.yml services:
| Service | Port | Purpose |
|---|---|---|
postgres | 55432 | Postgres 15 |
redis | 56379 | Redis 7 (no AUTH locally) |
fake-pubsub | 58085 | Pub/Sub emulator (gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators) |
fake-kms | 58000 | local KMS shim (melmastoon/fake-kms:latest) — Ed25519 + HSM-style API |
mock-idp | 58088 | Combined OIDC + SAML mock with seeded users |
mailhog | 58025 (web 58025, smtp 51025) | inbox for magic-link / reset emails |
Tear down + reset:
pnpm compose:down -v # removes volumes
5. Environment Variables
.env.example (committed, redacted):
NODE_ENV=development
PORT=4001
LOG_LEVEL=debug
# Postgres
DATABASE_URL=postgres://iam:iam@localhost:55432/iam?sslmode=disable
# Redis
REDIS_URL=redis://localhost:56379/0
# Pub/Sub emulator
PUBSUB_EMULATOR_HOST=localhost:58085
PUBSUB_PROJECT_ID=melmastoon-dev
# Fake KMS
KMS_ENDPOINT=http://localhost:58000
KMS_KEY_RING=projects/melmastoon-dev/locations/me-central1/keyRings/iam
KMS_SIGNING_KEY=projects/melmastoon-dev/locations/me-central1/keyRings/iam/cryptoKeys/jwt-signing
KMS_TENANT_CA_KEY=projects/melmastoon-dev/locations/me-central1/keyRings/iam/cryptoKeys/tenant-ca
# JWT
JWT_ISSUER=https://auth.dev.melmastoon.local
JWT_AUDIENCE=https://api.dev.melmastoon.local
JWT_ACCESS_TTL_S=900
JWT_REFRESH_TTL_S=2592000
JWT_REFRESH_TTL_OFFLINE_S=604800
# Argon2id
PASSWORD_ARGON2_M=65536
PASSWORD_ARGON2_T=3
PASSWORD_ARGON2_P=1
PASSWORD_HASH_VERSION=1
# Sessions
IDLE_LOCK_S=300
ACCOUNT_LOCK_THRESHOLD=10
ACCOUNT_LOCK_WINDOW_S=900
ACCOUNT_LOCK_DURATION_S=900
# OIDC mock
DEV_OIDC_ISSUER=http://localhost:58088/oidc
DEV_OIDC_CLIENT_ID=iam-dev
DEV_OIDC_CLIENT_SECRET=dev-secret
# SAML mock
DEV_SAML_METADATA_URL=http://localhost:58088/saml/metadata
# Notification (mailhog)
SMTP_HOST=localhost
SMTP_PORT=51025
SMTP_FROM=auth-dev@melmastoon.local
# AI orchestrator (point at local stub or disable)
AI_ORCHESTRATOR_URL=http://localhost:4090
AI_ORCHESTRATOR_ENABLED=false
# HIBP
HIBP_API_KEY=disabled-locally
HIBP_FAIL_OPEN=true
# Tenant fingerprint pepper (KMS-backed in prod, file in dev)
TENANT_FINGERPRINT_SECRET=dev-pepper-do-not-use-in-prod
Never commit .env.local.
6. Seed Data
pnpm db:seed populates:
| Tenant | Tenant ID | Notes |
|---|---|---|
Dev Hotel Group | ten_01HDEV0000000000000000000 | sample chain |
Beach Resort Doha | ten_01HDEV0000000000000000001 | property under chain |
| User | Password | Role | MFA | Notes | |
|---|---|---|---|---|---|
| Platform Admin | admin@melmastoon.local | Dev_Admin!2026 | platform_admin | TOTP enrolled (seed = JBSWY3DPEHPK3PXP) | tenant-less |
| Tenant Owner | owner@dev.melmastoon.local | Dev_Owner!2026 | tenant_admin | none | tenant …0000 |
| Front Desk Staff | frontdesk@dev.melmastoon.local | Dev_Front!2026 | staff | none | tenant …0001 |
| Housekeeping | housekeep@dev.melmastoon.local | Dev_House!2026 | staff | none | tenant …0001 |
| Guest | guest@example.com | Dev_Guest!2026 | guest | none | tenant-less guest record |
OIDC users in mock-idp (mock-idp/users.json): sso-user@dev.example, sso-admin@dev.example. Tenant-IdP linkage seeded so POST /api/v1/auth/sso/oidc/dev/init works for ten_…0001.
A seeded API key for the tenant-owner is printed during seed; copy it from console (mlk_...).
7. Common Commands
# Build
pnpm build
# Run
pnpm dev # tsx watch
pnpm start # built artifact
# Test
pnpm test # unit + integration (Testcontainers)
pnpm test:unit
pnpm test:integration
pnpm test:contract # pact provider verification (requires broker URL)
pnpm test:e2e # spins up docker-compose.e2e.yml + playwright
# DB
pnpm db:migrate
pnpm db:rollback
pnpm db:reset # drop + create + migrate + seed
pnpm db:psql # psql shell
# OpenAPI / AsyncAPI
pnpm openapi:generate
pnpm openapi:lint
pnpm asyncapi:lint
# Lint / format
pnpm lint
pnpm lint:fix
pnpm format
# Type check
pnpm typecheck
# Coverage
pnpm test:coverage
8. Useful Dev Scripts
8.1 Issue a test JWT for any seeded user
pnpm script:issue-jwt --email owner@dev.melmastoon.local
# → writes access_token + refresh_token to ./.dev-tokens.json
Use it:
TOKEN=$(jq -r .access_token .dev-tokens.json)
curl -H "Authorization: Bearer $TOKEN" http://localhost:4001/api/v1/users/me
8.2 Force-rotate the JWT signing key
pnpm script:rotate-jwt-key
# emits new kid, publishes JWKS, keeps old kid for 5 min
8.3 Simulate SSO callback
pnpm script:simulate-sso --tenant ten_01HDEV0000000000000000001 --email sso-user@dev.example
8.4 Decode a Melmastoon JWT
pnpm script:decode-jwt $TOKEN | jq .
8.5 List MailHog inbox
Browse http://localhost:58025 — magic links and password resets land here.
8.6 Trigger a fake credential-stuffing storm
pnpm script:storm --rps 50 --duration 30s
# observe http://localhost:4001/metrics
9. Connecting Electron Desktop Locally
cd ../../desktop/electron-shell
cp .env.example .env.local
# point AUTH_URL=http://localhost:4001
pnpm dev
The desktop app's first run requires:
- Login with
frontdesk@dev.melmastoon.local. - From the dev menu: "Bind device for offline" (re-prompts password = fresh-auth).
- After bind, you can disconnect network and the desktop will refresh tokens for up to 7 d via the device cert.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
KMS unavailable on startup | fake-kms not running | pnpm compose:up |
Migration already executed | stale DB | pnpm db:reset |
JWKS empty | signing key not initialized | pnpm script:rotate-jwt-key |
Pub/Sub: project not found | missing PUBSUB_PROJECT_ID env | check .env.local |
OIDC discovery 404 | mock-idp not up | pnpm compose:up |
argon2 native build failed | Python / build tools missing | install build essentials per argon2 README |
EADDRINUSE 4001 | another process on port | lsof -i :4001 | awk 'NR>1{print $2}' | xargs kill |
| Magic-link not received | SMTP env wrong, mailhog down | check http://localhost:58025 |
11. IDE Setup
VS Code recommended extensions (.vscode/extensions.json):
dbaeumer.vscode-eslintesbenp.prettier-vscodevitest.explorercucumberopen.cucumber-officialmtxr.sqltools+mtxr.sqltools-driver-pghumao.rest-client
Workspace settings enforce: format-on-save, organize imports, run ESLint on save.
12. Pre-commit Hooks
Husky runs:
pnpm lint:stagedpnpm typecheck(changed packages only)pnpm test:unit --changed
commit-msg enforces conventional commits: feat(iam): …, fix(iam): ….
13. Nice-to-haves
gcloud auth application-default loginif you want to test against a real GCP project (staging).playwright installonce before first E2E run.pactbroker docker is available viadocker-compose.pact.yml.signozlocal stack viadocker-compose.observability.ymlfor traces.