Skip to main content

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:

  1. Create a Cloudflare R2 bucket + scoped S3 token per org
  2. Create a MySQL database + scoped user on a shared cluster MySQL
  3. Reserve a Redis DB number (0–255) + ACL user on a shared cluster Redis
  4. Create a new Railway service running RAGFlow, pointed at the per-tenant MySQL/Redis/R2 slice
  5. Bootstrap a RAGFlow admin user, mint an API key, discover the tenant_id, create an OpenSearch role+user scoped to ragflow_<uuid>* index pattern
  6. Persist all of the above into an encrypted IntegrationCredential row

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_URL points 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.
  • IntegrationCredential stores ragflow_tenant_id, api_key, dataset ids — same downstream consumer contract as the per-tenant-service design.
  • #300 (cross-org isolation eval, closed completed 2026-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_URL is the canonical config var. Set per environment (dev / staging / production).
  • When RAGFLOW_DEFAULT_BASE_URL is set, provisionForOrg calls 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 in organizations.metadata, returns instructions, no external calls).
  • The prior RAGFLOW_PROVISIONING_MODE=ondemand path — 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:
    1. NestJS application-layer scope — every RagflowClient call carries the requesting org's tenant_id and api_key from IntegrationCredential. The OrgRlsInterceptor already enforces org_id scoping on the request layer.
    2. RAGFlow tenant_id boundary — RAGFlow's own multi-tenant model is the inner gate. Datasets, documents, and retrieval all key on tenant_id.
    3. R2 key prefix scopingR2_KEY_PREFIX=humanwork/${orgId} ensures S3-level path separation even within a shared bucket.

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 IntegrationCredential schema already allows pointing one tenant at a dedicated base_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_URL re-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 through provisionForOrg and 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 the RAGFLOW_PROVISIONING_MODE env var.

Implementation status

  • Shared-instance code path: already shipped in RagflowProvisioningService.provisionForOrg (the defaultBaseUrl branch, lines 97–114).
  • Manual mode: already shipped as the default behavior when RAGFLOW_DEFAULT_BASE_URL is unset.
  • Ondemand removal: done in PR #723's cleanup follow-up — the unreachable RAGFLOW_PROVISIONING_MODE=ondemand code 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