Architecture — dot-cOS
System Overview
graph TB
subgraph Clients
FE[cos.dotevolve.net\nFrontend\nReact/Vite]
AD[cos-admin.dotevolve.net\nAdmin Dashboard\nReact/Vite]
end
subgraph Platform
P[portal.dotevolve.net\nEnd-User Portal]
ADM[admin.dotevolve.net\nAdmin Portal]
PA[portal-api.dotevolve.net\nPortal API]
SB[Supabase Auth\nJWT IdP]
end
subgraph dot_cOS_Backend
GW[cos-api.dotevolve.net\nAPI Gateway\nNode.js/Express]
WS[cos-workflow.dotevolve.net\nWorkflow Service\nNode.js/Prisma]
RE[cos-rules.dotevolve.net\nRule Engine]
PG[(Supabase PostgreSQL\nWorkflow Data)]
RMQ[RabbitMQ\nEvent Bus]
end
FE -->|Bearer JWT| GW
AD -->|Bearer JWT| GW
GW -->|x-tenant-id header| WS
GW -->|x-tenant-id header| RE
WS -->|tenantId scoped| PG
WS -->|events| RMQ
RE -->|consumes| RMQ
P -->|SSO Link| FE
ADM -->|Manage Tenants| PA
SB -->|JWT| FE
SB -->|JWT| AD
Request Flow
Authenticated Request Through Gateway
sequenceDiagram
participant FE as dot-cOS Frontend
participant GW as API Gateway
participant SB as Supabase Auth
participant WS as Workflow Service
participant PG as PostgreSQL
FE->>GW: GET /api/v1/workflows (Bearer JWT)
GW->>SB: supabase.auth.getUser(token)
SB-->>GW: { user: { app_metadata: { activeTenantId } } }
GW->>GW: resolveTenant: req.tenantId = activeTenantId
GW->>WS: Proxy request + x-tenant-id: activeTenantId
WS->>WS: resolveTenant middleware reads x-tenant-id
WS->>PG: WorkflowInstance.findMany({ where: { tenantId } })
PG-->>WS: Workflow instances for this tenant only
WS-->>GW: Response
GW-->>FE: 200 OK
Tenant Resolution (No HTTP Call)
The API Gateway extracts activeTenantId directly from the JWT app_metadata — no HTTP call to the workflow service:
// API Gateway resolveTenant middleware
const meta = req.user?.app_metadata ?? {};
const tenantId = meta.activeTenantId;
if (!tenantId) {
return res
.status(401)
.json({ error: "No active tenant in JWT. Please re-login." });
}
req.tenantId = tenantId;
req.headers["x-tenant-id"] = tenantId;
next();
Service Responsibilities
| Service | Responsibility |
|---|---|
| API Gateway | JWT validation, tenant resolution from JWT claims, request proxying |
| Workflow Service | Compliance workflow CRUD, document generation, MCA form mapping, IDP pipeline |
| Rule Engine | Compliance rule evaluation, webhook processing |
| Admin Dashboard | Tenant-scoped configuration, SSO deep link entry point |
Tenant Isolation
graph LR
JWT[JWT\napp_metadata.activeTenantId] --> GW[API Gateway\nresolveTenant]
GW -->|x-tenant-id header| WS[Workflow Service]
WS --> Q1[WorkflowInstance.findMany\nwhere: tenantId]
WS --> Q2[Entity.findMany\nwhere: tenantId]
WS --> Q3[AuditLog.findMany\nwhere: tenantId]
All Prisma queries in the Workflow Service include where: { tenantId }. The Tenant model is a lightweight reference — status and plan are read from the Central Registry.
What Changed in the Central Portal Migration
| Concern | Before | After |
|---|---|---|
| Tenant resolution | HTTP call to /api/v1/me → DB lookup by email |
JWT claim extraction (no HTTP call) |
tenantCache |
In-memory email→tenantId Map | Removed |
| User password | Stored in Prisma User.password |
Removed — Supabase Auth |
subscriptionPlan |
Prisma Tenant.subscriptionPlan |
Removed — read from Central Registry |