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 hostname | Purpose | Zone |
|---|---|---|
dash.fouria.io | FourIA Dashboard (production) | fouria.io (bypass route) |
dev-dash.fouria.io | FourIA 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.iozone. Dashboard traffic atdash.fouria.ioanddev-dash.fouria.iois served on the fouria.io zone via bypass worker routes — the Dispatch Worker does not intercept these hostnames.
Request Flow:
- Request arrives at
{tenant}.fouria.io - Dispatch Worker extracts tenant slug from hostname (e.g.,
tenant-arua.fouria.io→tenant-arua) - Checks KV cache (
TENANT_INDEX) keyed byslug:{tenant}(60s TTL) - On cache miss, queries Supabase via PostgREST (service-role key):
clients—id,slug,billing_plan,status- plus
credit_pools(credits used/max), validatedclient_users(seat count), andinstances(instance id/status)
- Requires a valid Cloudflare Access JWT; production validates that the token’s
audresolves to the requesting tenant - Dispatches to User Worker (
tenant-{slug}) viaDISPATCHER.get()with plan-level CPU/subrequest limits - Forwards
X-FourIA-RoleandX-FourIA-Metadataheaders 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
audresolved 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:
| Name | Type | Purpose |
|---|---|---|
DISPATCHER | dispatch_namespace | fouria-tenants namespace |
SUPABASE_URL | secret_text | Supabase project URL (PostgREST endpoint) |
SUPABASE_SERVICE_ROLE_KEY | secret_text | Service-role key (bypasses RLS for reads) |
TENANT_INDEX | kv_namespace | Tenant slug → entry cache (60s TTL) |
PLATFORM_DOMAIN | plain_text | fouria.io |
Staging uses
STAGING_SUPABASE_URL/STAGING_SUPABASE_SERVICE_ROLE_KEYwhen 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):
| Binding | Type | Example |
|---|---|---|
Sandbox | durable_object_namespace | Per-tenant Sandbox class |
BACKUP_BUCKET | r2_bucket | fouria-backup-arua |
ANALYTICS | analytics_engine | fouria_metrics_arua |
BROWSER | browser | Shared remote browser |
MOLTBOT_GATEWAY_TOKEN | secret_text | Auto-generated per tenant |
CLOUDFLARE_AI_GATEWAY_API_KEY | secret_text | Platform AI credential from Lerma |
CLOUDFLARE_API_TOKEN | secret_text | REST transport alias of the platform AI credential |
CF_AI_GATEWAY_GATEWAY_ID | plain_text | Shared: "moltworker" |
CF_ACCESS_AUD | plain_text | Per-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:
| Environment | Backend | Database |
|---|---|---|
| dev/test | Ecto (PostgreSQL) | DATABASE_URL (Docker compose) |
| production | Ecto (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:
| Role | Scope | Capabilities |
|---|---|---|
| PASO4 root | Global | Full CRUD on all entities, create Partner roots |
| Partner root | Partner-scoped | CRUD 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:
| Check | Source | Description |
|---|---|---|
| Gateway status | Sandbox DO / process probe | Whether the OpenClaw gateway process is running (up/down) |
| Container heartbeat | Analytics 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
| Name | Domain | Database | Dispatch Namespace | AI Gateway |
|---|---|---|---|---|
| local | localhost | Supabase (docker-compose) | N/A (test pool) | Direct API keys |
| staging | fouria.io | Supabase (managed) | fouria-tenants-staging | Shared (moltworker) |
| production | fouria.io | Supabase (managed) | fouria-tenants | Shared (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):
- 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. - Least-loaded pooled gateway — the enabled pool entry whose
environmentmatches (or is unrestricted) and that still has capacity (used < max_instances), sorted by fewest bound instances. - Dedicated fallback — if no pool entry has capacity and
AI_GATEWAY_CREATE_DEDICATED=true(the default), a dedicated per-tenant gatewayfouria-{slug}[-dev]is created and owned by the tenant (deleted on rollback/deprovisioning). Whenfalse, provisioning fails withno_ai_gateway_capacityinstead.
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
| Resource | Dedicated (legacy) | Pooled (reuse) |
|---|---|---|
| Cloudflare AI Gateway | 1 per environment, fouria-{slug}[-dev] | 0 — reuses an existing pool gateway |
ai_gateways row | 0 | 1 (shared across many tenants) |
Instance ai_gateway_id / ai_gateway_pool_id | ai_gateway_id set | both 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: YesSubdomain-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:
| Subdomain | Purpose | Auth |
|---|---|---|
{tenant}.fouria.io | Tenant traffic → User Worker | Cloudflare Access JWT (aud resolved to tenant) |
dash.fouria.io | Internal management → Dashboard (fouria.io zone, bypass) | Cloudflare Access JWT (PASO4/Partner root) |
dev-dash.fouria.io | Dev 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):
| Table | Purpose |
|---|---|
clients | id, slug, billing_plan, status — tenant identity + plan |
credit_pools | lifetime_consumed, lifetime_purchased — credit usage for dispatch |
client_users | Validated seat count + RBAC role (ADMIN, BASE_USER) |
client_user_instances | Requires the user to be assigned to the requested instance |
instances | id, status — scope users and reject frozen/stopped instances |
Tables used by Dashboard (read/write):
| Table | Purpose |
|---|---|
clients | Tenant organizations |
client_users | Tenant invitations, verified identity, status, and role |
client_user_instances | Accepted user-to-instance access assignments |
instances | Cloudflare Worker containers per tenant |
dashboard_users | Platform administrators (ROOT role) |
client_subdivisions | Per-tenant agent namespaces |
client_user_subdivisions | User-to-subdivision membership |
invoices | Billing periods per tenant |
invoice_line_items | Individual billing line items |
analytics_events | OTLP-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:
| Header | Source | Purpose |
|---|---|---|
X-FourIA-Role | Dispatch Worker | User’s role within the tenant (ROOT, ADMIN, BASE_USER) |
X-FourIA-Metadata | Dispatch Worker | base64-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
| Layer | Mechanism |
|---|---|
| DNS | Cloudflare proxied (orange cloud) |
| Auth | Cloudflare Zero Trust Access — per-tenant JWT (aud resolved to tenant) |
| Routing | Tenant isolation via WFP dispatch namespace |
| Data | Per-customer R2 buckets (no cross-tenant access) |
| Platform | Supabase PostgreSQL — service-role PostgREST (Dispatch) + Ecto (Dashboard) |
| Dashboard | Supabase (shared) — internal HTTP API, not publicly exposed |
| Egress | Cloudflare AI Gateway — native PII filtering, only approved hosts |
| PII | Cloudflare AI Gateway native PII filtering (replaces Presidio anonymization) |
| Secrets | WFP secrets API (no secrets stored in dashboard DB) |
| Cache invalidation | Internal 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.
| Tier | CPU (ms) | Subrequests | Container | R2 Storage |
|---|---|---|---|---|
| Free | 10,000 | 10 | lite | 2 GB |
| Basic | 30,000 | 50 | standard-1 | 8 GB |
| Pro | 50,000 | 100 | standard-2 | 12 GB |
| Enterprise | 90,000 | 1,000 | standard-3+ | Custom |
Glossary
| Term | Definition |
|---|---|
| WFP | Cloudflare Workers for Platforms |
| Dispatch Namespace | Container holding all User Workers |
| User Worker | Per-customer Worker (tenant-{slug}) |
| Dispatch Worker | Routing + auth layer (fouria-dispatch) |
| Outbound Worker | (Deprecated) Egress + PII layer (fouria-outbound) — replaced by CF AI Gateway |
| Partner | Organization owning multiple clients |
| Client | A customer with one instance of the service |
| Instance | A deployed User Worker + Sandbox container |
| PASO4 root | Global platform admin role |
| Partner root | Partner-scoped admin role |
| Presidio | (Deprecated) Microsoft PII detection/anonymization service |
| FourIA Dashboard | Internal management app (Phoenix in Cloudflare Container) |
| KV Cache (TENANT_INDEX) | KV namespace caching tenant slug → entry (60s TTL) |
| Supabase | Managed PostgreSQL + PostgREST API — shared source of truth |
| SUPABASE_URL | Secret — Supabase project URL used by the Dispatch Worker (PostgREST) |
| SUPABASE_SERVICE_ROLE_KEY | Secret — service-role key that bypasses RLS for tenant lookup |
| ANALYTICS | Analytics Engine binding (fourai_cost_metrics) |