---
id: STATE-PFM-OPS-006
title: Workspace application operational state
product_area: platform
module: operations
revision: 1
status: draft
object: DOM-PFM-OPS-004
design_intent: confirmed
origin: >-
  services-platform:docs/proposals/application-access-and-entitlements.md#6.2 and #10 (built and accepted by
  Andrew 2026-10-15)
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
states:
  - name: never provisioned
    meaning: The workspace has no operational row for the application.
  - name: provisioning
    meaning: A preparation-start write exists; participants may be executing or the process may have stopped.
  - name: provisioning failed
    meaning: Preparation recorded a participant failure with an explanation.
  - name: enabled
    meaning: The operational record says the application is ready.
  - name: suspended
    meaning: Staff deliberately stopped operation while retaining data and configuration.
transitions:
  - from: never provisioned
    to: provisioning
    trigger: A grant or seed prepares an application without an operational row.
    guard: The caller found no operational record.
    side_effects:
      - Commit the preparation-start row before running participants.
    who: the grant capability on behalf of Rhythm staff
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/enablement/provision-tenant-application.integration.spec.js#says
      which participant could not run, in the reason an operator reads
  - from: never provisioned
    to: enabled
    trigger: Core activation or backfill inserts a missing always-available row.
    guard: The insert does not replace an existing row.
    side_effects:
      - Stamp first enablement.
      - Preserve all existing operational rows.
    who: 'the provisioning job’s last step, or the deploy'
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/persistence/tenant-applications.integration.spec.js#leaves
      a suspended row suspended, so a redeploy never undoes an operational decision
  - from: provisioning
    to: enabled
    trigger: 'All participants finish, including an empty participant list.'
    guard: The outcome writer makes no starting-state comparison; database constraints still apply.
    side_effects:
      - Stamp first enablement if absent.
      - Clear the state explanation.
    who: the preparation capability
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/enablement/provision-tenant-application.integration.spec.js#says
      which participant could not run, in the reason an operator reads
  - from: provisioning
    to: provisioning failed
    trigger: The participant runner catches its first error.
    guard: The outcome writer makes no starting-state comparison; database constraints still apply.
    side_effects:
      - Retain a participant explanation limited to one thousand characters.
      - Preserve the commercial decision.
    who: the preparation capability
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/enablement/provision-tenant-application.integration.spec.js#says
      which participant could not run, in the reason an operator reads
  - from: provisioning failed
    to: provisioning
    trigger: Authorized staff retry an application recorded as failed preparation.
    guard: The pre-write check finds provisioning_failed and the route requires licensing management.
    side_effects:
      - Run participants again.
      - Request cache refresh after the state write.
      - The route then attempts security-history insertion.
    who: Rhythm staff
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/enablement/provision-tenant-application.integration.spec.js#refuses
      every move for an application the tenant has never been provisioned for
  - from: enabled
    to: suspended
    trigger: Authorized staff suspend a currently enabled application with a reason.
    guard: The pre-write check finds enabled and validates the reason.
    side_effects:
      - Record the reason and staff attribution.
      - Request cache refresh.
      - Attempt security-history insertion.
    who: Rhythm staff
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/e2e/licensing.e2e.spec.js#refuses a suspension with no reason,
      with 400
  - from: suspended
    to: enabled
    trigger: Authorized staff resume an application recorded as suspended.
    guard: The pre-write check finds suspended.
    side_effects:
      - Clear the reason.
      - Preserve first enablement.
      - Request cache refresh.
      - Attempt security-history insertion.
    who: Rhythm staff
    provenance: test
    source: >-
      services-platform:modules/licensing/tests/integration/services/persistence/tenant-applications.integration.spec.js#keeps
      the instant it was first enabled through a suspension and a resumption
invalid:
  - from: enabled
    to: provisioning
    why: The database rejects provisioning when first enablement is present.
  - from: suspended
    to: provisioning
    why: The retry operation admits failed preparation rather than a deliberate suspension.
  - from: provisioning failed
    to: suspended
    why: Suspension checks for an enabled starting state.
  - from: provisioning
    to: suspended
    why: >-
      Staff suspension and retry both refuse an in-progress preparation, leaving interruption without a
      recovery action.
  - from: never provisioned
    to: suspended
    why: Staff actions cannot operate on an absent operational row.
concurrency: >-
  The persistence writer locks each row and resolves insertion conflicts, but service checks precede that
  lock. Participants execute outside database transactions, and outcome writes do not compare their starting
  state. Concurrent actions can repeat preparation or replace a later decision.
invariants:
  - INV-DOM-PFM-OPS-004-01
  - INV-DOM-PFM-OPS-004-02
  - INV-DOM-PFM-OPS-004-03
  - INV-DOM-PFM-OPS-004-05
  - INV-DOM-PFM-OPS-004-06
evidence:
  - >-
    services-platform at ad3f6a6: modules/licensing/services/enablement/provision-tenant-application.ts,
    services/persistence/tenant-applications.ts:setTenantApplicationState, db/tenant-applications.table.ts;
    docs/licensing.md §3, §6.
  - >-
    codex examination 2026-10-23: checked the pinned source anchors, planning references and enforcement
    boundaries; unresolved limits are recorded in module findings.
---

# Workspace application operational state

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> never_provisioned
  never_provisioned --> provisioning: prepare missing application
  never_provisioned --> enabled: insert missing core row
  provisioning --> enabled: preparation completes
  provisioning --> provisioning_failed: participant error
  provisioning_failed --> provisioning: retry failed preparation
  enabled --> suspended: reasoned suspension
  suspended --> enabled: resumption
```

## Transitions Explained

A never-prepared application has no row. Preparation records provisioning, runs declared participants, and records enabled or failed preparation. Core activation inserts enabled rows directly. Staff suspend enabled applications, resume suspended applications and retry failed preparation, with refresh and history following the committed move.

## What Cannot Happen

Staff controls refuse an absent record or the wrong initial state. The database prohibits provisioning with recorded first enablement. Backfill does not replace existing state. The lower-level writer is not a complete transition validator, and outcome writes do not protect against a newer state.

## Concurrency and Timing

Participants run without an open database transaction. A stopped process can leave provisioning indefinitely because no reconciler or staff retry accepts that state. Row locking protects an individual write rather than the whole check-and-act sequence.
