F FourIA GitHub ↗

FourIA Cloud Architecture

On this page

Last updated: Jul 2026 Platform: Cloudflare Workers for Platforms (WFP) Database: Supabase PostgreSQL — PostgREST from the Dispatch Worker, Ecto from the Dashboard (docker-compose for local dev)


Overview

FourIA uses Cloudflare Workers for Platforms (WFP) to provision isolated, per-customer gateways. Each customer receives their own User Worker (deployed as tenant-{slug} in the dispatch namespace) which manages an OpenClaw gateway inside a Sandbox container, with independent bindings (R2, Durable Objects, Analytics Engine).

All tenant traffic enters through a single Dispatch Worker bound to the wildcard domain *.fouria.io. The Dispatch Worker extracts the tenant slug from the subdomain, resolves it against the shared Supabase PostgreSQL database (with a KV cache layer), resolves the user’s role, and dispatches to the appropriate User Worker with role/billing context in headers. No subdomain routing is used in the URL path — each tenant gets its own subdomain.

The FourIA Dashboard runs as a standalone Phoenix app inside a Cloudflare Container at dash.fouria.io (on the fouria.io zone, with a bypass worker route — no worker assigned), backed by the shared Supabase PostgreSQL database. A separate dev instance runs at dev-dash.fouria.io. The Dashboard is a pure control panel — it has no direct dependency on fouria or the Dispatch Worker. Platform data (clients, partners, users, instances, billing) is managed exclusively through the Dashboard.

Architecture principle: lerma and fouria are independent services that share only a data store. No direct HTTP dependencies exist between them.

DNS zones: Tenant traffic is served via *.fouria.io (fouria.io zone). The Dashboard is served via dash.fouria.io and dev-dash.fouria.io on the fouria.io zone through bypass worker routes — these are not handled by *.fouria.io.

TENANT ROUTING — fouria.io zone
────────────────────────────────
*.fouria.io

  ├─ {tenant}.fouria.io ──── (tenant traffic)
  │    │
  │    ├─ 1. Dispatch Worker (workers/fouria-dispatch/)
  │    │      │  slug:     extract tenant slug from hostname
  │    │      │  cache:    check KV (TENANT_INDEX, 60s TTL)
  │    │      │  db:       fallback query to Supabase (PostgREST)
  │    │      │  auth:     Cloudflare Access JWT (aud resolved per tenant)
  │    │      │  user:     resolve user role from client_users
  │    │      │  headers:  forward X-FourIA-Role, X-FourIA-Metadata
  │    │      │  limits:   plan-level CPU & subrequest limits
  │    │      │
  │    │      └─ DISPATCHER.get("tenant-{slug}")
  │    │           └─ User Worker: tenant-{slug}
  │    │                ├─ Sandbox container (OpenClaw gateway)
  │    │                ├─ BACKUP_BUCKET (R2: per-tenant)
  │    │                ├─ ANALYTICS (AE: per-tenant metrics)
  │    │                └─ Secrets (API keys, gateway token)
  │    │
  │    └─ ... (unlimited tenants in fouria-tenants namespace)

DASHBOARD — fouria.io zone (bypass worker routes)
────────────────────────────────
dash.fouria.io ──── (production, internal management)

  └─ FourIA Dashboard (apps/lerma/)
         │  runtime: Phoenix app in Cloudflare Container
         │  backend: Supabase PostgreSQL (via Ecto/postgrex)
         │  db:      source of truth for clients, instances, billing
         │  api:     internal REST API (/api/clients, /api/instances, etc.)
         │  ops:     CF API client for WFP provisioning
         │  domains: dash.fouria.io, dev-dash.fouria.io

dev-dash.fouria.io ──── (development/staging dashboard)

