Files
cgalo5758 8e3c68c6be Make UI surfaces honestly reflect system state
- Add deployment-name branding to titles, mastheads, and OG tags
- Share one grant delivery-state query with lineage across grants
  surfaces
- Show pool status/usage, org owners, and config readiness
- Make billing views projection-aware with recency and sync vocabulary
- Guard FedWiki creation without domains and render route-aware 404s
2026-08-23 01:45:52 -05:00

13 KiB
Raw Permalink Blame History

title, audience, summary
title audience summary
Entitlements
developer
How capabilities are defined, delivered, and enforced: sets and their rules, grants as decrees, provisions as delivery, and the materializer that turns it all into effective limits.

Entitlements

Purpose

This model answers "what can this organization actually do, and how much of it?". A capability is named by a resource key (for example fedwiki_sites), carrying a kindnumeric (a countable limit) or boolean (an on/off capability). Most keys are owned by a provider — the registry entity representing an integrated external service such as FedWiki (federated wiki hosting) or Discourse (forums) — which is also the code that enforces them; a key with no provider is platform-owned. An entitlement set bundles rules; a boolean or limit rule says what one resource key grants (the reserved credit rule type names no key). Products point at sets — the Product Catalog card owns that linkage.

Delivery is a five-step pipeline:

  1. A funding source exists: a subscription, or a grant — the decree ledger, a durable record that an operator or the system decided to give an organization a product.
  2. Conferral turns the source into a provision row in the organization's resource pool, the per-organization container; every organization gets a default pool.
  3. Materialization recomputes the pool's effective entitlements from all its live provisions.
  4. The results land in two read models: numeric entitlements (core.numeric_entitlements, one limit per resource key, explained row-by-row in core.numeric_entitlement_contributions) and boolean entitlements (core.boolean_entitlements).
  5. Consumers read only those materialized rows — never the rules. A consumer resolves its workspace (the working surface an organization gets, where sites and services live; the identity model owns it) to a pool through core.pool_assignments, then reads the pool's rows, and reports consumption into core.numeric_entitlement_usage.

Three statuses live on three different objects and must never be conflated: a grant's status (active/revoked/expired) is issuance history; a provision's status (active/suspended/ended) is the currently-delivering fact; and the "live / superseded / inactive" labels the operator UI shows are derived at read time by joining the two — never stored.

Vocabulary reused from the Product Catalog card: product, conferral, provision, the five conferral functions. From Plan Ladders and Transitions: occupancy, supersession, default restoration. From Payments and Billing: subscription, reconciler.

Where this lives

Database objects sit in the core schema; baseline DDL in internal/db/migrations/00001_init.sql, altered by 0000400008 (the "doc41" in some file names is an internal change label, nothing more).

Piece What it is Where
core.resource_keys The capability namespace: kind (boolean or numeric) discriminates what rule shape a key accepts; provider names the owning integration, or is empty for platform-owned keys (integrations stamp their ownership at boot) 00001_init.sql; seeds in 00002; kind in 00008_resource_key_kinds.sql
core.entitlement_sets Named rule bundles; name is unique; is_active gates only whether new products can pick the set 00001_init.sql
core.entitlement_set_rules One rule per row: rule_type (boolean/limit/quota/credit), value, stacking policy, per-unit flag — with a CHECK forcing each type's fields into shape 00001_init.sql
core.grants The decree ledger: who granted which product to whom and why, with extends_grant_id chaining a replacement grant to its ancestor 00001_init.sql; reshaped in 00004
core.pool_provisions The delivery ledger: one row per (pool, product, source) with quantity, time bounds, and the active/suspended/ended status that is the currently-delivering fact 00001_init.sql; 00004
core.resource_pools, core.pool_assignments The per-organization container entitlements materialize onto, and its links to workspaces 00001_init.sql
core.numeric_entitlements, core.numeric_entitlement_contributions, core.numeric_entitlement_usage The materialized numeric limits, the per-provision contribution ledger explaining each limit, and the usage counters consumers increment 00001_init.sql
core.boolean_entitlements Materialized on/off capabilities: granted true while at least one active provision carries the rule; rows lapse to false but are never deleted, so an absent row always means never-conferred 00007_boolean_entitlements.sql
The five conferral functions The sole writers of the provision tables (verbs glossed in the ladders card), raising named error codes the conferral boundary translates 00005_doc41_conferral_functions.sql
Grant-lineage guards A CHECK rejects self-extension; two triggers reject cross-organization lineage and freeze extends_grant_id after insert 00001_init.sql
Materializer MaterializePoolEntitlements: full in-transaction re-evaluation of a pool's active provisions into limits, contributions, and boolean grants internal/entitlements/materialize.go
Conferral boundary Typed wrappers over the five functions, error mapping, and ConferGrantTx — decree, confer, materialize in one transaction internal/entitlements/conferral.go
Default restoration Mints and confers the baseline default grant (see the ladders card for the floor guard) internal/entitlements/reapply_defaults.go
Delivery-state query ListGrantsWithDelivery derives each grant's live/superseded/inactive label (plus supersession lineage — extends_grant_id and, for a superseded row, the grant that replaced it) by joining onto provisions at read time; one shared query an optional org filter narrows, so the grants index and the per-organization grants table render from the exact same derivation and can never disagree internal/entitlements/queries/grants.sql
Rule authoring Operator set and rule CRUD: rule_type derived from the key's kind, stacking fixed to additive (the selector left the form), constraint violations mapped to field errors internal/server/operator_entitlement_sets.go
Grant issuance The operator grant forms (quantity capped at 1..1,000,000): issuing composes through ConferGrantTx; extending hand-rolls the same three steps in its own transaction. The Issue Grant form discloses that recording the grant is immediate while any provider-side follow-through it triggers is separately queued work internal/server/operator_enrollment.go
Grant surfaces The grants index (/operator/grants) and the per-organization composite both render ListGrantsWithDelivery's rows: delivery state as a badge (never grants.status under a bare "Status" header), lineage, and an in-place ledger-vs-delivery explanation internal/server/operator_partials.go (index); internal/server/operator_enrollment.go (org detail)
Consumers The gate for claiming DNS domains (Domains Registry card), the member entitlements panel, and the FedWiki site-limit check reading limit vs usage internal/server/external_claim_gate.go; internal/server/member_products.go; internal/integrations/fedwiki/web/api.go

