System Architecture Overview
Last updated: 2026-03-12
Architecture Diagram
Keycloak (IdP — self-hosted, Helm subchart)
▲ │
│ │ ROPC grant (login) + Admin REST API (registration)
│ ▼
API Gateway (apps/api-gateway) ← Handles auth server-side
│ POST /auth/login → ROPC grant, create Redis session, set cookie
│ POST /auth/register → Admin API user creation, auto-login via ROPC
│ POST /auth/logout → clear Redis session + cookie
│
│ session_id cookie (HttpOnly, Secure, SameSite=Lax)
│
Browser ─── cookie sent automatically on every request ─────────┐
│ │
│ GraphQL (Apollo Client, credentials: 'include') │
▼ │
Next.js Frontend (apps/web) ◄────────────────────────────────────┘
│ Custom /login and /register pages (fetch POST to gateway)
│ middleware.ts: cookie presence check only
│
│ Next.js rewrites (locally), K8s ingress (prod)
│ routes /auth/* + /graphql → Gateway
│
▼
API Gateway
│ SessionGuard: looks up session in Redis, extracts { userId, roles }
│ Refreshes access token server-side if near expiry
│ NestJS + Apollo Server (code-first GraphQL)
│
│ Redis pub/sub — payload carries { userId, roles }, never the raw token
│
▼
service-core ← NestJS microservice
(CRUD for all
domain entities)
Service Map
| Service | Purpose | Communication |
|---|---|---|
| Keycloak | Identity Provider — user management, roles (PostgreSQL-backed) | ROPC + Admin API (gateway ↔ Keycloak server-side) |
apps/web | Next.js frontend, App Router, Apollo Client, custom auth pages, blog | GraphQL → API gateway (via Next.js rewrites / K8s ingress) |
apps/api-gateway | GraphQL API, REST auth endpoints, Redis session management | GraphQL in, Redis out |
apps/service-core | Generic CRUD for all domain entities | Redis transport |
Architectural Decisions
Services are split by functionality, not by domain:
service-corehandles CRUD for all entities (items, orders, etc.)- New microservices are only created for complex business logic (e.g., trading engine)
- Domain types live in
packages/shared, not in individual services
Auth Flow
- User hits a protected
(app)route → Next.js middleware checks forsession_idcookie - If no cookie → redirect to
/login(custom Next.js page) - User enters credentials → form submits
POST /auth/loginwith{ email, password } - Gateway authenticates with Keycloak via ROPC grant (
grant_type=password) - Gateway creates Redis session, sets opaque
session_idcookie (HttpOnly, Secure, SameSite=Lax) - Frontend redirects to the original protected route
- All subsequent GraphQL requests include the cookie automatically (browser handles this)
- Gateway
SessionGuardlooks up session in Redis, refreshes token if near expiry - Extracts
{ userId, email, name, roles }into GQL context - Redis microservice messages carry this identity payload — never the raw token
Registration: custom /register page → POST /auth/register → gateway creates user via Keycloak Admin REST API (service account with manage-users role), then auto-logs in via ROPC.
Data Flow (Request Lifecycle)
- Frontend sends GraphQL query/mutation via Apollo Client (cookie sent automatically)
- API Gateway
SessionGuardlooks up session in Redis, validates, and populatesreq.user - Resolver sends a Redis message via
ClientProxy.send(MessagePattern, { ...payload, userId, roles }) - Microservice controller receives the message via
@MessagePattern - Controller delegates to service class for business logic
- Service returns result → controller → Redis → gateway resolver → GraphQL response → frontend
Shared Packages
| Package | Purpose |
|---|---|
packages/shared | Entity interfaces (User, AuthUser), DTOs, message pattern enums, ServiceResponse<T>, utility functions (getEnvVar, getRedisConfig, generateId) |
packages/config | Shared ESLint preset, TypeScript configs for NestJS and Next.js |
Infrastructure
- Build: Turborepo for monorepo orchestration, pnpm workspaces
- Local dev:
docker compose up -d(Redis, PostgreSQL, Keycloak), thenpnpm dev - Containers: Multi-stage Docker builds (Alpine, non-root, turbo prune)
- CI/CD: GitHub Actions → Docker build → ArgoCD sync
- Deployment: Helm charts on Kubernetes (k3s locally, any K8s in production)
- Auth: Keycloak (self-hosted, PostgreSQL-backed), gateway-managed sessions with Redis store
- Docs: Nextra v4 integrated at
/docswithinapps/web - Inter-service: Redis (pub/sub transport for NestJS microservices + session store)
See deep dives:
Current State and Known Limitations
- Items entity uses in-memory storage — Profile and portfolio entities (Project, Skill, Experience) use PostgreSQL/Prisma, but Items is still in-memory
- No tests — CI test step exists but is a no-op
- No message persistence — Redis pub/sub drops messages if a service is down
- Keycloak + PostgreSQL required — local development needs
docker compose up -dbeforepnpm dev - Gateway stateful — session store depends on Redis; Redis data loss invalidates all sessions
- Blog pages are static placeholders — no CMS backend yet
- Portfolio image uploads not supported —
imageUrlfields accept external URLs only, no file upload - No portfolio search/discovery — public profiles are accessed directly via
/u/[userId], no directory or search