── Shared Platform Services ──

  ├─ AI Gateway (moltworker)
  │    → Unified AI provider routing (shared across tenants)
  │    → Native PII filtering — replaces the deprecated Outbound Worker (fouria-outbound)

  └─ Supabase PostgreSQL (shared, managed)
       → Source of truth: clients, partners, users, instances, plans, billing
       → Accessed via Ecto (Dashboard) and PostgREST (Dispatch Worker)

Component Connectivity

End-to-end component map — the control plane (lerma) and the data plane (Dispatch → Tenant → Sandbox) share the same Supabase PostgreSQL source of truth and have no direct HTTP dependency on each other:

                    ┌──────────────────────────────────────────────────────────┐
   CONTROL PLANE                       Supabase (PostgreSQL)                          DATA PLANE
                              clients · credit_pools · client_users ·
                               instances · billing · source of truth
                    └─────────▲─────────────────────────────────────┬──────────┘
                              │                                     │
      Ecto / postgrex         │       PostgREST (SUPABASE_URL       │
        (Dashboard)           │         + SERVICE_ROLE_KEY)         │
                              │                                     │
                  ┌───────────────────────┐         ┌────────────────────────────────┐
                  │   FourIA Dashboard    │         │        Dispatch Worker         │
                  │     (apps/lerma)      │         │    workers/fouria-dispatch     │
                  │    dash.fouria.io     │         │ *.fouria.io · KV · Access JWT  │
                  │  Phoenix + LiveView   │         │        role resolution         │
                  │     control plane     │         └───────────────┼────────────────┘
                  └───────────┼───────────┘                         │ DISPATCHER.get()
                              │ CF API (R2 buckets,                 │ "tenant-{slug}"
                              │ secrets, dispatch ns)               │ + X-FourIA-Role
                              ▼                                     │ + X-FourIA-Metadata
                           Cloudflare APIs                          ▼
                             (provisioning)         ┌────────────────────────────────┐
                                                    │         Tenant Worker          │
                                                    │         (apps/fouria)          │
                                                    └───────────────┼────────────────┘
                                                                    │ sandbox container

                                                    ┌────────────────────────────────┐
                                                    │       Sandbox Container        │
                                                    │       · OpenClaw Gateway       │
                                                    │        · moltlazy patch        │
                                                    │      · cf-unified-billing      │
                                                    └───────────────┼────────────────┘
                                                                    │ provider calls

                                                    ┌────────────────────────────────┐
                                                    │     Cloudflare AI Gateway      │
                                                    │       (moltworker) · PII       │
                                                    └───────────────┼────────────────┘

                                                              Upstream LLMs

Shared platform services: Cloudflare R2 (per-tenant backups + release bundles), Analytics Engine (fourai_cost_metrics), and the shared Cloudflare AI Gateway (moltworker).

Reserved hosts: The Dispatch Worker at *.fouria.io reserves the following hostnames (they are handled specially and are not valid tenant slugs):

Reserved hostnamePurposeZone
dash.fouria.ioFourIA Dashboard (production)fouria.io (bypass route)
dev-dash.fouria.ioFourIA Dashboard (development)fouria.io (bypass route)

Project Structure

workers/
├── fouria-dispatch/          # Dispatch Worker
│   ├── src/index.ts          # Wildcard dispatch, Supabase lookup, KV cache, cron fan-out
│   ├── src/auth.ts           # Cloudflare Access JWT verification
│   ├── src/supabase.ts       # PostgREST tenant + user-role resolution
│   ├── wrangler.jsonc        # DISPATCHER, TENANT_INDEX (KV), SUPABASE_* secrets
│   ├── package.json
│   └── tsconfig.json

