Files
cgalo5758 ad7a219adf Enforce schema and boot invariants
Enforce 10j's verified gaps (schema-hardening change):

- Migration 00010: partial unique indexes for one default pool and one
  primary assignment per workspace, plus CHECKs pinning
  pool/provider/subscription vocabularies and provider lifecycle
  timestamps.
- Workspace creation shares a transactional provisioning function;
  extension validates its target pool; last-tier deletion of a defaulted
  ladder is guarded; signup completes plan-less on a broken ladder.
- Boot asserts integration slug parity and validates declared config
  enums; Stripe invoice amounts are range-checked; domain cancellation
  runs a final evidence probe; rule authoring is additive-only.
2026-08-22 18:02:46 -05:00

17 KiB

auto-provisioning

Purpose

Defines the automatic provisioning of governance structures (person, organization, workspace, billing account) when a user first authenticates via OIDC.

Requirements

Requirement: Auto-provision governance structures on first login

The system SHALL create a complete set of governance structures when a user authenticates for the first time. The following records SHALL be created within a single database transaction: a users record, a persons record, a personal organizations record (with org_type = 'personal'), an org_members record with the owner system role, a default workspaces record, a default resource_pools record (with pool_type = 'default' and is_auto_managed = true), a pool_assignments record linking the workspace to the pool (with is_primary = true), a default billing.accounts record (with status = 'active') belonging to the organization, and — if the org type has a default_plan_ladder_id configured — a system-authored default grant for the rank-0 tier of that ladder, enacted via entitlements.confer (no bespoke provisioning path; see the entitlement-conferral capability for the primitive's full contract), and materialized entitlements on the pool.

Scenario: First-time OIDC authentication

  • WHEN a user completes OIDC authentication and no users record exists for their OIDC subject
  • THEN the system SHALL create a users record from the OIDC claims
  • AND the system SHALL create a persons record linked to that user
  • AND the system SHALL create an organizations record with org_type = 'personal' and slug derived from the username
  • AND the system SHALL create an org_members record linking the person to the organization with the owner system role
  • AND the system SHALL create a workspaces record named "default" in that organization
  • AND the system SHALL create a resource_pools record with pool_type = 'default', is_auto_managed = true, belonging to the organization
  • AND the system SHALL create a pool_assignments record with is_primary = true linking the workspace to the resource pool
  • AND the system SHALL create a billing.accounts record named "Default" with status = 'active' belonging to the organization
  • AND all records SHALL be created within a single database transaction

Scenario: First-time OIDC authentication with default plan configured

  • WHEN a user completes OIDC authentication and no users record exists for their OIDC subject
  • AND the personal org type has a non-NULL default_plan_ladder_id
  • THEN the system SHALL create all governance structures as in the first-time scenario
  • AND the system SHALL resolve the rank-0 tier of the configured ladder via billing.plan_ladder_tiers to obtain the default product
  • AND the system SHALL insert a grants record with product_id set to the resolved rank-0 product, granted_by_person_id = NULL, grant_reason = 'default', status = 'active', and quantity = 1 — system-authored per the default_iff_system_authored invariant, and carrying no entitlement_set_id of its own (that column is retired from grants; the set is resolved from the product)
  • AND the system SHALL invoke entitlements.confer with the organization's default resource pool, the resolved product, and p_grant_id set to the new grant's id — the same call every other conferral caller makes, with no separate provisioning path
  • AND confer SHALL derive entitlement_set_id from the product, insert the pool_provisions row (with product_id and entitlement_set_id denormalized), insert a pool_provision_ladders row attaching the pool to the configured ladder at rank 0 (the rank-0 tier product is position-bearing), and record an initiate pool_provision_transitions row — auto-provisioning performs no separate ladder-attachment step
  • AND the system SHALL materialize entitlements on the pool using the provision_id confer returns
  • AND all records (the grant, and everything confer creates) SHALL be created within the same database transaction

Scenario: First-time OIDC authentication without default plan configured

  • WHEN a user completes OIDC authentication and no users record exists for their OIDC subject
  • AND the personal org type has default_plan_ladder_id = NULL
  • THEN the system SHALL create all governance structures as in the first-time scenario
  • AND the system SHALL NOT create any grants, pool provisions, or materialized entitlements

Scenario: Transaction atomicity

  • WHEN any step of the auto-provisioning process fails (e.g., database error during pool creation or entitlement materialization)
  • THEN the entire transaction SHALL be rolled back
  • AND no partial governance structures or entitlements SHALL exist in the database

Scenario: Returning user login does not re-provision

  • WHEN a user completes OIDC authentication and a users record already exists for their OIDC subject
  • THEN the system SHALL NOT create any new organizations, memberships, workspaces, resource pools, pool assignments, billing accounts, or grants

Requirement: Personal organization naming

The personal organization SHALL derive its name from the user's display name (e.g., "Carlos's Organization") and its slug from the username. If the derived slug conflicts with an existing organization slug, the system SHALL append a numeric suffix to make it unique.

Scenario: Slug derived from username

  • WHEN a personal organization is created for a user with username "cgalo"
  • THEN the organization SHALL have slug = 'cgalo'

Scenario: Slug conflict resolution

  • WHEN a personal organization is created but the derived slug already exists
  • THEN the system SHALL append a numeric suffix (e.g., cgalo-2) and retry until a unique slug is found

Requirement: Session populated with governance context

After auto-provisioning (or on returning user login), the session SHALL carry: person_id (UUID string), org_id (UUID string — the personal organization), and workspace_id (UUID string — the default workspace), in addition to the existing auth session fields (authenticated, id_token, oidc_subject, email, name, username, roles).

Scenario: Session after first login

  • WHEN auto-provisioning completes for a new user
  • THEN the session SHALL contain person_id set to the newly created person's UUID
  • AND the session SHALL contain org_id set to the newly created personal organization's UUID
  • AND the session SHALL contain workspace_id set to the newly created default workspace's UUID

Scenario: Session after returning login

  • WHEN a returning user authenticates
  • THEN the session SHALL contain person_id, org_id, and workspace_id loaded from the existing database records
  • AND org_id SHALL be the user's personal organization (single-org experience for now)

Requirement: Progressive disclosure in UI

Solo users (single person, single organization, single workspace) SHALL see a simplified interface that does not expose organizational machinery. The UI SHALL show user-relevant information (e.g., "Your Sites") without requiring the user to navigate through org/workspace hierarchies.

Scenario: Solo user sees simplified view

  • WHEN a user with one organization and one workspace views the index page
  • THEN the page SHALL display their content directly (e.g., sites list) without an organization or workspace selector

Scenario: Operator panel shows full structure

  • WHEN an operator views the admin panel
  • THEN the panel SHALL display organizations, members, workspaces, and role assignments for administrative visibility

Requirement: ReapplyDefaultsForPool primitive

The system SHALL expose an entitlements.ReapplyDefaultsForPool(ctx, tx, pool_id) primitive that re-applies the owning organization's configured default plan to an existing resource pool via the same entitlements.confer primitive every other caller uses — no bespoke provisioning path. The primitive SHALL look up the pool's owning organization, read the org's organization.org_types.default_plan_ladder_id, and:

  • If default_plan_ladder_id is non-NULL: resolve the rank-0 tier of that ladder via billing.plan_ladder_tiers to obtain the product, insert a fresh grants row (grant_reason = 'default', granted_by_person_id = NULL, product_id = <rank-0 product>, quantity = 1) — every invocation mints a new grant row, whether or not the pool already holds a live default-sourced position — and invoke entitlements.confer(pool_id, product_id, p_grant_id := <new grant id>). confer derives the outcome: it is a no-op (decree duplication — the new grant stands as ledger while delivery is already true) when the pool already has a non-ended default-sourced provision of this same product; otherwise it supersedes any incumbent on the ladder and records the appropriate initiate/upgrade/downgrade/transfer transition, exactly as it would for any other caller.
  • If default_plan_ladder_id is NULL: the primitive SHALL confer nothing, end nothing, and return a no-op result. Applying a default is a floor operation; removing orgs from a former default is never a side effect of re-application — it is owned by the org-type default-change commit's explicit outgoing-default disposition (org-types capability), which ends the incumbent position via end_conferral resolved by its grant source.

The primitive SHALL NOT rely on a caller-supplied "prior position" discriminator to decide whether to mint and re-attach: because confer resolves liveness structurally from the position machinery itself, a reapply invoked after the pool's default attachment has already ended (by an operator revoke, a prior reapply, or a supersession) takes the identical path as any other invocation — a fresh grant is minted, and confer either re-attaches (finding no non-ended incumbent) or reports the decree-duplication no-op (finding one).

Re-conferral of a since-retired rank-0 product remains legal. If the rank-0 product's lifecycle_status has moved to retired between two reapplications, confer's lifecycle gate (entitlement-conferral capability) admits the re-conferral via the source-class predicate defined for grant_reason = 'default' grants: any prior default grant of this product to this pool satisfies the gate, not the literal same grant row — since each reapply mints a fresh row, the same-source-row form the gate uses for other callers would never be met by construction. This is continuity of a standing default, not new commercial intake: a retired rank-0 product remains re-conferrable to a pool that has held it before, even though it is no longer sellable to a pool that has not.

Whenever the primitive's conferral ends or replaces the pool's active provision (supersession inside confer), the resulting materialized numeric_entitlements SHALL be consistent with the resulting set of active provisions, with no stale contributions remaining from ended provisions. The primitive SHALL operate within the caller's transaction.

Branch-free interaction with supersession by a paid plan. If a pool's default-sourced rank-0 position is later superseded by a subscription conferring the same product at the same rank (an org that subscribes to the plan its default grant already delivers), that hand-off is recorded as a transfer transition (cross-ref plan-transitions) and is enacted entirely by the subscribing caller's own confer call. ReapplyDefaultsForPool requires no special case for it, before or after the hand-off has occurred, because liveness and source resolution are structural and read fresh on every call, never cached or caller-tracked.

Scenario: Re-apply with configured default mints a fresh grant and confers

  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool whose org has default_plan_ladder_id set to the core ladder (whose rank-0 tier is the Public Tier product)
  • AND the pool is currently off the core ladder
  • THEN a new grants row SHALL be inserted with grant_reason = 'default', granted_by_person_id = NULL, and product_id = <public-tier> (resolved from the ladder's rank-0 tier)
  • AND entitlements.confer SHALL be invoked with the new grant as source
  • AND confer SHALL insert a new pool_provisions row with status = 'active'
  • AND confer SHALL insert a new pool_provision_ladders row attaching the pool to core at rank 0
  • AND confer SHALL record a pool_provision_transitions row with transition_type = 'initiate' (or 'downgrade' if the pool was previously at a higher rank)
  • AND entitlements SHALL be re-materialized on the pool

Scenario: Re-apply with NULL default is a no-op

  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool whose org has default_plan_ladder_id = NULL
  • THEN no grant, provision, ladder, or transition row SHALL be created
  • AND no live position SHALL be ended — even a live default-sourced position survives (moving orgs off a former default requires the org-type default-change commit's explicit disposition, never a re-apply side effect)
  • AND the primitive SHALL return a no-op result without error

Scenario: Re-apply after the default attachment has already ended still mints a fresh grant

  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool whose prior default-sourced attachment at the rank-0 tier of default_plan_ladder_id has already been ended (by an operator revoke, a prior reapply, or a supersession)
  • AND default_plan_ladder_id is still non-NULL
  • THEN a new grants row SHALL be inserted with grant_reason = 'default' (the primitive does not suppress grant creation based on any caller-supplied prior-position discriminator)
  • AND entitlements.confer SHALL be invoked with the new grant as source, finding no non-ended incumbent on the ladder to supersede
  • AND confer SHALL insert a fresh pool_provisions row, a pool_provision_ladders row at rank 0, and record a pool_provision_transitions row with transition_type = 'initiate'
  • AND entitlements SHALL be re-materialized on the pool

Scenario: Re-apply mints a grant but no-ops at the provision layer when the default is already live

  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool that already has a non-ended default-sourced provision of the resolved rank-0 product
  • THEN a new grants row SHALL still be inserted with grant_reason = 'default' (the ledger fact is recorded regardless of delivery state)
  • AND entitlements.confer SHALL return the noop outcome naming the incumbent provision
  • AND no new pool_provisions, pool_provision_ladders, or pool_provision_transitions row SHALL be inserted
  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool whose org's configured rank-0 product now has lifecycle_status = 'retired'
  • AND this pool has previously held a grant_reason = 'default' grant conferring this same product (from an earlier reapply or the initial auto-provisioning grant)
  • THEN a new grants row SHALL still be inserted with grant_reason = 'default'
  • AND entitlements.confer's lifecycle gate SHALL admit the re-conferral via the source-class predicate (any prior default grant of this product to this pool), not the same-source-row predicate applied to other callers
  • AND the reapply SHALL proceed exactly as it would for a published product

Scenario: Re-apply inherits caller transaction

  • WHEN the caller invokes ReapplyDefaultsForPool inside an existing transaction and the caller later rolls back
  • THEN the grant row, and everything entitlements.confer created or ended on its behalf — including any re-materialization of the pool's entitlements — SHALL be rolled back as well

Requirement: First-login provisioning survives a missing bottom tier

When first-login provisioning cannot resolve the default plan ladder's bottom (rank 0) tier, it SHALL complete the signup without conferring a plan — user, person, organization, workspace, default pool, and primary assignment are still created — and SHALL log an unmistakable error-level alarm naming the broken ladder, instead of failing the signup with a server error. A plan-less organization provisioned this way SHALL be repairable through the existing reapply-defaults path once the ladder is fixed.

Scenario: Signup completes plan-less when the ladder is broken

  • WHEN a new user completes their first login while the default org type's ladder has no rank-0 tier
  • THEN the account and all governance structures are created, the user reaches the app logged in, no plan is conferred, and the server log carries an error naming the ladder

Scenario: Healthy ladder behavior unchanged

  • WHEN the default ladder has its bottom tier
  • THEN first-login provisioning confers it exactly as before