App State Machine & Lifecycle Enforcement
The application state machine governs transitions between lifecycle states, ensuring consistency and enforcing business rules across the platform.
Overview
Each application progresses through a fixed set of server-side states managed by
pkg/v2/domain/app.State. Marketplace services build an
aggregate.AppAggregate around that stateful object and call the lifecycle
state machine to validate mutations. That coordination keeps lifecycle hooks in
sync with the persisted state while preventing invalid transitions.
State Definitions
Applications can only exist in four states today—the additional Installing,
Running, Unhealthy, or Failed concepts mentioned in earlier design docs
were never implemented in the Go domain model.
Code Reference: pkg/v2/domain/app/state.go
Implemented States
StateUnregistered → StateRegistered → StateInstalled → StateUninstalled
↑ ↓
└─────────────── re-registration ─────┘
- StateUnregistered – No persisted app data exists.
- StateRegistered – Manifest/certificate were validated and stored, but the app has not been installed yet.
- StateInstalled – The app was installed successfully and can receive traffic. Marketplace services treat this as the “running” state.
- StateUninstalled – The app was installed previously but has been removed.
From here it can be deregistered back to
StateUnregisteredor reinstalled.
State.CanTransitionTo and the lifecycle validators enforce the diagram above.
Lifecycle Validators
lifecycle.NewStateMachine() wires a validator for each target state and keeps
those validators deliberately lightweight:
registerValidatorverifies a certificate exists and is currently valid and UI apps provide at least one asset. It stampsRegisteredAt/UpdatedAt.installValidatoronly checks that the current state isStateRegisteredbefore stampingInstalledAt. Dependency, settings, and route validation are handled by the marketplace service beforehand.uninstallValidatorrequiresStateInstalledbefore settingUninstalledAt.deregisterValidatorensures the app is registered (but not installed) before allowing cleanup.
Validators never call external systems. Permission checks, dependency analysis, route registration, and Docker orchestration are all performed by the marketplace service before or after the state transition runs.
Post-Transition Actions
There is no catch-all app.state_changed event. The marketplace service
publishes the concrete events defined in
pkg/v2/domain/event/app_events.go (app.registered, app.installed,
app.uninstalled, app.deregistered), and downstream components subscribe to
those specific topics. Cache invalidation or OpenFGA tuple updates are also
performed explicitly by the marketplace service rather than by the state
machine itself.
SDK Coordination
The Node.js SDK still models richer client-side phases (e.g. “installing”) so
it can orchestrate hooks, but when it communicates with the host it only sees
StateRegistered, StateInstalled, StateUninstalled, and StateUnregistered.
Marketplace.Subscribe sends AppStateResponse messages with the persisted
state—there is no STATE_CHANGED control message on the gRPC stream—so SDKs
should map their local states onto those four host states and treat
StateInstalled as “running”.
Allowed Transitions
app.State.CanTransitionTo confines transitions to the following set:
StateUnregistered → StateRegistered
StateRegistered → StateInstalled
StateRegistered → StateUnregistered
StateInstalled → StateUninstalled
StateUninstalled → StateRegistered
StateUninstalled → StateUnregistered
Self-transitions are also permitted for idempotency. Any other combination (for
example StateInstalled → StateRegistered) returns an
InvalidStateTransitionError from stateMachine.ValidateTransition.
Failure Handling
If installation or uninstallation fails inside the marketplace service, it surfaces the error and the aggregate stays in its previous state. There are no persisted “installing”, “failed”, or “unhealthy” markers—operational tooling relies on the emitted lifecycle events and Docker orchestration health checks.