Local Dev Setup
:::info Source
Sourced from services/identity-service/LOCAL_DEV_SETUP.md in the documentation repo.
:::
1. Prerequisites
| Tool | Version |
|---|---|
| Node.js | 20 LTS |
| pnpm | 9.x |
| Docker | 24.x |
| Docker Compose | v2 |
PostgreSQL CLI (psql) | 16 |
2. Repository Layout
services/identity-service/
├── src/
│ ├── domain/ # User, Credential, Session, Device aggregates
│ ├── application/ # Use cases (RegisterUser, LoginUser, …)
│ ├── infrastructure/ # Postgres repos, NATS outbox, KMS adapter
│ ├── api/ # Fastify routes
│ └── index.ts
├── test/
├── migrations/ # Liquibase / SQL
├── docker-compose.dev.yml
├── .env.example
├── package.json
└── README.md
3. Quick Start
cd services/identity-service
cp .env.example .env.local
pnpm install
docker compose -f docker-compose.dev.yml up -d # postgres, redis, nats, localstack (KMS), mailhog
pnpm db:migrate
pnpm seed
pnpm dev
Service listens on http://localhost:3001.
4. Required Dependencies (docker-compose.dev.yml)
| Service | Port | Purpose |
|---|---|---|
postgres | 5432 | Primary DB (schema identity) |
redis | 6379 | Session cache, rate-limit counters |
nats | 4222 | JetStream event bus |
localstack | 4566 | KMS emulator (signs JWTs with Ed25519) |
mailhog | 8025 (UI), 1025 (SMTP) | Captures outgoing email (password reset, verification) |
mock-idp | 8080 | OIDC + SAML mock IdP (lightweight, see tools/mock-idp) |
5. Environment Variables (.env.example)
PORT=3001
LOG_LEVEL=debug
ENV=dev
DATABASE_URL=postgres://identity:identity@localhost:5432/identity
DATABASE_POOL_MAX=20
DATABASE_RLS_ENFORCED=true
REDIS_URL=redis://localhost:6379/1
NATS_URL=nats://localhost:4222
NATS_STREAM=IDENTITY
NATS_OUTBOX_INTERVAL_MS=200
KMS_PROVIDER=localstack
KMS_ENDPOINT=http://localhost:4566
KMS_KEY_ID=alias/ghasi-identity-jwt-signer
JWT_ACCESS_TTL_SECONDS=900
JWT_REFRESH_TTL_DAYS=30
JWT_ISSUER=https://id.dev.ghasi.local
PASSWORD_ARGON2_MEMORY_KB=65536
PASSWORD_ARGON2_ITERATIONS=3
PASSWORD_ARGON2_PARALLELISM=1
MFA_TOTP_ISSUER=Ghasi-edTech-Dev
MAIL_SMTP_HOST=localhost
MAIL_SMTP_PORT=1025
MAIL_FROM=no-reply@dev.ghasi.local
# Mock IdPs
OIDC_MOCK_ISSUER=http://localhost:8080
SAML_MOCK_METADATA_URL=http://localhost:8080/metadata
RATE_LIMIT_LOGIN_IP=10
RATE_LIMIT_LOGIN_EMAIL=30
6. Seed Data
pnpm seed creates:
| Password | Role | Purpose | |
|---|---|---|---|
platform-admin@dev.local | Admin!2345 | platform_admin | Global admin |
owner@acme.dev.local | Owner!2345 | org_owner (tenant acme) | Tenant owner |
author@acme.dev.local | Author!2345 | author (tenant acme) | Authoring |
learner@acme.dev.local | Learner!2345 | learner (tenant acme) | Learner |
owner@globex.dev.local | Globex!2345 | org_owner (tenant globex) | Two-tenant iso tests |
learner@globex.dev.local | Learn!2345 | learner (tenant globex) | Two-tenant iso tests |
7. Mock Services
- Mock IdP (
tools/mock-idp) — issues OIDC tokens + SAML assertions fortest@dev.local. Reset password is the email itself. - Mock KMS (localstack) — Ed25519 keypair created on startup via
scripts/kms-init.sh. - Mock Email (MailHog) — browse captured emails at
http://localhost:8025.
8. Useful Commands
pnpm dev # Start with hot-reload (tsx watch)
pnpm test # Unit tests
pnpm test:integration # Testcontainers (Postgres, Redis, NATS)
pnpm test:contract # Pact tests (JWT claims → consumers)
pnpm lint
pnpm typecheck
pnpm db:migrate
pnpm db:rollback
pnpm seed
pnpm openapi:check # Diff against committed /openapi/identity.yaml
9. Debugging
- Logs: structured JSON to stdout; use
pnpm dev | pino-prettyfor readable output. - Traces: OTel → Jaeger at
http://localhost:16686(spawned in compose). - DB:
psql $DATABASE_URL— schemaidentity; all tables have RLS; set session withSET app.tenant_id = '<tid>'; - JWKS:
curl http://localhost:${PORT:-8080}/.well-known/jwks.json(not under/api/v1; defaultPORTin code is8080)
10. Two-Tenant Isolation Test
pnpm test:isolation
Runs the platform-wide "two-tenant simulator" against identity-service: attempts to read acme session as globex user; asserts all such attempts receive 403 authz.tenant_not_a_member.
11. Keycloak OIDC (US-116) — local realm
Point identity-service at a Keycloak realm (or any OIDC-compatible IdP that exposes standard discovery + PKCE). Required environment variables:
| Variable | Purpose |
|---|---|
KEYCLOAK_ISSUER | Issuer URL, e.g. http://localhost:8180/realms/ghasi (no trailing slash inconsistency: discovery issuer must match allowlist) |
KEYCLOAK_ALLOWED_ISSUERS | Comma-separated allowlist; must include the same issuer string returned by /.well-known/openid-configuration |
KEYCLOAK_CLIENT_ID | OIDC client ID (confidential or public with PKCE) |
KEYCLOAK_CLIENT_SECRET | Optional; omit for public clients |
IDENTITY_PUBLIC_BASE_URL | Public base URL of this service (used to build redirect_uri for /api/v1/auth/sso/keycloak/callback) |
IDENTITY_PLATFORM_JWT_SECRET | HS256 secret for short-lived platform access JWTs issued after SSO (replace with KMS-backed signing in production) |
IDENTITY_PLATFORM_JWT_ISSUER | iss claim for platform JWTs |
IDENTITY_PLATFORM_ACCESS_TTL_SECONDS | Optional; default 900 |
Register the redirect URI {IDENTITY_PUBLIC_BASE_URL}/api/v1/auth/sso/keycloak/callback on the Keycloak client. Flow: POST /api/v1/auth/sso/keycloak/start with Idempotency-Key, then browser redirect to IdP, then GET /api/v1/auth/sso/keycloak/callback?code=&state= → 302 to returnUrl with a short-lived exchange code. Integration tests use a mock OIDC server (test/integration/keycloak-sso.spec.ts).