Skip to content

Architecture Decision Records

The significant architectural decisions visible in the codebase, captured as short records. Each states the decision, why, and where it shows up in code.

ADR-1 — Shared database with Row-Level Security for tenant isolation

Decision. Use a shared database, tenant-scoped model. In PostgreSQL, enforce isolation with Row-Level Security plus a dedicated non-BYPASSRLS application role (falcone_app, with anon/service variants); in MongoDB, enforce it with an adapter-injected tenantId predicate.

Why. RLS makes isolation a property of the database, not just the application — a forgotten WHERE tenant_id = … is still constrained. Using a non-superuser role is essential because RLS does not apply to superusers/BYPASSRLS roles.

Evidence. docs/reference/postgresql/tenant-isolation-baseline.sql; executor SET LOCAL ROLE; packages/adapters/src/*.

ADR-2 — Credential-derived tenant; fail closed

Decision. The tenant/workspace are derived from a verified credential in strict precedence (API key → Bearer JWT → gateway-injected headers). A presented-but-invalid credential returns 401 and never falls back to client-supplied headers.

Why. Trusting a client-supplied x-tenant-id is a spoofing vector (a real bug, since fixed). Deriving identity from the credential makes spoofing impossible by construction.

Evidence. apps/control-plane-executor/src/runtime/server.mjs::resolveIdentity.

ADR-3 — Single gateway, two privilege domains

Decision. Front the whole platform with APISIX and split routes into structural_admin and data_access, enforced by a scope plugin. Route anon/service traffic by the apikey header.

Why. One ingress simplifies clients and TLS; domain separation keeps the management plane unreachable from data-plane credentials.

Evidence. deploy/gateway-config/public-route-catalog.json; scope-enforcement plugin + specs.

ADR-4 — Executor over adapter-built plans

Decision. Separate plan building (adapters, pure functions) from plan execution (a dependency-light executor running real drivers).

Why. Plan builders are testable without a database and concentrate the tenant predicate in one place; the executor stays small and driver-focused.

Evidence. packages/adapters/src/* (build*Plan) → apps/control-plane-executor/src/runtime/*-executor.mjs.

ADR-5 — Supabase-style anon/service API keys

Decision. Offer flc_anon_… (read-mostly, browser-safe, RLS-bound) and flc_service_… (server-side, elevated) keys, transported via the apikey header (and ?apikey= for SSE).

Why. Lets frontends talk to the platform directly without a backend, while keeping privileged access server-side. Anon keys bind to a constrained DB role so RLS still applies.

Evidence. api-keys.mjs; gateway vars on ^flc_.

ADR-6 — Realtime over SSE, tenant-scoped at the source

Decision. Deliver realtime via Server-Sent Events (no WebSocket dependency), sourced from the document store's Postgres logical-replication CDC (pgoutput on the DocumentDB engine; see ADR-14) and PostgreSQL trigger CDC, with tenant matching done inside the pipeline/channel.

Why. SSE is simple and proxy-friendly; matching the tenant at the source guarantees a subscriber cannot receive another tenant's changes even if routing is wrong. EventSource can't set headers, so SSE accepts ?apikey=.

Evidence. realtime-executor.mjs, postgres-realtime-executor.mjs.

ADR-7 — Per-key rate limiting with explicit policy

Decision. Rate-limit with APISIX limit-count keyed by var_combination on $http_apikey (per-key buckets), with a choice of local (node-local) or redis (globally exact) policy.

Why. A plain var key rate-limits globally across keys (a real bug, caught by live testing). var_combination isolates each key; the policy choice trades exactness for a Redis dependency.

Evidence. scripts/lib/gateway-policy.mjs; gateway route config.

ADR-8 — Umbrella Helm chart with layered values

Decision. Package the core platform as one umbrella chart with unconditional service dependencies, configured by layered values (environment → customer → platform → airgap → local) and sizing profiles (all-in-one / standard / ha).

Why. One artifact serves dev, Kubernetes, OpenShift and air-gapped installs; layering keeps environment/customer/platform concerns separable and reviewable.

Evidence. ../falcone-charts/charts/in-falcone/Chart.yaml, values/, templates/NOTES.txt.

ADR-9 — Secrets via OpenBao + External Secrets Operator

Decision. Source secrets from OpenBao through the External Secrets Operator; reference secret names in the chart, never values. (Migrated from HashiCorp Vault to OpenBao, a drop-in fork — PR #720.)

Why. Keeps secret material out of git and values files; integrates with existing secret stores.

Evidence. chart eso/openbao sections; secretKeyRef mounts.

ADR-10 — Soft-delete lifecycle for cascading cleanup

Decision. Govern entities with a draft → active → suspended → soft_deleted lifecycle.

Why. Soft deletion lets the platform deprovision tenants/workspaces and cascade resource cleanup while preserving audit history and avoiding orphaned cross-tenant data.

Evidence. packages/internal-contracts/src/domain-model.json (lifecycle_transitions).

ADR-11 — Temporal for the durable workflow (flows) engine

Decision. Adopt Temporal as the durable execution engine for the flows capability, using the TypeScript SDK (@temporalio/{worker,workflow,activity,client}). Flows are authored as a YAML DSL interpreted by a single generic interpreter workflow; the parsed definition is passed as workflow input (definition-passing strategy), not loaded per-replay. Branch conditions are evaluated by the CEL expression engine (cel-js). Tenancy uses a shared namespace with a tenantId custom search attribute (not namespace-per-tenant). Run-history visibility is served by Temporal's PostgreSQL SQL advanced-visibility store — no Elasticsearch. Temporal itself is internal-only, and its Web UI is operator-only (never exposed to tenants).

Why.

  • Adoption rationale. Temporal gives durable execution (workflow state survives process/worker crashes via event-sourced history replay), millisecond task dispatch via task-queue long-polling, native namespaces for multi-environment/multi-tenant partitioning, and is MIT-licensed (no commercial-license risk for self-hosted install). The trade-off accepted is that Temporal is code-first, so flows are wrapped in a YAML DSL + a generic interpreter workflow to give product/console users a declarative authoring surface over the code-first engine.
  • SDK = TypeScript. The entire backend is Node — there is no non-Node backend language in apps/ or services/. Evidence: apps/control-plane-executor/Dockerfile builds FROM node:22-alpine; apps/control-plane-executor/package.json declares "type":"module" (Node ESM); every service under services/ ships .mjs modules with "type":"module". The TypeScript SDK is the only first-class fit; introducing another language solely for flows would fragment the stack.
  • Tenancy = shared namespace + tenantId search attribute. Spike B measured both models at N = {1, 5, 20} tenants. Namespace-per-tenant scales pollers linearly (4 → 20 → 80) and gRPC connections super-linearly (4 → 60 → 840 total), an operational ceiling that worsens per tenant. The shared model holds fleet topology flat at 4 pollers / 4 connections for all N, and the tenantId filter isolates run history with zero leakage. The flat profile and low operational complexity win; the soft isolation boundary is enforced by mandating the tenantId filter on every visibility query.
  • Definition-passing = workflow input. Spike A confirmed the full parsed definition is recorded in the WorkflowExecutionStarted history event, making replay deterministic with no external lookup; the SDK replayer re-ran the history with no non-determinism error.
  • Expression engine = CEL. Spike A bundled and ran both cel-js and jsonata inside the Temporal V8 workflow sandbox — both survived and evaluated deterministically. CEL is chosen for its semantic fit (purpose-built side-effect-free boolean condition language); JSONata is the validated fallback. The measured bundle trade-off (CEL ~2.17 MB vs JSONata ~0.74 MB delta) is a one-time worker-image cost.
  • Visibility = PostgreSQL SQL, sufficient. Spike B ran tenantId-filtered visibility queries against the PostgreSQL advanced-visibility store and observed zero cross-tenant leakage at N up to 20, establishing that PostgreSQL SQL visibility is sufficient and Elasticsearch is not required at this tier.
  • UI stance. Temporal is an internal-only dependency; its Web UI is operator-only and never reachable by tenant credentials — flow authoring/inspection for tenants happens through the Falcone console/API, consistent with the structural_admin / data_access split (ADR-3).

Evidence. SDK: apps/control-plane-executor/Dockerfile (FROM node:22-alpine), apps/control-plane-executor/package.json ("type":"module"), services/*/package.json ("type":"module", .mjs). Spikes: spikes/add-flows-adr-temporal-spikes/spike-a/ (durable resume, retry-across-restart, replay determinism, expression-engines.md) and spikes/add-flows-adr-temporal-spikes/spike-b/ (measurements.md, comparison-table.md, PostgreSQL SQL-visibility proof).

