Files
Christian Galo 751bae7768 Use plan ladder for org defaults
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.
2026-04-27 01:57:17 -05:00

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 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 resolve the product's entitlement_set_id from billing.products
  • AND the system SHALL create a grants record with product_id set to the resolved rank-0 product, entitlement_set_id resolved from the product, granted_by_person_id = NULL, grant_reason = 'default', status = 'active', and quantity = 1
  • AND the system SHALL create a pool_provisions record linking the grant to the organization's default resource pool with the resolved entitlement_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 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. 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, invoke entitlements.CreateGrantInTx with product_id = <rank-0 product>, grant_reason = 'default', quantity = 1, and record a pool_provision_transitions row with transition_type determined by the prior ladder position (initiate if the pool was off-ladder, downgrade if the pool was at a higher rank).
  • If default_plan_ladder_id is NULL: record a single pool_provision_transitions row with transition_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 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' and product_id = <public-tier> (resolved from the ladder's rank-0 tier)
  • AND a new pool_provisions row SHALL be inserted with status = 'active'
  • AND a new pool_provision_ladders row SHALL be inserted attaching the pool to core at rank 0
  • AND a pool_provision_transitions row SHALL be recorded 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 records end-transition only

  • WHEN ReapplyDefaultsForPool(ctx, tx, pool_id) is invoked for a pool whose org has default_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_transitions row SHALL be recorded with transition_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 ReapplyDefaultsForPool inside 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 personal org type has default_plan_ladder_id set to ladder core (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_ladders row with plan_ladder_id = <core>, rank = 0
  • AND the system SHALL record a pool_provision_transitions row with transition_type = 'initiate', actor_type = 'system'

Scenario: Org creation with NULL default remains unchanged

  • WHEN a new user authenticates and the personal org type has default_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