apps/
├── lerma/         # FourIA Dashboard (Elixir/Phoenix, internal only)
│   ├── lib/lerma/
│   │   ├── repo.ex               # Ecto repo → Supabase PostgreSQL
│   │   ├── cloudflare/           # CF Workers API client (provisioning)
│   │   ├── provisioning/         # TenantProvisioner orchestrator
│   │   ├── clients/              # Client/user CRUD
│   │   ├── instances/            # Instance lifecycle
│   │   ├── billing/              # Billing context
│   │   └── dashboard/            # Dashboard accounts context
│   ├── lib/lerma_web/
│   │   ├── router.ex             # Routes (browser + API)
│   │   ├── plugs/cf_access_auth.ex # Cloudflare Access auth plug
│   │   └── live/                 # LiveView pages
│   ├── Dockerfile                # Container image (Phoenix app)
│   └── priv/repo/migrations/     # Supabase schema (Ecto migrations)

└── fouria/         # User Worker template
    ├── src/index.ts          # OpenClaw gateway proxy, auth, metrics
    ├── src/auth/              # JWT verification, RBAC
    ├── src/gateway/           # Sandbox container management
    ├── src/routes/            # API routes, admin UI, OTel forwarding
    ├── wrangler.jsonc         # 3 environments: local, staging, production
    ├── Dockerfile             # OpenClaw container image
    └── ...

Key Components

1. Dispatch Worker (workers/fouria-dispatch/)

The entry point for all platform tenant traffic at *.fouria.io. Extracts the tenant slug from the subdomain, resolves it via KV cache (60s TTL) with Supabase (PostgREST) fallback, validates Cloudflare Access JWT per-tenant, and dispatches to the correct User Worker.

Note: The Dispatch Worker only handles tenant routing on the *.fouria.io zone. Dashboard traffic at dash.fouria.io and dev-dash.fouria.io is served on the fouria.io zone via bypass worker routes — the Dispatch Worker does not intercept these hostnames.

Request Flow:

  1. Request arrives at {tenant}.fouria.io
  2. Dispatch Worker extracts tenant slug from hostname (e.g., tenant-arua.fouria.iotenant-arua)
  3. Checks KV cache (TENANT_INDEX) keyed by slug:{tenant} (60s TTL)
  4. On cache miss, queries Supabase via PostgREST (service-role key):
    • clientsid, slug, billing_plan, status
    • plus credit_pools (credits used/max), validated client_users (seat count), and instances (instance id/status)
  5. Requires a valid Cloudflare Access JWT; production validates that the token’s aud resolves to the requesting tenant
  6. Dispatches to User Worker (tenant-{slug}) via DISPATCHER.get() with plan-level CPU/subrequest limits
  7. Forwards X-FourIA-Role and X-FourIA-Metadata headers to the User Worker

Responsibilities:

  • Extract tenant slug from wildcard subdomain hostname
  • KV cache (TENANT_INDEX, 60s TTL) for tenant routing — warm reads, Supabase fallback
  • Direct Supabase queries (PostgREST via SUPABASE_URL + service-role key) — no dashboard HTTP API dependency
  • Per-tenant Cloudflare Access JWT verification (token aud resolved to the tenant)
  • Apply per-plan CPU and subrequest limits
  • Fan-out cron triggers to all active tenants every minute
  • Invalidate tenant cache via internal endpoint (/_invalidate/:slug)

Bindings:

NameTypePurpose
DISPATCHERdispatch_namespacefouria-tenants namespace
SUPABASE_URLsecret_textSupabase project URL (PostgREST endpoint)
SUPABASE_SERVICE_ROLE_KEYsecret_textService-role key (bypasses RLS for reads)
TENANT_INDEXkv_namespaceTenant slug → entry cache (60s TTL)
PLATFORM_DOMAINplain_textfouria.io

Staging uses STAGING_SUPABASE_URL / STAGING_SUPABASE_SERVICE_ROLE_KEY when present, falling back to the production credentials.

Auth model: Per-instance. Every tenant environment hostname has its own Cloudflare Access application and group ({slug}.fouria.io for production, {slug}-dev.fouria.io for dev). In production the token’s aud must resolve to the requested hostname — Paso4 ROOT operators are exempt (they may hold the platform wildcard *.fouria.io AUD or the shared Paso4 Root Users group) — and Dispatch resolves a client role only when the user is validated and linked to that exact instances.id. Staging continues to require the Paso4 platform audience (ROOT).

