# 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'`, `key = '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. Every one of those rows SHALL take the identifier the database generates for it; provisioning SHALL NOT supply, compute, or pin an id. The only `key` provisioning writes is the pool's `default` constant: the personal organization and its workspace SHALL be created with `key = NULL`, because a key is never derived from a person's data (`ui-vocabulary`). That constant is a declarative address for seeds, configuration files, and scripts; the console SHALL resolve an organization's default pool by the type predicate `pool_type = 'default'` and SHALL NEVER resolve it by its key. #### 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 `key = NULL` - **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, with `key = NULL` - **AND** the system SHALL create a `resource_pools` record with `pool_type = 'default'`, `key = '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: The console resolves the default pool by its type, not its key - **WHEN** any console code path resolves an organization's default resource pool - **THEN** the query SHALL select on `pool_type = 'default'` scoped to the organization - **AND** no console code path SHALL select a pool on `key = 'default'` #### 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 SHALL carry no `key`. A key is never derived from a person's data (`ui-vocabulary`, `docs/identifiers.md` §4), so nothing is derived from the username, no collision handling exists, and organization names are not unique. #### Scenario: Name derived from display name - **WHEN** a personal organization is created for a user whose display name is "Carlos" - **THEN** the organization SHALL be named "Carlos's Organization" - **AND** its `key` SHALL be NULL #### Scenario: Two members with the same display name collide in neither column - **WHEN** two users whose display name is "Dara" each sign up - **THEN** both organizations SHALL be named "Dara's Organization" and both SHALL carry a NULL key, and neither insert SHALL fail ### 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 = `, `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 := )`. `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 = ` (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 #### 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 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