Superseded by ADR-024
ADR-009: RAGFlow shared instance with tenant_id isolation, not per-tenant Railway services
Date: 2026-05-25
Status: Accepted
Issues: #291 (closed as not_planned), #612 (RAGFlow workstream parent)
Supersedes: D-RF-1, D-RF-2, D-RF-14, D-RF-15, D-RF-16 (shared cluster + per-tenant slice design)
Context
#291 ("Per-tenant RAGFlow provisioning service — shared cluster + Railway + existing-orgs backfill") specified a 12-step orchestrator that, on every org creation, would:
- Create a Cloudflare R2 bucket + scoped S3 token per org
- Create a MySQL database + scoped user on a shared cluster MySQL
- Reserve a Redis DB number (0–255) + ACL user on a shared cluster Redis
- Create a new Railway service running RAGFlow, pointed at the per-tenant MySQL/Redis/R2 slice
- Bootstrap a RAGFlow admin user, mint an API key, discover the tenant_id, create an OpenSearch role+user scoped to
ragflow_<uuid>*index pattern - Persist all of the above into an encrypted
IntegrationCredentialrow
Estimate: XL, ~1–2 weeks focused work. Prerequisite: a shared cluster bootstrap template (railway-template/bootstrap/) that doesn't exist in the repo yet.
The downstream issues blocked on it (#298 soft-delete, #299 circuit breaker, #315 org-member sync, #296 onboarding business-context upload) all inherit the same prerequisite chain.
Meanwhile, the existing RagflowProvisioningService already implements a shared-instance + tenant_id mode that works today:
RAGFLOW_DEFAULT_BASE_URLpoints at a single shared RAGFlow instance.- On org provisioning, the service calls RAGFlow's admin API to create a per-org tenant inside that instance.
- Three datasets per tenant:
default-kb,conversations,corrections. IntegrationCredentialstoresragflow_tenant_id,api_key, dataset ids — same downstream consumer contract as the per-tenant-service design.- #300 (cross-org isolation eval, closed
completed2026-05-24) gated this path.
This ADR formalizes the pivot from the per-tenant-Railway-service design to the shared-instance design as the canonical path.
Decision
One RAGFlow service per environment, multi-tenant internally via RAGFlow's native tenant_id boundary.
RAGFLOW_DEFAULT_BASE_URLis the canonical config var. Set per environment (dev / staging / production).- When
RAGFLOW_DEFAULT_BASE_URLis set,provisionForOrgcalls RAGFlow's admin API to create a per-org tenant in the shared instance. When unset, the service falls back to manual mode (records desired state inorganizations.metadata, returns instructions, no external calls). - The prior
RAGFLOW_PROVISIONING_MODE=ondemandpath — intended to invoke the 12-step per-tenant orchestrator — is removed. The env var itself is also removed; no replacement is needed. - Cross-tenant isolation rests on three gates:
- NestJS application-layer scope — every
RagflowClientcall carries the requesting org'stenant_idandapi_keyfromIntegrationCredential. TheOrgRlsInterceptoralready enforces org_id scoping on the request layer. - RAGFlow tenant_id boundary — RAGFlow's own multi-tenant model is the inner gate. Datasets, documents, and retrieval all key on tenant_id.
- R2 key prefix scoping —
R2_KEY_PREFIX=humanwork/${orgId}ensures S3-level path separation even within a shared bucket.
- NestJS application-layer scope — every
Why
- Cost. A Railway RAGFlow service is ~$15–25/mo. At 100 tenants that's ~$2k/mo just for RAGFlow infra, before any actual usage. The shared-instance path is one service.
- Operational surface. The 12-step orchestrator touches 5 external admin APIs (Cloudflare R2, Railway GraphQL, MySQL admin, OpenSearch security plugin, Redis ACL) and a per-tenant rollback state machine. Each is an independent failure mode. The shared path has one admin API (RAGFlow itself).
- Prerequisite that doesn't exist. The shared cluster bootstrap template (
railway-template/bootstrap/) was a prerequisite. It hasn't been built and shipping it first would push #291 further out. - Shared path already works. #289 through #316 shipped between 2026-05-12 and 2026-05-24 against the shared-instance path. Cross-org isolation eval (#300) gated and passed.
- Reversibility. If a per-tenant deploy becomes necessary for a specific enterprise customer (compliance / locality), the existing
IntegrationCredentialschema already allows pointing one tenant at a dedicatedbase_url. The per-tenant orchestrator can be added behind a feature flag for that single tenant without rewriting the dispatch path.
Consequences
Positive
- Time-to-ship for the downstream issues (#298 / #299 / #315 / #296) drops by ~1–2 weeks each. None of them actually require the per-tenant infrastructure; they require any provisioning surface, and the shared one already exists.
- Cluster operations (MySQL/OpenSearch/Redis admin) leave the surface. We don't need to operate a multi-tenant slice manager.
- Cost per tenant trends to ~$0 in marginal infra spend.
Negative
- Single shared instance is a single failure domain. A RAGFlow upgrade gone wrong takes down KB retrieval for every tenant simultaneously. Mitigation: blue/green RAGFlow service swap behind
RAGFLOW_DEFAULT_BASE_URLre-pointing. - Noisy-neighbor risk. A tenant uploading 100k docs can push the shared instance into degraded retrieval latency for everyone. Mitigation: per-tenant rate limits at the NestJS layer (#299 circuit breaker remains relevant, just at the API layer rather than per-instance).
- Compliance edge case. Some enterprise customers may contractually require per-tenant data plane isolation. Mitigation: the escape hatch above (one tenant → dedicated
base_url).
Neutral
- The 27 existing dev orgs that the backfill script (
api/scripts/ragflow-backfill.ts) targets continue to flow throughprovisionForOrgand create tenants in the shared dev instance. No backfill path change. - The provisioning runbook (
docs/_archive/ragflow-provisioning.md) is updated in the same cleanup PR to drop theRAGFLOW_PROVISIONING_MODEenv var.
Implementation status
- Shared-instance code path: already shipped in
RagflowProvisioningService.provisionForOrg(thedefaultBaseUrlbranch, lines 97–114). - Manual mode: already shipped as the default behavior when
RAGFLOW_DEFAULT_BASE_URLis unset. - Ondemand removal: done in PR #723's cleanup follow-up — the unreachable
RAGFLOW_PROVISIONING_MODE=ondemandcode path was pruned and the env var dropped from configs/docs. - Cross-tenant isolation regression test: already shipped (#300).
No new code required for this ADR to be effective.
Open follow-ups
- Verify shared-tenant isolation against the dev RAGFlow instance under the post-per-org-runtime topology (after HP-2104 end-to-end smoke). Filed separately.
- If a paying customer triggers the compliance edge case, file a new issue for the per-tenant escape hatch — not before.
References
api/src/ragflow/ragflow-provisioning.service.ts— current canonical implementationapi/scripts/ragflow-backfill.ts— backfill CLI (shared-instance compatible)docs/_archive/ragflow-provisioning.md— operational runbook- #300 — cross-org isolation eval (closed, passed)
- ADR-004 — per-org corrections dataset
- ADR-008 — KB architecture K1 + K2