Architecture — dot-cOS
System Architecture
graph TB
subgraph Clients
FE[dot-cos-frontend\ncos.dotevolve.net]
AD[dot-cos-admin-dashboard\ncos-admin.dotevolve.net]
end
subgraph Gateway
GW[dot-cos-api-gateway\ncos-api.dotevolve.net\nNode.js / Express]
end
subgraph Services
WS[dot-cos-workflow-service\ncos-workflow.dotevolve.net\nNode.js / Prisma]
end
subgraph Data
PG[(Supabase PostgreSQL\nWorkflow Data)]
QST[QStash\nAsync messaging]
REDIS[(Redis\nRate Limiting)]
end
subgraph Platform
PA[dot-portal-api\nPortal API]
SB[Supabase Auth]
CR[(Central Registry\nSupabase PostgreSQL)]
end
FE -->|Bearer JWT| GW
AD -->|Bearer JWT| GW
GW -->|supabase.auth.getUser| SB
GW -->|x-tenant-id header| WS
WS -->|tenantId scoped| PG
WS -->|events| QST
WS -->|tenant status| CR
AD -->|SSO| GW
API Gateway — Tenant Resolution
The gateway extracts activeTenantId from JWT claims — no HTTP call to the workflow service:
flowchart TD
A[Request + Bearer JWT] --> B[requireAuth middleware]
B --> C[supabase.auth.getUser token]
C --> D{Valid?}
D -->|No| E[403 Forbidden]
D -->|Yes| F[Attach user.app_metadata to req.user]
F --> G[resolveTenant middleware]
G --> H{activeTenantId in app_metadata?}
H -->|No, protected route| I[401 Unauthorized]
H -->|No, public route| J[next - no tenantId]
H -->|Yes| K[req.tenantId = activeTenantId]
K --> L[Set x-tenant-id header]
L --> M[Proxy to downstream service]
Workflow Service — Tenant Resolution
flowchart TD
A[Request + x-tenant-id header] --> B[resolveTenant middleware]
B --> C{x-tenant-id present?}
C -->|No| D[401 Unauthorized]
C -->|Yes| E[Query Central Registry\nfor tenant status]
E --> F{Status?}
F -->|suspended| G[403 Forbidden]
F -->|pending_payment| H[402 Payment Required]
F -->|not found| I[401 Unauthorized]
F -->|active| J[req.tenantId = tenantId]
J --> K[All Prisma queries\nwhere: tenantId]
Data Flow — Workflow Creation
sequenceDiagram
participant FE as Frontend
participant GW as API Gateway
participant WS as Workflow Service
participant PG as PostgreSQL
participant QST as QStash
FE->>GW: POST /api/v1/workflows (Bearer JWT)
GW->>GW: Validate JWT, extract activeTenantId
GW->>WS: POST /api/v1/workflows (x-tenant-id: tenantId)
WS->>WS: Validate tenant status
WS->>PG: WorkflowInstance.create({ tenantId, templateId, entityId })
PG-->>WS: Created instance
WS->>QST: Publish WORKFLOW_CREATED event
WS-->>GW: 201 Created
GW-->>FE: 201 Created
Prisma Schema Changes (Central Portal Migration)
| Change | Before | After |
|---|---|---|
Tenant.subscriptionPlan |
String @default("FREE") |
Removed — read from Central Registry |
User.password |
String // Hashed |
Removed — Supabase Auth |
User.supabaseId |
Not present | String? @unique |
| Tenant resolution | HTTP call to /api/v1/me → DB lookup |
JWT claim extraction via x-tenant-id header |
tenantCache |
In-memory email→tenantId Map | Removed entirely |
Infrastructure
dot-cOS services
├── Supabase PostgreSQL (workflow data — Prisma ORM)
├── Supabase Auth (JWT validation)
├── Central Registry (tenant status checks)
├── QStash (Upstash) (async HTTP messaging)
├── Redis (rate limiting)
└── OpenAI GPT-4o (IDP — document extraction)
Local Development
# Start all infrastructure
docker compose up -d # Postgres, Redis (see service docs for QStash env)
# Run workflow service
cd dot-cos-workflow-service
npm run dev
# Run API gateway
cd dot-cos-api-gateway
npm run dev
See dot-cos/docker-compose.yml for the full local stack definition.