ADR-12 — MCP server hosting: runtime, gateway, OAuth and isolation

Decision. Host tenant MCP servers by reusing Falcone's existing Knative per-tenant runtime — each MCP server is a Knative Service (ksvc) in the tenant's namespace, served over Streamable HTTP. Front inbound traffic with the existing APISIX gateway (ADR-3), enforcing OAuth and per-tool scopes; adopt agentgateway only if APISIX per-tool RBAC proves insufficient. Use the existing realm-per-tenant Keycloak as the OAuth 2.1 Authorization Server (per-tool scopes = client scopes; dynamic client registration curated through the control-plane; consent via the approval-flow precedent). Instant MCP generation always passes through a mandatory curation gate (never a raw dump). Keep runtime pods stateless while storing registry, version, audit, and rate-limit state durably in PostgreSQL; idle servers scale to zero (free via Knative). The runtime/operator and gateway are internal-only, never tenant-exposed. Do not adopt the ToolHive operator or KEDA at this time.

Why.

  • Runtime = reuse Knative (validated live). The platform already runs Knative Serving + Kourier with functions deployed as ksvcs (fn-primary-multiplier), giving namespace-per-tenant, HTTP invocation and scale-to-zero that the team operates today. Spike (a) deployed a minimal MCP server (initialize / tools/list / tools/call, protocol 2025-11-25) as a ksvc in a per-tenant namespace on test-cluster-b; it served MCP JSON-RPC over Streamable HTTP and returned per-tenant context (tenant=tenant-a). Adopting the ToolHive operator would add a new operator to run and an OpenShift-SCC surface to validate, for no isolation/scale capability Knative does not already provide; revisit only if MCP-native MCPServer CRD ergonomics become compelling.
  • Statelessness + scale-to-zero (validated live). Spike (d): the ksvc scaled 1 → 0 replicas at ~30s idle and cold-started in ~1.16s on the next request, returning the correct result — idle tenant servers cost nothing and KEDA is unnecessary while on Knative.
  • Gateway = reuse APISIX. The platform already fronts everything with APISIX and a scope-enforcement plugin across two privilege domains (ADR-3); MCP adds an MCP-aware route terminating Streamable HTTP, validating OAuth tokens and enforcing per-tool scopes. agentgateway (native MCP/A2A, per-tool RBAC) is the recorded fallback.
  • OAuth = extend Keycloak (validated live). Spike (b): the running Keycloak exposes OIDC discovery with a dynamic client registration endpoint (/realms/{realm}/clients-registrations/openid-connect) and supports authorization_code + client_credentials + refresh_token grants — the OAuth 2.1 primitives MCP requires. Falcone models per-tool scopes as client scopes and curates DCR through the control-plane (never exposing raw Keycloak admin), with consent reusing the wf-con-001-user-approval pattern. This avoids building a greenfield Authorization Server.
  • Generation requires mandatory curation. Auto-generated tools from a raw schema/API degrade LLM tool-call quality; Instant MCP therefore emits a draft manifest that must pass a curation gate (prune + LLM-optimized descriptions + scopes) before publish.
  • Spec baseline. Keep the hosted runtime stateless, persist control-plane MCP state in PostgreSQL, pin contracts to the 2025-11-25 stable spec, and treat 2026-07-28 as an announced RC — verify wording before locking any server contract (notably the Tasks extension used by workflows-as-tools).

