Skip to content

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