Skip to content

Latest commit

 

History

65 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sisques Account (account-api)

Identity + tenancy service for the Sisques Labs platform (Gardenia, Nexora, and future apps). It owns:

  • User — the platform identity record (id, email, displayName, platformAdmin).
  • Auth — registration/login backed by Keycloak (self-hosted, shared via local-dev-stack), and Sisques Account's own signed JWT (access token) + opaque refresh token (its own Session aggregate). Apps never talk to Keycloak directly — only account-api does.
  • Tenancy — the platform-level mechanics of a tenant (name, members, roles). What each role means inside a given app (e.g. Gardenia's "member" can water but not delete plants) is that app's own concern, not this service's.

See /Users/javi/Documents/projects/sisques-labs/sisques-account-architecture.md for the full design. This repo currently implements the MVP: account-api standalone, validated via tests/Postman — no account-web frontend yet, no email-based tenant invites.

Built from sisques-labs/nestjs-template — DDD + CQRS + Hexagonal architecture. Three bounded contexts: src/contexts/user/, src/contexts/auth/ and src/contexts/tenancy/ — see each context's own README.md for the one/two/three-context decision, aggregates, and public API.

Prerequisites

  • Node (see .nvmrc) + pnpm (see packageManager in package.json)
  • Docker + Docker Compose v2
  • local-dev-stack running, for the shared Postgres and Keycloak — see below

Running locally

1. Shared Postgres + Keycloak (local-dev-stack)

account-api uses local-dev-stack's shared Postgres instance and shared Keycloak instance rather than spinning up its own — see "Keycloak — where it runs" below for why. account_db is already registered in that repo's docker/postgres/init-db.sh, and the sisques-account realm + account-api client are registered as local-dev-stack/docker/keycloak/realms/account-api-realm.json. Start (or reuse) the stack:

cd ../local-dev-stack
docker compose up -d

If the stack was already running from before this database was added, init-db.sh won't retroactively create it (it only runs on first boot of an empty volume) — create it by hand instead:

docker compose exec postgres psql -U devuser -d postgres -c "CREATE DATABASE account_db;"

Similarly, if the stack was already running from before the account-api-realm.json file was added, re-run the import job by hand instead of waiting for a fresh boot:

docker compose up -d keycloak-realm-import

Keycloak admin console at http://localhost:8084 (admin / admin, local dev only — see local-dev-stack's README "Keycloak" section). The service account (manage-users/view-users/query-users on realm-management) isn't part of the imported realm JSON (that combination crashes Keycloak's import on this version — see local-dev-stack's README); grant those role mappings once by hand in the admin console: Users → service-account-account-api → Role mapping → Assign role → filter by clients → realm-management.

2. The app

pnpm install
cp .env.example .env   # defaults already point at local-dev-stack's shared
                        # Postgres and Keycloak — see .env.example
pnpm dev

Migrations run automatically on boot (DATABASE_MIGRATIONS_RUN defaults to true) and create the 4 MVP tables: app, user, tenant, tenant_membership.

Keycloak — where it runs

Decision: Keycloak lives in local-dev-stack, not in this repo's own docker-compose.yml. It used to be kept here on the reasoning that it had exactly one consumer (account-api is the architecture doc's one explicit exception to "apps never talk to Keycloak directly" — it is the adapter boundary), mirroring how this template keeps its own otel-collector/Jaeger alongside local-dev-stack's shared Postgres. That's no longer true: more than one service now needs Keycloak, so it's centralized in local-dev-stack the same way Postgres already was, with each service registering its own realm/client under local-dev-stack/docker/keycloak/realms/ (this repo's is account-api-realm.json).

Running tests

pnpm test              # unit (mocked, no infra needed)
pnpm test:db:up        # postgres-test (5433) + keycloak-test (8082)
pnpm test:integration  # persistence boundaries, real Postgres
pnpm test:e2e          # full HTTP flows — real Postgres AND real Keycloak
pnpm test:db:down

docker-compose.test.yml provisions an isolated keycloak-test (same realm-export.json, port 8082) purely so e2e specs can exercise the real KeycloakIdentityProviderAdapter for register/login — separate from the shared dev Keycloak on 8084 (local-dev-stack) so both can run at once. This test-only container still uses Keycloak's own --import-realm flag (see docker-compose.test.yml / .github/workflows/ci.yml) rather than the REST-API-based import local-dev-stack uses for dev — it hasn't hit the startup crash local-dev-stack's README documents, but if it ever does, apply the same fix there.

Example flow

Register → login → create an app → create a tenant (creator becomes owner) → add an existing user as a member → list members → refresh.

BASE=http://localhost:3000/api/v1

# 1. Register the tenant creator
curl -s -X POST $BASE/auth/register -H 'Content-Type: application/json' -d '{
  "email": "owner@example.com", "password": "Sup3rStrongPassw0rd!", "displayName": "Owner"
}'

# 2. Log in — returns { accessToken, refreshToken } (also set as cookies)
curl -s -X POST $BASE/auth/login -H 'Content-Type: application/json' -d '{
  "email": "owner@example.com", "password": "Sup3rStrongPassw0rd!"
}'
# -> save accessToken as $TOKEN, refreshToken as $REFRESH

# 3. Register the app (bootstrapping plumbing — no MVP endpoint touches `app`
#    otherwise, but `tenant.app_id` is a required FK)
curl -s -X POST $BASE/apps -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
  "slug": "gardenia", "name": "Gardenia"
}'
# -> save appId as $APP_ID

# 4. Create a tenant — $TOKEN's user becomes owner automatically
curl -s -X POST $BASE/tenants -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
  "appId": "'$APP_ID'", "name": "My Garden"
}'
# -> save tenantId as $TENANT_ID (slug defaults to "my-garden" when omitted)

# 5. Register a second user to add as a member
curl -s -X POST $BASE/auth/register -H 'Content-Type: application/json' -d '{
  "email": "member@example.com", "password": "Sup3rStrongPassw0rd!", "displayName": "Member"
}'

# 6. Add them as a member (by email — no invite flow in the MVP)
curl -s -X POST $BASE/tenants/$TENANT_ID/members -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
  "email": "member@example.com", "role": "member"
}'

# 7. List members — owner + the new member
curl -s $BASE/tenants/$TENANT_ID/members -H "Authorization: Bearer $TOKEN"

# 8. Refresh — rotates the refresh token, issues a new access token
curl -s -X POST $BASE/auth/refresh -H 'Content-Type: application/json' -d '{
  "refreshToken": "'$REFRESH'"
}'

Swagger UI at http://localhost:3000/docs documents every request/response shape.

What's included (cross-cutting)

Area Where
Config + env validation src/core/config/ (Zod), incl. auth.config.ts (JWT + Keycloak)
Auth infrastructure src/core/security/JwtAuthGuard, @CurrentUser(), shared JwtService. Cross-cutting (used by every context), not owned by auth
Health checks src/core/health/GET /api/health/live, GET /api/health/ready
Logging / OTel / MCP / Kafka forwarding Unchanged from the template — see openspec/config.yaml and each module's own comments
Database src/database/migrations/ — the MVP tables (app, user, tenant, tenant_membership, session)

Architecture

DDD + CQRS + Hexagonal. Full rules in .claude/skills/architecture/SKILL.md; project-wide conventions in openspec/config.yaml. Context-specific design (aggregates, cross-context ports, public API) lives in src/contexts/user/README.md, src/contexts/auth/README.md and src/contexts/tenancy/README.md.

About

Sisques Account API - identity, tenancy and token issuance for the Sisques Labs platform

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages