Add default_plan_ladder_id with a forward data migration and update the runtime to resolve the ladder's rank-0 tier at use-time. Regenerate sqlc, update auto-provisioning, ReapplyDefaultsForPool, operator UI and tests; add GetTierByLadderRank and pool/provision query helpers. Add a CSP-safe confirm-action modal and wire operator actions to it. Close plan-sole-writer safety gaps and serialize IssueGrant with a FOR UPDATE pool lock to prevent ladder races.
12 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 default grant for the rank-0 tier of that ladder, a pool provision, 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 resolve the product's
entitlement_set_idfrombilling.products - AND the system SHALL create a
grantsrecord withproduct_idset to the resolved rank-0 product,entitlement_set_idresolved from the product,granted_by_person_id = NULL,grant_reason = 'default',status = 'active', andquantity = 1 - AND the system SHALL create a
pool_provisionsrecord linking the grant to the organization's default resource pool with the resolvedentitlement_set_id - AND the system SHALL materialize entitlements on the pool
- AND all records (including grant, provision, and materialized entitlements) 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. 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_tiers, invokeentitlements.CreateGrantInTxwithproduct_id = <rank-0 product>,grant_reason = 'default',quantity = 1, and record apool_provision_transitionsrow withtransition_typedetermined by the prior ladder position (initiateif the pool was off-ladder,downgradeif the pool was at a higher rank). - If
default_plan_ladder_idis NULL: record a singlepool_provision_transitionsrow withtransition_type = 'end',to_rank = NULL, and return. No grant, provision, or ladder row SHALL be created.
The primitive SHALL operate within the caller's transaction. It SHALL be idempotent with respect to a pool that already has an active default-sourced provision on the ladder (in which case it SHALL be a no-op and return the existing provision without recording a new transition).
Scenario: Re-apply with configured default issues a grant
- 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'andproduct_id = <public-tier>(resolved from the ladder's rank-0 tier) - AND a new
pool_provisionsrow SHALL be inserted withstatus = 'active' - AND a new
pool_provision_laddersrow SHALL be inserted attaching the pool tocoreat rank 0 - AND a
pool_provision_transitionsrow SHALL be recorded 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 records end-transition only
- WHEN
ReapplyDefaultsForPool(ctx, tx, pool_id)is invoked for a pool whose org hasdefault_plan_ladder_id = NULL - AND the pool was previously at a non-null rank on a ladder
- THEN no grant, provision, or ladder row SHALL be created
- AND a single
pool_provision_transitionsrow SHALL be recorded withtransition_type = 'end',to_rank = NULL - AND the primitive SHALL return without error
Scenario: Re-apply is idempotent when default already active
- WHEN
ReapplyDefaultsForPool(ctx, tx, pool_id)is invoked for a pool that already has an active default-sourced provision at the rank-0 tier of the configured ladder - THEN no grant, provision, ladder, or transition row SHALL be inserted
- AND the primitive SHALL return the existing active provision
Scenario: Re-apply inherits caller transaction
- WHEN the caller invokes
ReapplyDefaultsForPoolinside an existing transaction and the caller later rolls back - THEN the grant, provision, ladder, and transition rows created by the primitive SHALL be rolled back as well
Requirement: Default-grant AutoProvision path creates ladder attachment
When AutoProvision creates the org-creation-time default grant (existing behavior), the system SHALL resolve the rank-0 tier of the org type's default_plan_ladder_id to obtain the product, create the grant, additionally create a pool_provision_ladders row attaching the pool to the configured ladder at rank 0, and record a pool_provision_transitions row with transition_type = 'initiate', actor_type = 'system', and reason = 'auto-provisioning on org creation'. This preserves existing behavior for orgs with no default plan configured (no grant, no ladder attachment, no transition row).
Scenario: Org creation with plan default creates ladder attachment
- WHEN a new user authenticates via OIDC for the first time
- AND the
personalorg type hasdefault_plan_ladder_idset to laddercore(whose rank-0 tier is a plan product at rank 0) - THEN the system SHALL create all existing governance structures and the default grant for the resolved rank-0 product as previously specified
- AND the system SHALL additionally create a
pool_provision_laddersrow withplan_ladder_id = <core>,rank = 0 - AND the system SHALL record a
pool_provision_transitionsrow withtransition_type = 'initiate',actor_type = 'system'
Scenario: Org creation with NULL default remains unchanged
- WHEN a new user authenticates and the
personalorg type hasdefault_plan_ladder_id = NULL - THEN no grant, provision, ladder attachment, or transition row SHALL be created
- AND existing governance structures SHALL still be created as previously specified