Routing gate: Before authentication, Dispatch checks the resolved tenant’s operational state (tenantRouteGate). The production environment is gated on clients.status (active/production); the dev environment is gated on the instance status instead, so a running dev instance behind a still-draft client is routable.

Invitation flow: Lerma stores new client users as invited with only a SHA-256 token hash and a seven-day expiry. The emailed link lets the recipient accept or reject the invitation. Invitation emails are delivered by a supervised background task (Lerma.Invitations) after the user row is committed, so a slow or failing SMTP relay never holds a database connection or blocks the dashboard; the delivery outcome is broadcast over PubSub and surfaced as a toast plus a per-user badge. Acceptance records a structured Cloudflare Access identity reference, changes the user to validated, and syncs their email only into the groups for accepted instance assignments. invited and rejected users are excluded from both Access groups and Dispatch role resolution. Existing Access applications are reconciled to a group-only allow policy during provisioning and user synchronization; everyone is never retained.

2. User Workers (apps/fouria/)

Each customer gets their own User Worker deployed to the fouria-tenants dispatch namespace. These are instantiated from the same codebase but with per-customer bindings.

User Workers receive tenant context and user identity from the Dispatch Worker via HTTP headers (X-FourIA-Role, X-FourIA-Metadata). They never access the platform database directly — all user/tenant resolution is handled by the Dispatch Worker before the request reaches the User Worker.

Per-Customer Bindings (provisioned by FourIA Dashboard):

BindingTypeExample
Sandboxdurable_object_namespacePer-tenant Sandbox class
BACKUP_BUCKETr2_bucketfouria-backup-arua
ANALYTICSanalytics_enginefouria_metrics_arua
BROWSERbrowserShared remote browser
MOLTBOT_GATEWAY_TOKENsecret_textAuto-generated per tenant
CLOUDFLARE_AI_GATEWAY_API_KEYsecret_textPlatform AI credential from Lerma
CLOUDFLARE_API_TOKENsecret_textREST transport alias of the platform AI credential
CF_AI_GATEWAY_GATEWAY_IDplain_textShared: "moltworker"
CF_ACCESS_AUDplain_textPer-tenant AUD tag

All platform data access goes through the Dispatch Worker (Supabase PostgREST) and Dashboard (Supabase + Ecto). User Workers never access the platform database directly — they receive billing context via X-FourIA-Metadata and role via X-FourIA-Role, both set by the Dispatch Worker.

3. FourIA Dashboard (apps/lerma/)

Internal management application for Paso4 managers only. Runs as a standalone Phoenix app inside a Cloudflare Container (Durable Object PHOENIX_CONTAINER) at dash.fouria.io (on the fouria.io zone, with a bypass worker route). A separate dev instance is served at dev-dash.fouria.io.

The Dashboard is a pure control panel — it has no direct dependency on fouria or the Dispatch Worker. It manages platform data (clients, partners, users, instances, billing) through its Supabase backend and interacts with Cloudflare APIs for provisioning.

Database architecture:

EnvironmentBackendDatabase
dev/testEcto (PostgreSQL)DATABASE_URL (Docker compose)
productionEcto (PostgreSQL)DATABASE_URL (managed Supabase)

In production, the Dashboard connects to Supabase PostgreSQL via postgrex using the DATABASE_URL environment variable (direct PostgreSQL wire protocol).

Communication model: The Dashboard communicates with Supabase PostgreSQL via Ecto — no public HTTP API is needed for platform data. Cloudflare API calls (WFP provisioning, R2 management, secrets) use a CF API client with admin-level credentials.

Authorization model: Two role levels control API access:

RoleScopeCapabilities
PASO4 rootGlobalFull CRUD on all entities, create Partner roots
Partner rootPartner-scopedCRUD limited to associated clients and their data