Invariants

An invariant is a rule the model guarantees everywhere; code that would break one is wrong even if it works locally. Tags: [db] — a schema constraint rejects violations; [db-code] — database-side code upholds it; [app] — only Go code upholds it. (The position-table invariants live in the ladders card: only the five functions write them (its invariant 2), one live provision per source (4), supersession is whole-bundle and atomic (7). They hold here unchanged.)

  1. [db] A set carries at most one active rule per resource key — a partial unique index (a uniqueness rule applied only to rows matching a condition, here active rules with a key) rejects the duplicate at insert — and each rule's fields must match its type: a boolean rule must leave value, stacking, and per-unit empty.
  2. [db] A resource key's kind is one of boolean or numeric, CHECK-enforced.
  3. [app] Rule shape follows key kind: the authoring form derives the rule type from the chosen key's kind, so the UI cannot create a mismatched rule. No CHECK ties rule_type to the key's kind, so direct SQL still could — the guarantee is app-layer.
  4. [db] Every grant names a real product — product_id is NOT NULL, and no column lets a grant point at an entitlement set directly.
  5. [db] A grant's reason is one of exactly eight allowed values — manual, default, evaluation, promotional, complimentary, sponsored, board_decision, legacy — and default is legal only on system-authored grants (no granting person recorded).
  6. [db-code] Grant lineage is safe: a CHECK rejects self-extension, and triggers reject cross-organization lineage and freeze the lineage pointer after insert.
  7. [app] grants.status is issuance history only — active, revoked, expired, written by the operator and expiry paths, with no schema CHECK. The currently-delivering fact lives on the provision's status, and any UI classification is derived per read by joining the two. Code must never read a grant's status as delivery.
  8. [app] Materialization is an idempotent full re-evaluation, and it runs in the same database transaction as every conferral-changing write — grant issuance, Stripe reconciliation, expiry, tier changes, default restoration — so effective entitlements never lag a committed conferral.
  9. [app] Rule authoring accepts only the additive stacking policy; the policy selector no longer exists on the form, and a request carrying any other value is rejected. The materializer still computes additive and maximum for rows that already carry them, and any other stored value contributes nothing to the limit. (What stacking policies are for, and how mixtures should combine, is an open exploration in status/issues.md.)
  10. [app] A boolean entitlement is granted while at least one active provision carries its rule; when the last one goes, the row flips to not-granted but is never deleted — absence always means never-conferred.
  11. [app] A per-unit rule contributes its value multiplied by the provision's quantity; a flat rule contributes its value once. The choice is per rule, applied within one provision — quantity never multiplies across provisions.
  12. [db] A provision's quantity is positive — a CHECK rejects zero and negatives.
  13. [app] The usage counter has one mutator: consumers increment it. The materializer seeds each counter row at zero when it first materializes a limit, then never touches the value; enforcement compares limit to usage at read time.

Dimensions

Four quantity-flavored axes come first, because conflating any two of them is this model's classic bug:

  • Quantity — how many units one source delivers, stored on the provision; resizing mutates the same row. (Today every checkout confers quantity 1, so the plumbing is exercised only by grants.)
  • Provision count — how many sources deliver to the pool; each subscription, purchase, or grant is its own provision, and cross-source accumulation is intentional.
  • Per-unit — whether one rule's value multiplies by quantity (within a single provision only).
  • Stacking — how contributions from different provisions fold into one limit: additive sums, maximum keeps the largest. Each contribution applies its own rule's policy against the running total, so mixing policies on one resource key gives order-dependent results — a known trap, tracked as a bug candidate in status/issues.md under the entitlements ledger.

And the rest:

  • Ledger vs delivery vs position — grants (decree history), provisions (what delivers now), occupancy (which ladder rungs — the ladders card). Three layers; UI must join, never shortcut.
  • Rule shape — the key's kind decides boolean vs numeric, which decides the materialized table consumers read.
  • Admitted vs materialized rule types — the schema's CHECK admits four types, but only boolean and limit materialize; quota and credit are reserved (their intended machinery — periodic reset, spendable balance — is unbuilt), the UI never offers them, and the materializer skips any that direct SQL might create.
  • Definition vs usage — limits say what is allowed; usage counters say what is consumed; the comparison happens at the consumer (invariant 13).
  • Conferral snapshot vs current catalog — a provision freezes its product and set at conferral; when the catalog changes shape under a live provision, core.align_conferral_shape reconciles its ladder occupancy — the frozen product and set on the provision itself are never rewritten.