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, MongoDB 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 Mongo Kafka MinIO Funcs
                              ▼                  │     │     │     │     │
                    ┌────────────────────┐      ▼     ▼     ▼     ▼     ▼
                    │  Platform metadata │   (per-workspace data backends, tenant-scoped)
                    │  (tenants, plans…) │
                    └────────────────────┘

         Identity: Keycloak (OIDC)  ·  Secrets: Vault + 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 (services/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/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.)
  • MongoDB — the data adapter injects a tenantId predicate into every read and stamps it on every write; the realtime change-stream pipeline $matches 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 capabilities make a tenant's backend consumable by AI agents, both served by the control plane and both off by default:

   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, enabled by 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 (mcp-engine, enabled by MCP_ENABLED). 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.