Risks / caveats.

  • NetworkPolicy isolation is not provable on kind. test-cluster-b's CNI is kindnet, which does not enforce NetworkPolicy, so network-level cross-namespace blocking cannot be demonstrated there. Isolation in this ADR rests on namespace-per-tenant + gateway-only ingress; production/CI must run a policy-enforcing CNI (Calico/Cilium) for the NetworkPolicy/egress controls of issue #399. Recorded as a deployment requirement, not an assumption.
  • Temporal is not present on the spike cluster, so workflows-as-MCP-tools (#395) evidence is deferred to an environment where Temporal runs.

Evidence. Spikes: spikes/add-mcp-hosting-adr-spikes/runtime/ (server.mjs, Dockerfile, ksvc.yaml, evidence/invocation.txt, evidence/scale-to-zero.txt). Live cluster test-cluster-b: a Knative ksvc served MCP JSON-RPC; scaled to zero at ~30s with ~1.16s cold-start; Keycloak OIDC discovery exposes the DCR endpoint and the required grants. Reuses deploy/kind/knative, packages/provisioning-orchestrator/src/appliers/functions-applier.mjs (namespace = tenantId), the APISIX gateway (ADR-3), and packages/adapters/src/keycloak-admin.mjs.

ADR-13 — Migrate object store from MinIO to SeaweedFS

Runbook. Operational architecture, topology, filer-on-PostgreSQL, per-tenant identity model, replication, PVC sizing, TLS/networking, day-2 ops, observability and licensing: SeaweedFS Storage Runbook.

Decision. Replace the bundled MinIO object store with SeaweedFS (Apache-2.0) as the S3-compatible backend for the storage capability. Pin the migration to SeaweedFS 4.33 (chrislusf/seaweedfs@sha256:f0b358973e81f884304737645dd3b278c590c2c9d47d60089729d46324f70495). Back the filer with PostgreSQL ([postgres2], one table per bucket) in a dedicated database, reusing Falcone's existing Postgres tier rather than adding a new stateful dependency. Serve S3 on the confirmed port 8333 with path-style addressing. Bootstrap S3 identities from a static config file and onboard tenants live via s3.configure (no gateway restart). Reject MinIO CE (licence + console regression), RustFS (alpha), and Ceph/Rook (operational weight).

Why.

  • Licence & sustainability. MinIO Community Edition removed OIDC/SSO from its console in May 2025 and its flagship repository was archived in February 2026; its AGPLv3 network-copyleft is a legal misfit for a BaaS that exposes S3 directly to tenants. SeaweedFS is Apache-2.0 with no commercial-licence risk for the self-hosted install, and is purpose-fit for the platform's small-object (SDK artefacts, tenant blobs) access pattern.
  • S3 compatibility validated against Falcone's actual callers. The spike scored 13 SUPPORTED / 1 PARTIAL / 0 UNSUPPORTED at 4.33: path-style + SigV4 presigned GET, ListBuckets/ListObjectsV2, createBucket, object round-trip, putBucketVersioning, putBucketLifecycleConfiguration, putBucketCors, object versioning and object-lock/WORM all work; only putBucketPolicy is PARTIAL (requires Principal:"*" string form vs the AWS-canonical {"AWS":[…]}, a small normalization shim). The gateway ignores the credential region scope, so both region:'auto' (packages/openapi-sdk-service/src/sdk-storage.mjs) and region:'us-east-1' (apps/control-plane/storage-handlers.mjs) work unchanged. Critically, the live runtime's hand-rolled regex XML parser (storage-handlers.mjs:76-97) is byte-compatible with SeaweedFS's list envelopes, including the "-encoded ETag quotes and the NextContinuationToken pagination path — no parser change is required.
  • Operational fit — filer-on-PostgreSQL. SeaweedFS's postgres2 filer reuses the existing Postgres tier with a trivial (dirhash, name, directory, meta) schema and no extensions beyond the default plpgsql. It creates one table per bucket (physical metadata isolation that maps to a tenant/workspace boundary) and DROPs that table on bucket delete (no orphaned metadata — consistent with cascading tenant cleanup, ADR-10). The required correction is an explicit createTable template (the default crashes the filer at boot).
  • Multitenancy — per-tenant identities. New tenant identities are added live through s3.configure -apply with no gateway restart; per-bucket actions (Read:bkt/Write:bkt/List:bkt) enforce isolation (a scoped identity is 403 AccessDenied on another tenant's bucket). The SeaweedFS identity fields map directly onto the synthetic AKST…/sk_… credentials Falcone already derives in packages/adapters/src/storage-programmatic-credentials.mjs; only the scope→actions translation and the injection call are net-new (today provisionWorkspaceStorageBoundary is a NOT_YET_IMPLEMENTED stub and the synthetic keys reach no backend).

Risks / caveats.

  • Version divergence. All downstream changes are written against SeaweedFS 4.33 / the pinned digest; any upgrade must re-run the relevant compatibility-matrix cells before adoption.
  • Filer-on-PostgreSQL schema coupling. Bucket name = PostgreSQL table name, so the filer's DB role needs CREATE/DROP TABLE at runtime and bucket names inherit PG identifier limits (63-byte). SeaweedFS creates these tables out-of-band from Falcone's managed migrations — mitigated by giving SeaweedFS a dedicated database (not the application DB).
  • Bucket-policy shape. putBucketPolicy needs a Principal-normalization shim ({"AWS":[…]}"*") in the SeaweedFS provider; getBucketPolicy round-trips for accepted policies.
  • STS / session tokens unconfigured. The gateway logs Failed to load IAM configuration: no signing key found for STS service at boot; static access/secret-key auth (the path Falcone uses) is unaffected. Set jwt.filer_signing.key only if AssumeRole/STS is later required.
  • Extra surfaces. 4.33 also starts an Iceberg REST catalog on 8181; the deployment change should leave it unexposed.

Rejected alternatives.

  • MinIO CE — AGPLv3 network-copyleft misfit for a tenant-facing S3 BaaS, console OIDC/SSO removed (May 2025), and the flagship repo archived (Feb 2026): not a sustainable bundled default.
  • RustFS — alpha-maturity S3 reimplementation, not production-ready for the tenant data path.
  • Ceph / Rook — operationally heavy (full distributed-storage operator) and disproportionate to Falcone's small-object S3 tier.

ADR-14 — Migrate document store from MongoDB to FerretDB v2 + DocumentDB

Findings. Version-pinned compatibility matrix (aggregation, indexes, transactions, change streams), the per-tenant tenancy spike, the colocated-vs-dedicated decision, and gap remediation paths with downstream owners — all grounded in a live instance (reproducible harness + raw captures recorded with the spike findings).

Runbook. Operational architecture, two-layer topology, pinned image pair + upgrade order, PostgreSQL extension prerequisites, the verified tenancy model, the known-compatibility-differences table, change-stream remediation, observability and licensing: FerretDB + DocumentDB Document-Store Runbook.

Decision. Replace the bundled MongoDB document store with FerretDB v2 — Apache-2.0, Linux Foundation governance — which speaks the MongoDB wire protocol over a PostgreSQL 17 + DocumentDB storage engine. Pin the migration to the version pair ghcr.io/ferretdb/ferretdb:2.7.0 (sha256:5706414241eb84f0515512c37b46db0f1b1eac9e5ceb7e4c2523211c184b1985) / ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0 (sha256:2386795ec2aa7ae559304361979f1dc5708d383ee9020ae63dadc2940dfe58f7). Run the document store on a dedicated Postgres instance (the postgres-documentdb image), not colocated on Falcone's existing relational Postgres, because pg_documentdb_core/pg_documentdb ship only in that image and require shared_preload_libraries (a server-start setting, not session-settable) — colocation would force replacing the relational bitnami/postgresql image for every tenant's RLS schemas. Reject Percona Server (SSPL), native-JSONB (not MongoDB-wire-compatible), ArangoDB (BSL), RavenDB (AGPL), and Couchbase (source-available).

Why.

  • Licence misfit. MongoDB's SSPL is a source-available copyleft licence incompatible with an open-source BaaS that exposes the document store's wire protocol directly to tenants. FerretDB is Apache-2.0 under Linux Foundation governance, removing the licensing risk while preserving the MongoDB driver/wire contract Falcone's clients already use.
  • Wire compatibility validated against Falcone's actual usage (live). hello reports maxWireVersion:21 (MongoDB 7.0 wire level), buildInfo.version 7.0.77, ferretdb v2.7.0. All 15 aggregation stages allowed by packages/adapters/src/mongodb-data-api.mjs ($match/$project/$sort/$limit/$skip/$group/$unwind/$lookup/$count/$facet/$addFields/$set/$unset/$replaceRoot/$replaceWith) are SUPPORTED; $group $sum/$avg over mixed int+double is exact; all five index types (single, compound, unique→E11000, sparse, TTL→purged in ~45s) are SUPPORTED. No PARTIAL aggregation/index entries.
  • Postgres leverage + AI-ready path. The engine is PostgreSQL 17.6 with the document data stored as rows in documentdb_data, so Falcone reuses its Postgres operational tooling. The engine image bundles vector 0.8.1 (pgvector) alongside postgis 3.6.0 and rum 1.3 — an in-place vector-search path for tenant documents — and Falcone's schema-per-tenant RLS (non-BYPASSRLS falcone_app role + app.tenant_id GUC) coexists cleanly with the DocumentDB extensions on the same instance (verified: RLS still enforces, no DDL conflict).

Evidence. The pinned image tags and digests for both ferretdb and postgres-documentdb are recorded above. Upgrade order: start the DocumentDB engine first, then the FerretDB gateway — the pg_documentdb libraries are preloaded via shared_preload_libraries and the documentdb_api schema must exist before the gateway's first wire handshake, so gateway-first fails. Engine GUCs observed: shared_preload_libraries=pg_cron,pg_documentdb_core,pg_documentdb, cron.database_name=postgres, wal_level=logical (the enabling GUC for the logical-replication realtime path). The full per-feature matrix (aggregation task 2, indexes task 3, transaction gap task 4, change-stream gap task 5, tenancy spike task 7, colocated-vs-dedicated task 8), its remediation paths, the reproducible harness (compose.yaml, probes probe-main.mjs/probe-refine.mjs/probe-tenancy.mjs/probe-ttl.mjs) and raw captures are in the findings file linked above; code touchpoints packages/adapters/src/mongodb-data-api.mjs (applyTenantScopeToFilter/injectTenantIntoDocument, AGGREGATION_BLOCKED_STAGES), apps/control-plane-executor/src/runtime/mongo-data-executor.mjs, apps/control-plane-executor/src/runtime/realtime-executor.mjs (#454).

Risks / caveats.

  • Change streams are UNSUPPORTED — highest severity. collection.watch() returns CommandNotSupported (115) ($changeStream is not supported yet) and collMod changeStreamPreAndPostImages returns UnknownBsonField (40415). This blocks both apps/control-plane-executor/src/runtime/realtime-executor.mjs and packages/mongo-cdc-bridge/src/ChangeStreamWatcher.mjs. Remediation is re-architect via a Postgres logical replication slot + pgoutput on the documentdb_data tables, with REPLICA IDENTITY FULL substituting for pre-images (needed to tenant-scope deletes); no native-wire shim exists at this version pair — escalated to the realtime/CDC children.
  • Multi-document transactions are UNSUPPORTED. commitTransaction returns CommandNotFound (59) and the in-flight write persists with no atomicity; abortTransaction is a silent no-op that does not roll back. The mongodb-data-api.mjs transaction op (declared snapshot/majority) must be shimmed to single-operation semantics (DocumentDB guarantees single-doc atomicity) and reject true multi-doc atomic requests at the API boundary, or be dropped.
  • Per-database role isolation is NOT a boundary. A user scoped readWrite on one tenant database successfully read another tenant's database; DocumentDB 0.107 does not enforce per-database role scoping. Falcone's application-layer tenantId scoping (applyTenantScopeToFilter/injectTenantIntoDocument) remains the authoritative isolation mechanism; hard DB-level isolation requires a dedicated DocumentDB database/instance per tenant tier.
  • Aggregation policy nuance. $out, $merge, and $geoNear are actually accepted by the engine (not rejected as assumed); Falcone keeps blocking them via the adapter allowlist (AGGREGATION_BLOCKED_STAGES) — no engine-side guarantee to rely on.
  • Version divergence. Every downstream child is coded to 2.7.0 / 0.107; any upgrade must re-run the affected matrix cells before adoption.
  • Colocated-Postgres extension coupling. shared_preload_libraries cannot be patched per-session, which is exactly why the dedicated instance is chosen; revisiting colocation would re-introduce a whole-instance restart/image change.

Rejected alternatives.

  • Percona Server for MongoDB — distributed under SSPL, the same source-available copyleft that makes stock MongoDB a misfit; swapping vendors does not change the licence problem.
  • Native PostgreSQL JSONB — would drop the MongoDB wire protocol, breaking every existing tenant MongoDB driver and the mongodb-data-api.mjs contract; a client-rewrite, not a migration.
  • ArangoDB — core is BSL (Business Source License), a non-open delayed-source licence that fails the open-source BaaS bar.
  • RavenDBAGPLv3 network-copyleft, a legal misfit for a document store exposed directly to tenants (same class of problem as MinIO's AGPL in ADR-13).
  • Couchbasesource-available (BSL-style) rather than OSI open-source, and not MongoDB-wire-compatible, so it offers neither the licence nor the drop-in-driver property required.

Released under the MIT License.