Users

Management of FourIA user entities (logical users linked to a Cloudflare Zero Trust Access identity and role).

GET    /user              → List all users (paginated)
GET    /user/:user_id     → Get single user details

PASO4 root & Partner root (scoped):

POST   /user              → Create a user entity (linked to Zero Trust Access ID + role)

Partners

A partner entity represents an organization that can own multiple clients, each with one instance of the service.

GET    /partner/:partner_id/style  → Get partner custom branding (views, logos)
PUT    /partner/:partner_id/style  → Update partner custom branding

PASO4 root only:

GET    /partner            → List all partners (paginated)
GET    /partner/:id        → Get partner details
POST   /partner            → Create a new partner
PUT    /partner/:id        → Update partner
DELETE /partner/:id        → Delete partner

Clients

Client entities belong to a partner and each has one associated instance. A client can also be internal (no partner association).

PASO4 root & Partner root (scoped):

GET    /client                          → List all clients (paginated, scoped by role)
GET    /client/:client_id               → Get client details from Supabase
POST   /client/new                      → Create a new client
PUT    /client/update                   → Update client information

PASO4 root only:

GET    /client/:client_id/partner       → Get associated partner (null if internal client)

Instances

Each client has exactly one instance (User Worker deployed via WFP dispatch namespace).

PASO4 root & Partner root (scoped):

POST   /client/instance/create                              → Create a new instance via WFP binding
                                                                    (limited to 1 per client)
PUT    /client/instance/:instance_id/worker-plan/:plan_type  → Manually change instance worker plan
GET    /client/instance/:instance_id/health                  → Instance health status

Health endpoint returns instance observability data via Worker bindings:

CheckSourceDescription
Gateway statusSandbox DO / process probeWhether the OpenClaw gateway process is running (up/down)
Container heartbeatAnalytics Engine (container-active)Most recent heartbeat timestamp
OTel health(to be defined)Additional health signals exposed via OpenTelemetry integration

The OTel integration will be extended to surface further instance state metrics (memory pressure, CPU throttle, R2 mount status, WebSocket connectivity). These signals will be defined during OTEL implementation.


Client Users

Manages email-verified invitations and instance-scoped Cloudflare Zero Trust Access assignments. User responses include status, validated_at, cf_access_identity, and instance_ids.

PASO4 root & Partner root (scoped):

GET    /api/clients/:client_id/users         → List invited, validated, and rejected users
POST   /api/clients/:client_id/users         → Create an invitation for selected instance_ids (email sent asynchronously)
PUT    /api/clients/:client_id/users/:id     → Update name or role
DELETE /api/clients/:client_id/users/:id     → Remove a user and revoke assigned access

Invite recipient (token-authenticated):

GET    /invitations/:token                   → Review and accept or reject an invitation

Analytics

Cost and usage analytics drawn from Cloudflare Analytics Engine. See cost-observability.md for the full event schema and example queries.

PASO4 root & Partner root (scoped):

GET    /analytics/global                    → Executive summary dashboard
                                                 PASO4 root: segmented by partners
                                                 Partner root: filtered to partner's clients only

Analytics Engine dataset: fourai_cost_metrics — written by User Workers via the ANALYTICS binding. The dashboard fetches aggregated queries from this dataset to render cost breakdowns, container uptime, R2 bandwidth, and WebSocket session metrics.


Provisioning Flow (for a new customer):

1. POST /partner → Create Partner (if not internal)
2. PUT  /partner/:id/style → Configure partner branding
3. POST /client/new → Create Client (slug, plan, partner_id)
4. POST /client/instance/create → Provision User Worker via WFP:
   a. Create R2: "fouria-backup-{slug}" (via CF API)
   b. Auto-generate gateway token
   c. Deploy User Worker to dispatch namespace (via WFP API)
      → multipart upload: worker bundle + binding metadata
   d. Set secrets (via WFP secrets API): MOLTBOT_GATEWAY_TOKEN, CDP_SECRET, R2 keys, etc.
   e. Set tags: customer_id:{id}, plan:{plan}, env:production
   f. Set CF_ACCESS_AUD: Cloudflare Access audience tag (per-tenant JWT auth)
