@@ -123,15 +123,30 @@ which store is installed.
123123Four physical tables — ` artifact ` , ` artifact_version ` , ` upload ` ,
124124` mail_attachment_ref ` — plus this package's own migration ledger.
125125
126- ** Hard control-plane foreign keys, by design.** ` tenant_id ` references
127- ` public.tenant(id) ` (` ON DELETE CASCADE ` — a deleted tenant takes its artifacts
128- with it) and ` principal_id ` / ` owner_principal_id ` reference
126+ ** Hard control-plane foreign keys, by design.** ` tenant_id ` is ` NOT NULL ` and
127+ references ` public.tenant(id) ` (` ON DELETE CASCADE ` — a deleted tenant takes its
128+ artifacts with it) and ` principal_id ` / ` owner_principal_id ` reference
129129` public.principal(id) ` (` ON DELETE SET NULL ` — a removed principal detaches its
130130artifacts rather than destroying them). This package is coupled to Interchange:
131131it mounts on Interchange-shaped hosts only, and the host's own migrations must
132132have run before ` runArtifactMigrations ` . The internal key —
133133` artifact_version.artifact_id ` — cascades with its artifact.
134134
135+ ** Cheap row-local CHECKs.** ` artifact.version ` and ` artifact_version.version `
136+ must be ≥ 1; ` upload.size ` and ` mail_attachment_ref.size ` must be ≥ 0. These are
137+ single-column constraints applied by a ledgered migration — free at write time.
138+
139+ ** Principal↔tenant alignment is host-owned.** The package FKs each column into
140+ the control plane independently; it does ** not** enforce that ` principal_id ` (or
141+ ` owner_principal_id ` ) belongs to the same tenant as ` tenant_id ` . A multi-table
142+ trigger or composite FK into ` public.principal ` would couple every write to a
143+ control-plane lookup and is deliberately out of scope. The host's
144+ ` resolvePrincipal ` is the authority: it returns the ` (tenantId, principalId) `
145+ pair every route and tool write stamps, so a correctly mounted host never
146+ plants a cross-tenant principal. Operators cleaning legacy rows before the
147+ ` tenant_id NOT NULL ` migration must assign a valid tenant or delete orphans —
148+ the migration fails with an explicit message if null ` tenant_id ` rows remain.
149+
135150** ` kind ` is free-form text, not a pg enum,** validated at the application edge.
136151New kinds cost no migration. What is * not* free-form is the import allowlist:
137152` POST /api/artifacts ` may only mint ` link ` or ` document ` , so an untrusted caller
@@ -186,6 +201,22 @@ every boot of every replica.
186201 fresh databases diverge silently. Ship a new migration instead. The column is
187202 ` NOT NULL ` , so the guarantee is unconditional: there is no unrecorded row for
188203 the runner to adopt and wave through.
204+ - Event timestamps (` created_at ` , ` updated_at ` , ` archived_at ` ) are
205+ ** ` timestamptz ` ** . The initial create migration still lays them down as
206+ zoneless ` timestamp ` ; a follow-on migration retypes them with
207+ ` USING col AT TIME ZONE 'UTC' ` , treating existing walls as the UTC clocks the
208+ package always assumed. List keyset cursors project through
209+ ` AT TIME ZONE 'UTC' ` and compare with ` ::timestamptz ` , so paging and date
210+ filters stay on the absolute instant under any session ` TimeZone ` . Rollback is
211+ the reverse cast (` TYPE timestamp USING col AT TIME ZONE 'UTC' ` ) plus a new
212+ ledgered migration — never edit a shipped one.
213+ - A later ledgered migration sets ` artifact.tenant_id NOT NULL ` and adds the
214+ version/size CHECKs. If null-tenant rows still exist, that migration raises
215+ before altering the column so the operator can clean them up first.
216+ - Empty ledger + pre-existing package objects fails closed
217+ (` MigrationAdoptError ` ). ` { adopt: true } ` records checksums without re-DDL
218+ only after shape validation: tables, column types, required nullability, and
219+ the named CHECK constraints. Column presence alone is not enough.
189220
190221** The package owns its own Postgres schema.** Every table, index and the ledger
191222live in ` artifacts ` , created by the runner and qualified in every
0 commit comments