Cross-cutting invariants — the rules that span subsystems
Fail-closed authorization, everywhere. Every BFF entity-proxy read/write resolves grants under
application_id=admin-bff; UMS unreachable → 503 (fail-closed); no grants returned →1=0deny-all. The public reader is twin-gated (Gate A surface admissibility + Gate B UMS row-RLS) and fails closed (EmptyPublishedStatesError). Action gates reuse the same criteria at read-time availability and submit-time enforcement, so CTAs never drift.Tenant identity is derived, never trusted from inbound headers. Tenant comes from the JWT claim (Step 3a) and is forwarded as
X-Tenant-Idon outbound ontology calls. The sole cross-plane authoring bridge isX-Author-Tenant, honored only for callers holding the right UMS tier grant (authoring_update_allowed, auth.py:275 — a raw SpiceDB tier-group read, independent of data-plane rule resolution, to prevent confused-deputy amplification).Declare-not-code. Policy, actions, surfaces, and notifications are ontology rows compiled per request — not Python branches. The
admin_entity_config.actionsJSONB cache is dead; the rows ARE the runtime contract (#292 S2). Adding a capability = adding a row + (if new) a registry key.Fork fidelity by copy-and-rewire. A new tenant comes up fully configured via one-shot full-catalog fork keyed on
metadata.forkable(fork_plan.py:91-101); copied rows are FK-rewired into the tenant plane. Same editors serve every tenant because each endpoint keys on the caller's resolved tenant. Copy-on-create, no live inheritance.Presentation-only design system. The renderer (
@aiaiai-pt/design-system) dispatchesBlockdescriptors to widgets via registry keys and carries no editing metadata; authoring field-schema lives in the host. TH-08: only known-good registry-key literals reach the DOM.Views, not endpoints, for aggregates. Every count/tally/group-by is an ontology VIEW (the only aggregate primitive), surfaced as a Metabase card (staff) or
source_kind:viewpublic feed (citizen). Never a bespoke BFF stats route. A view bakes its creator's ACL (and every JOINed type's ACL) into a frozencompiled_sqlsnapshot, soX-Tenant-Idnever narrows a view.Dedup ledgers are written AFTER the action they guard. Idempotency rows (
RecurrenceNotificationLog,ConflictNotificationLog, theaction_submissionsstatus guard) are created after the guarded action succeeds, withget_or_create(notcreate) underUniqueConstraint, and composite keys sorted for order-independence — so a failed dispatch can still retry.Schema evolves additively only. The ontology never deletes a column; the sheet diff is no-delete; round-trip-to-zero holds (re-importing an applied sheet is a no-op).
tenant_scopedis REQUIRED-EXPLICIT in the sheet grammar — but note the engine's own default isFalse(repo.py:208,245), the divergence named below.Plane-agnostic engine. The ontology engine sees only ordinary tenant-scoped rows; all vocabulary/template/tenant and public-surface semantics live BFF-side. This boundary is deliberate and, per the open questions, possibly permanent.