Skip to content

Architecture Overview

In Falcone is a multi-tenant BaaS assembled from a small set of cooperating services in front of standard data backends. The design has three throughlines:

  1. One gateway, two privilege domains. Everything enters through APISIX, which classifies a request by its credential and routes it to either the control plane (structural_admin) or the executor (data_access).
  2. Identity derives the tenant — never the client. The tenant/workspace come from a verified credential (API key → JWT → gateway-injected headers, in that order). Client-supplied x-tenant-id is never trusted. Invalid credentials fail closed.
  3. Isolation is enforced at the data layer, not just at the edge. PostgreSQL Row-Level Security with a non-BYPASSRLS role, FerretDB/DocumentDB adapter-injected predicates, and tenant-scoped realtime matching mean a routing mistake cannot leak data.

Request lifecycle

                 ┌──────────────────────────────────────────────────────┐
   Browser /     │                  APISIX  (API gateway)                │
   App / CLI ───▶│  • classify credential:  apikey: flc_…  | Bearer JWT  │
                 │  • enforce scope (structural_admin | data_access)     │
                 │  • rate-limit per key  • inject verified identity hdrs │
                 └───────────────┬───────────────────────┬──────────────┘
                                 │ structural_admin       │ data_access
                                 ▼                        ▼
                    ┌────────────────────┐   ┌──────────────────────────────┐
                    │   Control Plane    │   │          Executor            │
                    │  tenants/workspaces│   │  resolveIdentity()           │
                    │  schemas/functions │   │  build*Plan() → run on driver│
                    │  api-keys/quotas   │   └───┬─────┬─────┬─────┬─────┬───┘
                    │  /v1/flows /v1/mcp │       │     │     │     │     │
                    └─────────┬──────────┘       │     │     │     │     │
                              │              Postgres FerretDB Kafka SeaweedFS Funcs
                              ▼                  │     │     │     │     │
                    ┌────────────────────┐      ▼     ▼     ▼     ▼     ▼
                    │  Platform metadata │   (per-workspace data backends, tenant-scoped)
                    │  (tenants, plans…) │
                    └────────────────────┘

         Identity: Keycloak (OIDC)  ·  Secrets: OpenBao + External Secrets  ·  Metrics: Prometheus
  • A data-plane call (e.g. GET /v1/collections/todos/documents with apikey: flc_anon_…) is routed by the gateway to the executor. The executor verifies the key, resolves the tenant/workspace and DB role from it, builds an adapter query plan, and runs it against the workspace's real database — under RLS.
  • A structural-admin call (e.g. POST /v1/tenants with a Bearer admin token) is routed to the control plane, which mutates platform metadata and reconciles downstream resources (gateway routes, identity realm, provisioning).

The two privilege domains

The public route catalog (deploy/gateway-config/public-route-catalog.json) tags every route with a privilege_domain. The gateway's scope-enforcement plugin checks the caller's domain before forwarding:

DomainExamplesTypical caller
structural_adminPOST /v1/tenants, POST /v1/workspaces, POST /v1/schemas, POST /v1/functions, POST /v1/api-keys, PUT /v1/quotas, POST /v1/services/configureOperators, tenant admins (Bearer JWT)
data_access…/collections/{name}/documents, …/collections/{name}/query, …/objects/{bucket}/{key}, …/functions/{id}/invoke, …/events/publish, …/events/subscribe, …/analytics/queryApplications (API keys / JWT)

Identity resolution (precedence)

The executor's resolveIdentity() (apps/control-plane-executor/src/runtime/server.mjs) applies a strict order. Each authoritative credential derives the tenant from itself:

1. API key (apikey: flc_… | Authorization: ApiKey/Bearer flc_… | x-api-key)
      → tenant, workspace, and DB role come from the verified key
      → invalid key  ⇒  401  (never falls back to headers)

2. Bearer JWT (when a verifier is configured)
      → identity from verified token claims
      → invalid token ⇒ 401

3. Gateway-injected headers (x-tenant-id / x-workspace-id / x-auth-subject / x-pg-role)
      → only trusted when no credential was presented; the gateway validated the
        token upstream and stripped any client-supplied context headers

This precedence is the reason a client cannot spoof a tenant: the only way to set x-tenant-id is through the gateway, after it has validated a token — and any presented flc_… key or Bearer JWT overrides headers entirely.

Data isolation strategy

In Falcone uses a shared-database, tenant-scoped model with defense in depth:

  • PostgreSQL — RLS policies on tenant-scoped tables, plus a dedicated non-BYPASSRLS application role (falcone_app, with anon/service variants). The executor runs queries via SET LOCAL ROLE so even a buggy query is constrained by RLS. (A superuser would bypass RLS — which is exactly why the app role is not a superuser.)
  • FerretDB + DocumentDB (document store) — the data adapter injects a tenantId predicate into every read and stamps it on every write (the authoritative isolation boundary; per-database role scoping is NOT enforced at the engine — ADR-14); the realtime pipeline (Postgres logical replication) filters consumer-side on the verified tenant.
  • Every layer — caches, queues, object keys, events and logs are keyed by tenant so isolation holds beyond the database.

See Security & Auth for the full model.

AI-native layer (Flows & MCP) — Preview

Two core capabilities make a tenant's backend consumable by AI agents; both are served by the control plane in a fresh install:

   Control Plane
   ├─ /v1/flows/workspaces/{ws}/…  ──▶  Temporal  ◀──poll──  workflow-worker
   │  (durable workflow definitions,                          (DslInterpreterWorkflow +
   │   versions, executions)                                   first-party activity catalog)
   └─ /v1/mcp/workspaces/{ws}/servers/…  ──▶  mcp-engine  ──▶  Knative ksvc (per-tenant MCP server)
      (create · curate · publish · call · audit)              served over Streamable HTTP, OAuth 2.1
  • Flows — a durable workflow engine on Temporal (chart components temporal + workflowWorker, wired by the chart through TEMPORAL_ADDRESS). Tenants author a YAML DSL; the control plane stores immutable versions and runs each execution as a Temporal workflow. Isolation is by server-generated workflow ids ({tenantId}:{workspaceId}:{flowId}:{runUuid}) prefix-checked on every command.
  • MCP server hosting — the control-plane runtime serves the MCP management API through mcp-engine as part of the core install. Instant MCP and the official server are live (Preview); custom (BYO-image) hosting and workflows-as-tools are Experimental; per-tenant OAuth 2.1 + per-tool scopes via Keycloak; MCP-server pods are internal-only.

Where to go next

Released under the MIT License.