- 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
11 KiB
title, audience, summary
| title | audience | summary | |
|---|---|---|---|
| Resource Pools |
|
The per-organization container where delivered capabilities live: pool identity, workspace assignments, the two resolution directions, and pool-grain usage counters. |
Resource Pools
Purpose
A resource pool is the container where everything conferral delivers to an organization actually lands: provisions attach to a pool, the materializer writes the pool's effective entitlements, and consumers draw against pool-grain counters. The pool is also the join point between two worlds that do not reference each other in the schema: governance (an organization and its workspaces — the working surfaces where sites and services live) and commerce (billing accounts and subscriptions). A pool belongs to an organization and has no billing linkage; money reaches it only through provisions. (The design wanted the opposite parentage — pool under billing account — so this orthogonality is as-built, not as-designed; tracked in status/issues.md.)
Every resolution in this model runs in one of two directions, and they are different idioms with different failure modes. The delivery side resolves organization → default pool: conferral, Stripe reconciliation, and grant expiry all ask "which pool receives this?". The consumption side resolves workspace → primary assignment → pool: the domain-claim gate and the FedWiki site-limit check ask "which pool does this workspace draw from?". Today both directions collapse onto the same single pool, because every organization gets exactly one — the auto-managed default pool minted at signup — but the schema admits many, and several code paths already guard defensively against that future.
Usage lives at pool grain: one counter row per (pool, resource key), so sibling workspaces of one organization share one bucket. Acquiring quota is a single atomic, limit-guarded statement — concurrent acquisitions cannot overshoot.
Vocabulary reused from the Entitlements card: provision, conferral, materializer, resource key, numeric and boolean entitlements, usage counter, grant. From Plan Ladders and Transitions: ladder positions and occupancy. From Payments and Billing: billing account, subscription. An organization type classifies organizations and carries the default-plan pointer (the identity model owns it; today every organization is the personal type). "Tracked in status/issues.md" below always means the model-documentation ledger there, not a database table.
Where this lives
Database objects sit in the core schema, defined in internal/db/migrations/00001_init.sql.
| Piece | What it is | Where |
|---|---|---|
core.resource_pools |
Pool identity: organization, name, per-org-unique slug, pool_type (only default is ever written), is_auto_managed, and a status that in practice is always active |
00001_init.sql |
core.pool_assignments |
Workspace-to-pool links with an is_primary flag and their own status column (resolvers require active); a workspace can be assigned to a given pool only once |
00001_init.sql |
core.numeric_entitlement_usage |
Pool-grain usage counters, one row per materialized numeric entitlement, with period-window columns (reserved for the unbuilt quota-reset machinery) | 00001_init.sql |
| Pool queries | Create and fetch pools; GetDefaultPoolByOrgID is the delivery-side resolver (pool_type='default', active, first match); ListResourcePoolsByOrgIDAnyStatus is a display-only read that, unlike the active-only fetchers, does not hide a non-active pool from view |
internal/entitlements/queries/resource_pools.sql |
| Assignment queries | GetPrimaryPoolAssignmentByWorkspace — the consumption-side resolver every consumer uses |
internal/entitlements/queries/pool_assignments.sql |
| Atomic usage counters | AtomicIncrementUsage / AtomicDecrementUsage: one guarded UPDATE resolving the workspace to its primary pool; increment refuses at the limit, decrement stops at zero. ListPoolUsageWithLimitsByPoolID is the read-only counterpart pairing each counter with its materialized limit, for display |
internal/entitlements/queries/numeric_entitlement_usage.sql |
| Signup provisioning | AutoProvision, the only production pool creator: one transaction minting user → person → organization → workspace → pool → assignment → billing account → default grant |
internal/provisioning/provisioning.go |
| Workspace creation | Attaches each new workspace to the org's default pool as primary | internal/server/workspace_partials.go |
| Delivery-side callers | Grant conferral, Stripe reconciliation, grant expiry, and org-type changes all target the default pool | internal/entitlements/conferral.go; internal/fulfillment/reconcile.go; internal/workflows/entitlements/activities.go; internal/server/operator_org_types.go |
| Consumption-side callers | The domain-claim gate and the FedWiki quota reads | internal/server/external_claim_gate.go; internal/integrations/fedwiki/web/api.go |
| Operator enrollment guard | Refuses grant issuance for an organization with more than one pool, and locks the pool row to serialize conferral-mutating flows. Also refuses issuance outright when the organization has zero pools, rendering a breakage warning (missing default pool) in place of the pool panel's neutral empty state and blocking the Issue Grant form with the same reason | internal/server/operator_enrollment.go |
| Org-detail pools panel | Read-only: each pool's status (not just its existence) and its per-resource usage counters (used/limit), so a suspended pool and a quota refusal are both diagnosable from the console | internal/server/operator_enrollment.go; internal/embeds/templates/partials/operator_enrollment.html |
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.
- [db] A pool belongs to exactly one organization, and the schema gives pools no billing-account linkage — funding reaches a pool only through provisions.
- [db] Pool slugs are unique per organization, and a workspace is assigned to a given pool at most once.
- [db] Every organization gets exactly one pool: the auto-managed default pool minted at signup. No other production path creates pools, and no operator pool-management surface exists. A partial unique index (
uq_resource_pools_one_default_per_org) rejects a second default pool for an organization, and a CHECK pinspool_typeto its known values so an unknown type cannot slip past the index. - [db] Every workspace is linked to its organization's default pool as primary, in the same transaction that creates the workspace. A partial unique index (
uq_pool_assignments_one_primary_per_workspace) rejects a second primary assignment for a workspace, so the atomic counter — which debits every pool the workspace's primary assignments resolve to — can match at most one pool. - [app] Delivery targets the default pool: the four canonical conferral paths (grant issuance, Stripe reconciliation, grant expiry, org-type changes) all resolve organization → default pool. One path does not: the operator grant-extend flow confers into the pool named in its URL without resolving or checking the default — harmless while every organization has one pool, and tracked in
status/issues.md. - [app] All consumption resolves workspace → primary active assignment → pool; consumers never enumerate multiple pools.
- [app] Usage is pool-grain: counters key on (pool, resource key), so every workspace of an organization draws down the same row.
- [app] Quota acquisition is atomic and limit-guarded in one statement — the increment's WHERE clause requires usage below limit, so concurrent acquires cannot overshoot; decrement clamps at zero. Callers detect refusal by the statement reporting no row changed — which is also what a missing counter row or a missing primary assignment produces, so "no row changed" means "not allowed", not specifically "at limit".
- [db-code] A usage counter row exists only under a materialized numeric entitlement (foreign keys and a per-entitlement uniqueness reject orphans), and only the materializer creates one — seeded at zero.
- [app] Pool status never leaves
activeas built: nothing writessuspendedorarchived. The delivery-side resolvers filter on pool status, so suspending a pool would sever conferral; the quota paths consult only assignment status, so consumption would keep working — a writer must not be introduced before that cascade is defined (tracked instatus/issues.md). - [app] The pool row is the operator-side serialization lock: the operator revoke path and org-type change enactment take a row lock on the pool before touching ladder positions. Stripe-driven mutation serializes differently — a per-subscription advisory lock inside the reconciler (see the Payments and Billing card).
- [app] Operator grant issuance refuses a multi-pool organization outright rather than guessing which pool to target — an explicit placeholder for the unbuilt multi-pool future.
Dimensions
- Pool grain vs workspace grain — entitlements and usage attach to the pool; workspaces are consumers that resolve into it. Sibling workspaces share one quota bucket.
- Governance scope vs funding scope — a pool lives in an organization; money reaches it only through provisions. The absence of any pool-to-billing link is deliberate.
- Delivery direction vs consumption direction — organization → default pool for delivering; workspace → primary assignment → pool for consuming. Same pool today, different idioms and different failure modes.
- Designed multi-pool vs as-built single-pool — the schema admits N pools and N assignments (the designed
pool_typevaluessharedanddedicatedare unreachable; onlydefaultis written, and no CHECK constrains the column); the code creates exactly one pool and guards the rest — the enrollment refusal guard, and the plan-change path scanning every org pool for a ladder's active position (internal/fulfillment/plan_change.go). - Container vs lock — beyond holding entitlements, the pool row is the per-organization concurrency unit (invariant 11).
- Auto-managed vs operator-managed —
is_auto_managedmarks system-owned pools; still write-only (the column is never read back for display), but no longer contrast-less: the organization-detail pools panel is a read-only operator surface (invariant 3's "no pool-management surface" still holds — the panel creates and edits nothing) showing each pool's status and usage counters.