Split out of #11611 (customer question: "platform/system tables never carry organization_id — by design, or planned?"). #11611 resolved 7 of its 8 table families; sys_file is the one that could not be answered without a ruling, so it gets its own card rather than being decided inside a question thread.
⛔ Not graded, not routed. This is a decision card: the measurement is settled, the intent is not.
Measured, on origin/main at 34d30118c
1. The insert carries no context. packages/services/service-storage/src/metadata-store.ts:195:
async createFile(rec: FileRecord): Promise<FileRecord> {
const now = new Date().toISOString();
const full: FileRecord = { created_at: now, updated_at: now, ...rec };
if (!this.engine) {
this.files.set(full.id, full);
return full;
}
await this.engineOp('sys_file', 'insert', FILE_INSERT_CONSEQUENCE, (engine) =>
engine.insert('sys_file', full),
);
return full;
}
Reverse control: the string context appears 0 times in the whole of metadata-store.ts. There is no context-threading that is merely being dropped — there is no channel at all.
git show origin/main:packages/services/service-storage/src/metadata-store.ts | grep -c context # → 0
2. sys_file is a tenancy-ENABLED object. packages/services/service-storage/src/objects/system-file.object.ts:21 declares no tenancy key at all in its 183 lines, so isTenancyDisabled() (packages/spec/src/data/object.zod.ts:700) reads false and the registry injects organization_id as it does for any business object. The column exists on the table; nothing writes it. That is the shape the reporter measured: sys_file sits in #11611's 233-row "config & metadata misc" bucket, 100% NULL.
3. Both callers already hold a session. packages/services/service-storage/src/storage-routes.ts:320 and :424 call store.createFile({...}), and each one reads the session two lines below — owner_id: session?.userId at :330 and :435. The organization is available at the call site; it is simply not part of the store's signature.
4. sys_file has no sibling with this door. Zero non-test call sites of engine.insert('sys_attachment', …) anywhere in the repo — sys_attachment has no context-free service-store door of the kind sys_file has, which is what makes option B's "the boundary is sys_attachment" a testable claim rather than an assertion.
⚠️ Read-visibility consequence — load-bearing for A
Forward-stamping is not visibility-neutral on a walled deployment. The SQL driver's tenant predicate is NULL-tolerant (packages/drivers/driver-sql/src/sql-driver.ts:11683):
(organization_id = :tenantId OR organization_id IS NULL)
but Layer 0 AND-composes a strict predicate above it. packages/plugins/plugin-security/src/tenant-layer.ts:101 — { organization_id: <id> } is "the isolated wall, AND-composed unconditionally"; and packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts:40 states the consequence in terms:
Layer 0's strict organization_id = :tenant AND-composes over the driver's compatibility arm and the conjunction is the strict equality alone.
So under group/isolated, once stamping starts: new files become org-walled, existing NULL-org files become invisible to every principal. A forward-only stamp therefore splits the table's read behaviour in two. Whether that is acceptable, or whether A must ship with a backfill, is part of what is being decided here — this card does not assume it.
(single posture is inert — computeTenantLayer0Filter returns null — so nothing changes for single-tenant installs under any option.)
Options
A — rule it a gap. Thread the session organization through StorageMetadataStore.createFile so new rows are stamped. Forward-stamping only; a backfill of the existing NULL rows is a separate decision, and per the paragraph above it may be a required companion rather than an optional one on walled deployments. Cost: a signature change on a service-store method plus its two call sites.
B — rule it by design. sys_file is the blob ledger (fileId ↔ backend key), deliberately org-less because a stored blob is addressed by key and can be shared; the tenancy boundary is sys_attachment, which binds a file to a record that is itself org-scoped. Under this ruling sys_file should say so out loud — tenancy: { enabled: false }, the ADR-0066 platform-global posture — rather than carrying an always-NULL column that reads as an unfinished migration. ⛔ This inherits #11611's unresolved column-drop question: turning the key off stops the registry injecting organization_id, and whether the schema sync then DROPs the existing column on a customer database is not established here. Unlike the job family (5,613 rows), the column is 100% NULL here, so no data is at stake — but it is still destructive DDL on a production table, which is maintainer floor either way.
C — defer / record only. Note that the customer explicitly asked us not to take this one: #11611's ask was to "know whether org markers will be added later, so we can decide whether to account for it in operations", and C returns exactly the non-answer they wrote in to avoid.
Recommendation
A. The asymmetry in fact 3 is the argument: every other platform family in #11611 was either resolved or had a principled reason to be org-less, whereas here the organization is sitting in the caller's hand two lines from the insert and is dropped only because the store's signature has no place to put it. B is defensible as architecture but is a post-hoc reading of an absence, and it asks for destructive DDL to make the absence honest.
Not established here
- No claim about whether A needs the backfill or can ship forward-only — the visibility split above is measured, its acceptability is not judged.
- Whether the schema sync DROPs a de-injected tenant column (B's real cost) is unmeasured.
- No severity grade. Nothing here is a malfunction: the reporter's own framing is "everything works".
Re-check
git show origin/main:packages/services/service-storage/src/metadata-store.ts | grep -c context
git grep -n "tenancy" origin/main -- packages/services/service-storage/src/objects/system-file.object.ts
git grep -n "owner_id: session?.userId" origin/main -- packages/services/service-storage/src/storage-routes.ts
git grep -n "AND-composes over the driver" origin/main -- packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts
Dedup
Searched the org-scoping family: #11611 is this card's parent and is being closed against it; #3249 (ADR-0066 platform-global posture) and #3623 / ADR-0105 D2 (group-posture read reach) are the mechanisms cited above, not duplicates; #11303 (PR #11698) is forward-stamping for a different table set and does not reach sys_file. No match.
Split out of #11611 (customer question: "platform/system tables never carry
organization_id— by design, or planned?"). #11611 resolved 7 of its 8 table families;sys_fileis the one that could not be answered without a ruling, so it gets its own card rather than being decided inside a question thread.⛔ Not graded, not routed. This is a decision card: the measurement is settled, the intent is not.
Measured, on
origin/mainat34d30118c1. The insert carries no context.
packages/services/service-storage/src/metadata-store.ts:195:Reverse control: the string
contextappears 0 times in the whole ofmetadata-store.ts. There is no context-threading that is merely being dropped — there is no channel at all.2.
sys_fileis a tenancy-ENABLED object.packages/services/service-storage/src/objects/system-file.object.ts:21declares notenancykey at all in its 183 lines, soisTenancyDisabled()(packages/spec/src/data/object.zod.ts:700) readsfalseand the registry injectsorganization_idas it does for any business object. The column exists on the table; nothing writes it. That is the shape the reporter measured:sys_filesits in #11611's 233-row "config & metadata misc" bucket, 100% NULL.3. Both callers already hold a session.
packages/services/service-storage/src/storage-routes.ts:320and:424callstore.createFile({...}), and each one reads the session two lines below —owner_id: session?.userIdat:330and:435. The organization is available at the call site; it is simply not part of the store's signature.4.
sys_filehas no sibling with this door. Zero non-test call sites ofengine.insert('sys_attachment', …)anywhere in the repo —sys_attachmenthas no context-free service-store door of the kindsys_filehas, which is what makes option B's "the boundary issys_attachment" a testable claim rather than an assertion.Forward-stamping is not visibility-neutral on a walled deployment. The SQL driver's tenant predicate is NULL-tolerant (
packages/drivers/driver-sql/src/sql-driver.ts:11683):but Layer 0 AND-composes a strict predicate above it.
packages/plugins/plugin-security/src/tenant-layer.ts:101—{ organization_id: <id> }is "theisolatedwall, AND-composed unconditionally"; andpackages/plugins/plugin-security/src/bootstrap-declared-permissions.ts:40states the consequence in terms:So under
group/isolated, once stamping starts: new files become org-walled, existing NULL-org files become invisible to every principal. A forward-only stamp therefore splits the table's read behaviour in two. Whether that is acceptable, or whether A must ship with a backfill, is part of what is being decided here — this card does not assume it.(
singleposture is inert —computeTenantLayer0Filterreturnsnull— so nothing changes for single-tenant installs under any option.)Options
A — rule it a gap. Thread the session organization through
StorageMetadataStore.createFileso new rows are stamped. Forward-stamping only; a backfill of the existing NULL rows is a separate decision, and per the paragraph above it may be a required companion rather than an optional one on walled deployments. Cost: a signature change on a service-store method plus its two call sites.B — rule it by design.
sys_fileis the blob ledger (fileId ↔ backend key), deliberately org-less because a stored blob is addressed by key and can be shared; the tenancy boundary issys_attachment, which binds a file to a record that is itself org-scoped. Under this rulingsys_fileshould say so out loud —tenancy: { enabled: false }, the ADR-0066 platform-global posture — rather than carrying an always-NULL column that reads as an unfinished migration. ⛔ This inherits #11611's unresolved column-drop question: turning the key off stops the registry injectingorganization_id, and whether the schema sync then DROPs the existing column on a customer database is not established here. Unlike the job family (5,613 rows), the column is 100% NULL here, so no data is at stake — but it is still destructive DDL on a production table, which is maintainer floor either way.C — defer / record only. Note that the customer explicitly asked us not to take this one: #11611's ask was to "know whether org markers will be added later, so we can decide whether to account for it in operations", and C returns exactly the non-answer they wrote in to avoid.
Recommendation
A. The asymmetry in fact 3 is the argument: every other platform family in #11611 was either resolved or had a principled reason to be org-less, whereas here the organization is sitting in the caller's hand two lines from the insert and is dropped only because the store's signature has no place to put it. B is defensible as architecture but is a post-hoc reading of an absence, and it asks for destructive DDL to make the absence honest.
Not established here
Re-check
Dedup
Searched the org-scoping family: #11611 is this card's parent and is being closed against it; #3249 (ADR-0066 platform-global posture) and #3623 / ADR-0105 D2 (group-posture read reach) are the mechanisms cited above, not duplicates; #11303 (PR #11698) is forward-stamping for a different table set and does not reach
sys_file. No match.