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.
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
usersrecord exists for their OIDC subject - THEN the system SHALL create a
usersrecord from the OIDC claims - AND the system SHALL create a
personsrecord linked to that user - AND the system SHALL create an
organizationsrecord withorg_type = 'personal'andslugderived from the username - AND the system SHALL create an
org_membersrecord linking the person to the organization with theownersystem role - AND the system SHALL create a
workspacesrecord named "default" in that organization - AND the system SHALL create a
resource_poolsrecord withpool_type = 'default',is_auto_managed = true, belonging to the organization - AND the system SHALL create a
pool_assignmentsrecord withis_primary = truelinking the workspace to the resource pool - AND the system SHALL create a
billing.accountsrecord named "Default" withstatus = '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
usersrecord exists for their OIDC subject - AND the
personalorg type has a non-NULLdefault_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_tiersto obtain the default product - AND the system SHALL insert a
grantsrecord withproduct_idset to the resolved rank-0 product,granted_by_person_id = NULL,grant_reason = 'default',status = 'active', andquantity = 1— system-authored per thedefault_iff_system_authoredinvariant, and carrying noentitlement_set_idof its own (that column is retired fromgrants; the set is resolved from the product) - AND the system SHALL invoke
entitlements.conferwith the organization's default resource pool, the resolved product, andp_grant_idset to the new grant's id — the same call every other conferral caller makes, with no separate provisioning path - AND
conferSHALL deriveentitlement_set_idfrom the product, insert thepool_provisionsrow (withproduct_idandentitlement_set_iddenormalized), insert apool_provision_laddersrow attaching the pool to the configured ladder at rank 0 (the rank-0 tier product is position-bearing), and record aninitiatepool_provision_transitionsrow — auto-provisioning performs no separate ladder-attachment step - AND the system SHALL materialize entitlements on the pool using the
provision_idconferreturns - AND all records (the grant, and everything
confercreates) 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
usersrecord exists for their OIDC subject - AND the
personalorg type hasdefault_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
usersrecord 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_idset to the newly created person's UUID - AND the session SHALL contain
org_idset to the newly created personal organization's UUID - AND the session SHALL contain
workspace_idset 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, andworkspace_idloaded from the existing database records - AND
org_idSHALL 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_idis non-NULL: resolve the rank-0 tier of that ladder viabilling.plan_ladder_tiersto obtain the product, insert a freshgrantsrow (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 invokeentitlements.confer(pool_id, product_id, p_grant_id := <new grant id>).conferderives 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 appropriateinitiate/upgrade/downgrade/transfertransition, exactly as it would for any other caller. - If
default_plan_ladder_idis 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-typescapability), which ends the incumbent position viaend_conferralresolved 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 hasdefault_plan_ladder_idset to thecoreladder (whose rank-0 tier is the Public Tier product) - AND the pool is currently off the
coreladder - THEN a new
grantsrow SHALL be inserted withgrant_reason = 'default',granted_by_person_id = NULL, andproduct_id = <public-tier>(resolved from the ladder's rank-0 tier) - AND
entitlements.conferSHALL be invoked with the new grant as source - AND
conferSHALL insert a newpool_provisionsrow withstatus = 'active' - AND
conferSHALL insert a newpool_provision_laddersrow attaching the pool tocoreat rank 0 - AND
conferSHALL record apool_provision_transitionsrow withtransition_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 hasdefault_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 ofdefault_plan_ladder_idhas already been ended (by an operator revoke, a prior reapply, or a supersession) - AND
default_plan_ladder_idis still non-NULL - THEN a new
grantsrow SHALL be inserted withgrant_reason = 'default'(the primitive does not suppress grant creation based on any caller-supplied prior-position discriminator) - AND
entitlements.conferSHALL be invoked with the new grant as source, finding no non-ended incumbent on the ladder to supersede - AND
conferSHALL insert a freshpool_provisionsrow, apool_provision_laddersrow at rank 0, and record apool_provision_transitionsrow withtransition_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
grantsrow SHALL still be inserted withgrant_reason = 'default'(the ledger fact is recorded regardless of delivery state) - AND
entitlements.conferSHALL return thenoopoutcome naming the incumbent provision - AND no new
pool_provisions,pool_provision_ladders, orpool_provision_transitionsrow SHALL be inserted
Scenario: Re-apply of a since-retired rank-0 product remains legal
- WHEN
ReapplyDefaultsForPool(ctx, tx, pool_id)is invoked for a pool whose org's configured rank-0 product now haslifecycle_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
grantsrow SHALL still be inserted withgrant_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
ReapplyDefaultsForPoolinside an existing transaction and the caller later rolls back - THEN the grant row, and everything
entitlements.confercreated 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