Architecture overview
How the system fits together: Go microservices behind a Go gateway, a React micro-frontend shell, and Redis Streams as the event backbone. Read this before changing anything that crosses a service boundary; the drawings are in system diagrams.
Gateway topology
flowchart LR
subgraph clients
W[web host :5173]
R1[web-auth :5174]
R2[web-admin-users :5175]
R3[web-admin-roles :5176]
end
W --> GW
R1 -.federated into.-> W
R2 -.federated into.-> W
R3 -.federated into.-> W
GW[gateway :8000<br/>JWT verify · CORS · rate limit<br/>fail-closed route registry · docs]
GW --> A[auth :8081]
GW --> U[users :8082]
GW --> RB[rbac :8083]
GW --> WK[worker :8084]
RT[realtime :8085]:::ws
GW -. ws upgrade .-> RT
SC[scheduler :8086]:::ws
SC -. leader-elected events .-> WK
classDef ws stroke-dasharray: 5 5
- JWTs verified once at the edge; downstream services trust identity headers bound by the internal secret (
X-User-Id,X-Email,X-Internal-Secret). - The route registry is generated from every service’s
openapi.yamlat boot — a route without a spec does not exist, and an annotated permission missing from the compile-time catalog refuses boot (fail-closed).
Data ownership (schema-per-service)
erDiagram
auth_users ||--o{ auth_sessions : "sessions"
users_profiles {
bigint id PK
text display_name
}
rbac_roles ||--o{ rbac_role_permissions : ""
rbac_permissions ||--o{ rbac_role_permissions : ""
audit_audit_logs {
bigint id
text msg_id UK
}
| Schema | Owner | Never holds |
|---|---|---|
auth | credentials + sessions | profile fields |
users | profiles keyed by sub | credentials |
rbac | roles / permissions / user_roles | anything else |
audit | append-only trail (worker-only writer) | business data |
Cross-service writes are forbidden; lifecycle rides events instead.
The detailed worker/realtime, authentication, registration, token rotation, session-state, stream-flow, and trust-boundary views live in DIAGRAMS.md.
Streams & topics
flowchart LR
AU[auth] -- user.created --> ST1[(users.events)]
US[users] -- user.deleted --> ST1
US -- admin delete --> PL[[purge:profiles list]]
AU -- email.send --> ST2[(mail.jobs)]
ALL[any api] -- audit.entry --> ST3[(audit.events)]
US -- consumes --> ST1
RB[rbac] -- consumes default-role saga --> ST1
WK[worker] -- consumes: mail send, audit flush --> ST2
WK -- consumes --> ST3
US -- scheduled purge sweep --> PL
SC[scheduler] -- configured jobs --> ST4[(domain streams)]
AUTH2[auth force-logout] -- pub/sub channel --> RTK[realtime kick]
- Consumer groups start at stream position
0: pre-consumer events backfill. - Worker delivery is at-least-once with idempotent handlers and a DLQ after 5 attempts (
<stream>:dlq). - Claim freshness: role edits bump affected users’
ver; tokens carry it and the gateway enforces refresh on mismatch of permissions.
Web federation
flowchart TD
HOST["apps/web (host)<br/>router · auth context · RequirePermission"]
AUTH["apps/web-auth<br/>login/register/forgot/reset"]
USERS["apps/web-admin-users<br/>paginated table + modals"]
ROLES["apps/web-admin-roles<br/>role editor + permission sync"]
UI["@starter/ui tokens+primitives"]
CT["@starter/contracts typed client"]
HOST --> AUTH
HOST --> USERS
HOST --> ROLES
AUTH & USERS & ROLES --> UI
AUTH & USERS & ROLES --> CT
CT -- bearer + silent refresh --> GW[gateway :8000]
Remote URLs are runtime-configurable via /config.js (window.__REMOTE_URLS__), so the same images serve any deployment topology.
Observability flow
flowchart LR
SVC[every Go service<br/>/metrics + OTLP spans] --> PROM[Prometheus]
SVC -- traces --> OTEL[otel-collector]
SVC -- JSON logs --> LOKI[Loki]
OTEL --> TEMPO[Tempo]
PROM --> ALERT[Alertmanager]
PROM & LOKI & TEMPO --> GRAF[Grafana dashboards]
Trace IDs ride the traceparent header from the gateway through every hop and appear in each service’s slog lines next to the request id.