5. POST /user → Create user entities for Zero Trust Access identities
6. POST /client/:client_id/user/:user_id → Link users to client with roles
7. Invalidate dispatch cache: GET /_invalidate/{slug} on Dispatch Worker

Environments

Environment Matrix

NameDomainDatabaseDispatch NamespaceAI Gateway
locallocalhostSupabase (docker-compose)N/A (test pool)Direct API keys
stagingfouria.ioSupabase (managed)fouria-tenants-stagingShared (moltworker)
productionfouria.ioSupabase (managed)fouria-tenantsShared (moltworker)

Staging vs Production

Staging shares the AI Gateway with production (same gateway ID) but has its own Supabase database and dispatch namespace. This allows feature testing with real provider models without risk of touching production data.

Secret Strategy:

  • Staging: Reuses production AI Gateway key (shared), separate R2 keys
  • Production: Full secrets per customer via WFP secrets API

AI Gateway Reuse (Gateway Pool)

Cloudflare limits AI Gateways to 10 (Free) / 20 (Paid) per account. To avoid exhausting this quota with one gateway per tenant environment, lerma manages a reusable AI Gateway pool (DB table ai_gateways, dashboard at /dashboard/ai-gateways, API under /api/ai-gateways).

How selection works

At provisioning time (TenantProvisioner:ai_gateway step → Lerma.AiGateways.resolve/3):

  1. Explicit override — an operator-provided ai_gateway_id / ai_gateway_pool_id (or the binding of an existing error-state instance being re-provisioned) wins.
  2. Least-loaded pooled gateway — the enabled pool entry whose environment matches (or is unrestricted) and that still has capacity (used < max_instances), sorted by fewest bound instances.
  3. Dedicated fallback — if no pool entry has capacity and AI_GATEWAY_CREATE_DEDICATED=true (the default), a dedicated per-tenant gateway fouria-{slug}[-dev] is created and owned by the tenant (deleted on rollback/deprovisioning). When false, provisioning fails with no_ai_gateway_capacity instead.

Shared pool gateways are never deleted by the provisioner — the tenant only stores ai_gateway_id + ai_gateway_pool_id on its instance record and binds its worker to that gateway.

Manual distribution control

Each pool entry has a max_instances ceiling — the maximum number of tenant instances that may bind it. An operator edits max_instances per gateway (dashboard edit form or PUT /api/ai-gateways/:id) to steer where new tenants land based on usage, e.g. route high-usage customers onto a dedicated-capacity gateway and everyone else onto a shared pool.

Tenants can be rebalanced at any time by reassigning an instance to another gateway (POST /api/instances/:id/assign-gateway, or the “Change AI Gateway” action on /dashboard/instances/:id). This re-deploys the worker so its CF_AI_GATEWAY_GATEWAY_ID binding points at the new gateway.

What it creates per tenant

ResourceDedicated (legacy)Pooled (reuse)
Cloudflare AI Gateway1 per environment, fouria-{slug}[-dev]0 — reuses an existing pool gateway
ai_gateways row01 (shared across many tenants)
Instance ai_gateway_id / ai_gateway_pool_idai_gateway_id setboth set

Routing & DNS

DNS Configuration

# Tenant routing — fouria.io zone
Type: A
Name: *.fouria.io
Target: Dispatch Worker (Cloudflare-managed)
Proxied: Yes

# Dashboard — fouria.io zone (bypass worker routes — no worker assigned)
Type: CNAME (or Worker route)
Name: dash.fouria.io
Target: FourIA Dashboard (Cloudflare-managed)
Proxied: Yes

