Skip to main content

Local Dev Setup

:::info Source Sourced from services/identity-service/LOCAL_DEV_SETUP.md in the documentation repo. :::

1. Prerequisites

ToolVersion
Node.js20 LTS
pnpm9.x
Docker24.x
Docker Composev2
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)

ServicePortPurpose
postgres5432Primary DB (schema identity)
redis6379Session cache, rate-limit counters
nats4222JetStream event bus
localstack4566KMS emulator (signs JWTs with Ed25519)
mailhog8025 (UI), 1025 (SMTP)Captures outgoing email (password reset, verification)
mock-idp8080OIDC + 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:

EmailPasswordRolePurpose
platform-admin@dev.localAdmin!2345platform_adminGlobal admin
owner@acme.dev.localOwner!2345org_owner (tenant acme)Tenant owner
author@acme.dev.localAuthor!2345author (tenant acme)Authoring
learner@acme.dev.localLearner!2345learner (tenant acme)Learner
owner@globex.dev.localGlobex!2345org_owner (tenant globex)Two-tenant iso tests
learner@globex.dev.localLearn!2345learner (tenant globex)Two-tenant iso tests

7. Mock Services

  • Mock IdP (tools/mock-idp) — issues OIDC tokens + SAML assertions for test@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-pretty for readable output.
  • Traces: OTel → Jaeger at http://localhost:16686 (spawned in compose).
  • DB: psql $DATABASE_URL — schema identity; all tables have RLS; set session with SET app.tenant_id = '<tid>';
  • JWKS: curl http://localhost:${PORT:-8080}/.well-known/jwks.json (not under /api/v1; default PORT in code is 8080)

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:

VariablePurpose
KEYCLOAK_ISSUERIssuer URL, e.g. http://localhost:8180/realms/ghasi (no trailing slash inconsistency: discovery issuer must match allowlist)
KEYCLOAK_ALLOWED_ISSUERSComma-separated allowlist; must include the same issuer string returned by /.well-known/openid-configuration
KEYCLOAK_CLIENT_IDOIDC client ID (confidential or public with PKCE)
KEYCLOAK_CLIENT_SECRETOptional; omit for public clients
IDENTITY_PUBLIC_BASE_URLPublic base URL of this service (used to build redirect_uri for /api/v1/auth/sso/keycloak/callback)
IDENTITY_PLATFORM_JWT_SECRETHS256 secret for short-lived platform access JWTs issued after SSO (replace with KMS-backed signing in production)
IDENTITY_PLATFORM_JWT_ISSUERiss claim for platform JWTs
IDENTITY_PLATFORM_ACCESS_TTL_SECONDSOptional; 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).