Summary
The PostgreSQL backend advertises BackendCapability::SchemaPerTenant and BackendCapability::DatabasePerTenant, but neither is implemented. The backend does shared-schema multitenancy only, via a tenant_id discriminator column.
This is the design discussion #28 actually chose:
For SQL-backed FHIR persistence, the shared schema approach with a tenant_id discriminator is pragmatic, but the enforcement layer must be airtight - you literally cannot construct a storage operation without providing tenant context.
Schema-per-tenant and database-per-tenant appear there as surveyed alternatives, not commitments. So the capability advertisement is wrong, not merely ahead of the implementation.
Evidence
What the backend claims — crates/persistence/src/backends/postgres/backend.rs, both in supports() (~line 720) and capabilities() (~line 748):
BackendCapability::SharedSchema,
BackendCapability::SchemaPerTenant,
BackendCapability::DatabasePerTenant,
What it actually does — shared schema with a tenant discriminator, crates/persistence/src/backends/postgres/schema.rs:101:
CREATE TABLE IF NOT EXISTS resources (
tenant_id TEXT NOT NULL,
resource_type TEXT NOT NULL,
id TEXT NOT NULL,
...
PRIMARY KEY (tenant_id, resource_type, id)
)
No schema/database switching exists in any code path. crates/persistence/src/strategy/ contains ~2,100 lines of scaffolding — schema_per_tenant.rs (542 lines, SET search_path TO tenant_x, shared, public, CREATE SCHEMA ... TEMPLATE), database_per_tenant.rs (760 lines), shared_schema.rs (522 lines) — but outside the module itself the only references in the entire workspace are the re-exports at crates/persistence/src/lib.rs:190-191 and a doc comment in crates/rest/tests/admin_tenants.rs:159. No backend calls set_search_path_sql(). There is no configuration knob (env var or otherwise) to select a tenancy strategy.
The test matrix disagrees with the backend. crates/persistence/tests/common/capabilities.rs:162-163 marks Postgres as:
(BackendCapability::SchemaPerTenant, SupportLevel::Planned),
(BackendCapability::DatabasePerTenant, SupportLevel::Planned),
Planned in the matrix vs. reported-as-supported by capabilities(). Both cannot be right.
(Note: crates/persistence/tests/postgres_tests.rs:98 also lists these variants, but that block ends in assert!(!expected.is_empty()) — it is a compile-time variant-existence check, not an assertion about what the backend reports. It does not currently pin the wrong behavior, though it reads as if it does.)
Why it matters
capabilities() / supports() are the introspection surface callers use to decide whether a backend can satisfy a deployment's isolation requirement. A caller that asks "does this backend give me schema-level tenant isolation?" gets true and would provision on a false premise — an isolation guarantee that isn't there. For a healthcare server, an overstated isolation capability is the wrong direction to be wrong in.
Proposed fix
Drop SchemaPerTenant and DatabasePerTenant from both supports() and capabilities() in the Postgres backend, leaving SharedSchema. Then reconcile tests/common/capabilities.rs so the matrix and the backend agree.
Whether crates/persistence/src/strategy/ should be implemented or deleted is tracked separately in #370. This issue stays scoped to correcting the advertisement; #370 settles what happens to the code behind it.
Summary
The PostgreSQL backend advertises
BackendCapability::SchemaPerTenantandBackendCapability::DatabasePerTenant, but neither is implemented. The backend does shared-schema multitenancy only, via atenant_iddiscriminator column.This is the design discussion #28 actually chose:
Schema-per-tenant and database-per-tenant appear there as surveyed alternatives, not commitments. So the capability advertisement is wrong, not merely ahead of the implementation.
Evidence
What the backend claims —
crates/persistence/src/backends/postgres/backend.rs, both insupports()(~line 720) andcapabilities()(~line 748):What it actually does — shared schema with a tenant discriminator,
crates/persistence/src/backends/postgres/schema.rs:101:No schema/database switching exists in any code path.
crates/persistence/src/strategy/contains ~2,100 lines of scaffolding —schema_per_tenant.rs(542 lines,SET search_path TO tenant_x, shared, public,CREATE SCHEMA ... TEMPLATE),database_per_tenant.rs(760 lines),shared_schema.rs(522 lines) — but outside the module itself the only references in the entire workspace are the re-exports atcrates/persistence/src/lib.rs:190-191and a doc comment incrates/rest/tests/admin_tenants.rs:159. No backend callsset_search_path_sql(). There is no configuration knob (env var or otherwise) to select a tenancy strategy.The test matrix disagrees with the backend.
crates/persistence/tests/common/capabilities.rs:162-163marks Postgres as:Plannedin the matrix vs. reported-as-supported bycapabilities(). Both cannot be right.(Note:
crates/persistence/tests/postgres_tests.rs:98also lists these variants, but that block ends inassert!(!expected.is_empty())— it is a compile-time variant-existence check, not an assertion about what the backend reports. It does not currently pin the wrong behavior, though it reads as if it does.)Why it matters
capabilities()/supports()are the introspection surface callers use to decide whether a backend can satisfy a deployment's isolation requirement. A caller that asks "does this backend give me schema-level tenant isolation?" getstrueand would provision on a false premise — an isolation guarantee that isn't there. For a healthcare server, an overstated isolation capability is the wrong direction to be wrong in.Proposed fix
Drop
SchemaPerTenantandDatabasePerTenantfrom bothsupports()andcapabilities()in the Postgres backend, leavingSharedSchema. Then reconciletests/common/capabilities.rsso the matrix and the backend agree.Whether
crates/persistence/src/strategy/should be implemented or deleted is tracked separately in #370. This issue stays scoped to correcting the advertisement; #370 settles what happens to the code behind it.