Type: CNAME (or Worker route)
Name: dev-dash.fouria.io
Target: FourIA Dashboard dev (Cloudflare-managed)
Proxied: Yes

Subdomain-Based Routing (Dispatch Worker)

The Dispatch Worker is bound to the wildcard *.fouria.io route. The tenant is identified by the subdomain, not by the URL path:

SubdomainPurposeAuth
{tenant}.fouria.ioTenant traffic → User WorkerCloudflare Access JWT (aud resolved to tenant)
dash.fouria.ioInternal management → Dashboard (fouria.io zone, bypass)Cloudflare Access JWT (PASO4/Partner root)
dev-dash.fouria.ioDev dashboard (fouria.io zone, bypass)Cloudflare Access JWT (dev audience)

The Dispatch Worker only handles subdomains of *.fouria.io. Dashboard hostnames on fouria.io are bypassed — they are not dispatched by the Dispatch Worker via wildcard matching.

// Dispatch Worker routing pattern (tenant traffic only)
const slug = extractSlug(hostname, PLATFORM_DOMAIN);
// tenant-a.fouria.io → "tenant-a"

// Step: resolveTenant(slug) → DISPATCHER.get(workerName).fetch()

Supabase Lookup + KV Cache

Request flow:
  Browser → {tenant}.fouria.io
  → Dispatch Worker extracts slug from hostname
  → Check KV cache (TENANT_INDEX, key: slug:{tenant})
    ├─ HIT (60s TTL) → dispatch with cached worker name + plan limits
    └─ MISS → query Supabase via PostgREST (service-role key):
         clients:      id, slug, billing_plan, status
         credit_pools: lifetime_consumed, lifetime_purchased
         client_users: seat count for the client
         instances:    status (frozen/stopped guard)
       → Resolve user role (production, non-platform aud):
         client_users: role WHERE client_id = ? AND email = ?
       → Cache result in KV (60s TTL)
       → Forward X-FourIA-Role + X-FourIA-Metadata headers
       → DISPATCHER.get(tenant-{slug}) → User Worker

Data Architecture

Shared Supabase PostgreSQL Database

Both the Dispatch Worker and the Dashboard access the same Supabase PostgreSQL database. The Dashboard owns the schema (Ecto migrations), while the Dispatch Worker reads from it at runtime via the PostgREST API (SUPABASE_URL + service-role key).

Tables used by Dispatch Worker (read-only):

TablePurpose
clientsid, slug, billing_plan, status — tenant identity + plan
credit_poolslifetime_consumed, lifetime_purchased — credit usage for dispatch
client_usersValidated seat count + RBAC role (ADMIN, BASE_USER)
client_user_instancesRequires the user to be assigned to the requested instance
instancesid, status — scope users and reject frozen/stopped instances

Tables used by Dashboard (read/write):

TablePurpose
clientsTenant organizations
client_usersTenant invitations, verified identity, status, and role
client_user_instancesAccepted user-to-instance access assignments
instancesCloudflare Worker containers per tenant
dashboard_usersPlatform administrators (ROOT role)
client_subdivisionsPer-tenant agent namespaces
client_user_subdivisionsUser-to-subdivision membership
invoicesBilling periods per tenant
invoice_line_itemsIndividual billing line items
analytics_eventsOTLP-style analytics event storage

All inter-service data access goes through Supabase (PostgREST for the Dispatch Worker, Ecto for the Dashboard) plus a KV cache layer. The FourIA platform API is not exposed publicly — tenant routing is resolved by the Dispatch Worker, metrics are written directly to Analytics Engine, and the Dashboard reads/writes platform data through its Supabase backend.

User Resolution Flow

User Workers receive role and billing context from the Dispatch Worker via HTTP headers:

HeaderSourcePurpose
X-FourIA-RoleDispatch WorkerUser’s role within the tenant (ROOT, ADMIN, BASE_USER)
X-FourIA-MetadataDispatch Workerbase64-encoded plan/billing metadata (plan, seats, credits, etc.)

The Dispatch Worker resolves user identity by querying client_users and dashboard_users tables during request routing. User Workers read these headers in their resolveCustomerRole middleware — they never make external API calls for user resolution.


PII Filtering (Cloudflare AI Gateway)

PII filtering for all AI provider traffic is handled natively by Cloudflare AI Gateway. No separate outbound worker or Presidio container is required.

User Worker → fetch() → AI Gateway (native PII filtering) → AI provider → response returned

The previously planned Outbound Worker + Presidio pipeline has been replaced by Cloudflare AI Gateway’s built-in PII filtering capabilities.


OTel & Metrics Flow

All telemetry is written directly to Cloudflare Analytics Engine via the ANALYTICS binding — no HTTP API involved:

OpenClaw Gateway (container) → generates OTel metrics
  → POST /api/otel/v1/metrics (User Worker receives)
  → User Worker parses and accepts metrics (status: 200)
  → Metrics are written to ANALYTICS.writeDataPoint() for AI queries, container activity, etc.

The User Worker’s /api/otel/v1/metrics endpoint accepts JSON OTLP payloads and acknowledges receipt. Actual cost/usage metrics are tracked independently through separate event types in the ANALYTICS binding (see src/metrics.ts).


Security Model

LayerMechanism
DNSCloudflare proxied (orange cloud)
AuthCloudflare Zero Trust Access — per-tenant JWT (aud resolved to tenant)
RoutingTenant isolation via WFP dispatch namespace
DataPer-customer R2 buckets (no cross-tenant access)
PlatformSupabase PostgreSQL — service-role PostgREST (Dispatch) + Ecto (Dashboard)
DashboardSupabase (shared) — internal HTTP API, not publicly exposed
EgressCloudflare AI Gateway — native PII filtering, only approved hosts
PIICloudflare AI Gateway native PII filtering (replaces Presidio anonymization)
SecretsWFP secrets API (no secrets stored in dashboard DB)
Cache invalidationInternal endpoint /_invalidate/:slug (bearer-token protected)

Plan Limits (infrastructure-level, not client billing)

Note: These are internal infrastructure limits mapped to Cloudflare resource allocations per deployment. They are NOT client-facing billing tiers. v1.0-beta uses a single flat-fee plan with credit-based billing. Tiered infrastructure limits exist in code for future multi-plan support.

TierCPU (ms)SubrequestsContainerR2 Storage
Free10,00010lite2 GB
Basic30,00050standard-18 GB
Pro50,000100standard-212 GB
Enterprise90,0001,000standard-3+Custom

Glossary

TermDefinition
WFPCloudflare Workers for Platforms
Dispatch NamespaceContainer holding all User Workers
User WorkerPer-customer Worker (tenant-{slug})
Dispatch WorkerRouting + auth layer (fouria-dispatch)
Outbound Worker(Deprecated) Egress + PII layer (fouria-outbound) — replaced by CF AI Gateway
PartnerOrganization owning multiple clients
ClientA customer with one instance of the service
InstanceA deployed User Worker + Sandbox container
PASO4 rootGlobal platform admin role
Partner rootPartner-scoped admin role
Presidio(Deprecated) Microsoft PII detection/anonymization service
FourIA DashboardInternal management app (Phoenix in Cloudflare Container)
KV Cache (TENANT_INDEX)KV namespace caching tenant slug → entry (60s TTL)
SupabaseManaged PostgreSQL + PostgREST API — shared source of truth
SUPABASE_URLSecret — Supabase project URL used by the Dispatch Worker (PostgREST)
SUPABASE_SERVICE_ROLE_KEYSecret — service-role key that bypasses RLS for tenant lookup
ANALYTICSAnalytics Engine binding (fourai_cost_metrics)