Lifecycle hooks
The internal endpoints the Control Plane calls around a tenant lifecycle — provision, deprovision, plan change, config reload and upgrade.
Hooks let the platform call you at lifecycle moments
Some of an app's work has to happen around a tenant's lifecycle, not in response to a user request — run migrations when a new instance is provisioned, drop data when it is torn down, recompute overrides when a plan changes. You declare these as lifecycle hooks: a spec.lifecycle_hooks block naming the internal endpoints the Control Plane should call at each moment. CP drives the timing; your endpoints do the work.
There are five hook points, each an array (you may register more than one endpoint per moment):
| Hook | CP fires it when… | You typically… |
|---|---|---|
on_provision | A new tenant instance of your app is being created | Run migrations, seed the instance's baseline structure |
on_deprovision | An instance is being torn down | Delete or archive that tenant's data per your retention policy |
on_plan_change | The org's subscription plan changes | Recompute feature overrides / quotas after an upgrade or downgrade |
on_config_reload | An operator saves a settings change | Re-read the affected config without a redeploy |
on_upgrade | Your app is upgraded to a new version | Run post-upgrade data migrations |
Each entry is small: a required description (what the hook does and why a reviewer cares) and an optional endpoint CP POSTs to. An endpoint is usually your own internal route (/v1/<app_code>/internal/..., x-api-key-gated — the tier CP uses for all service-to-service calls, see Auth & the five gates); it can also delegate to another app as app://<target>/<path>, or name an absolute https:// URL, resolved the same way as the manifest's other cross-service endpoints.
What actually fires today: provisioning itself does not go through this table — when an instance is created, CP calls your app's SDK-mounted lifecycle endpoint directly at its conventional path, as a required step of the provisioning pipeline (see Provisioning for that contract — it retries, and its failure fails the instance). Declared on_deprovision hooks are fired automatically at teardown, fail-closed: CP refuses to complete a deprovision until every cleanup hook succeeds. The remaining phases are persisted at publish and dispatched on demand through an operator refire route (POST .../refire-lifecycle-hook?phase=…) — the recovery path for when a consumer was unreachable at the original moment. Declaring a hook publishes the intent regardless; wire your endpoint and it runs whenever its phase dispatches.
lifecycle_hooks:
on_provision:
- description: Run migrations / seed initial structure for a new tenant instance.
endpoint: /v1/myservice/internal/lifecycle/provision
on_deprovision:
- description: Drop tenant data per retention policy.
endpoint: /v1/myservice/internal/lifecycle/deprovision
on_plan_change:
- description: Recompute feature overrides after a plan upgrade/downgrade.
endpoint: /v1/myservice/internal/lifecycle/plan-changed
on_upgrade:
- description: Run post-upgrade data migrations.
endpoint: /v1/myservice/internal/lifecycle/upgradeMake hooks idempotent
Every hook must be idempotent, because every delivery path can call it more than once — but the failure semantics differ by phase, and it pays to know which one you're in. The provisioning pipeline's call to your provision endpoint is required and retried: a failure is retried with backoff and ultimately fails the instance. Declared on_deprovision hooks are fail-closed: a failed cleanup blocks the teardown, and CP retries it rather than stranding tenant data. The other declared phases are best-effort: CP POSTs each hook, logs a failure without rolling back the parent mutation or blocking the other hooks, and recovery is the operator refire route. In all three cases a hook can run again after a partial first run — write check-and-create handlers, not blind-insert, and surface real failures in your own logs and metrics.
Two things provisioning hooks are not for:
- It seeds infrastructure and baseline structure, never application demo data.
on_provisionprepares an empty, working instance — schema, default roles, required rows — not sample records for the tenant to look at. - It is not where you emit events or send mail as a side effect. Those belong on the request path; a lifecycle hook is CP calling you, not your app reacting to its own change.
on_config_reload — the settings-reload hook
on_config_reload is the declared hook for the runtime half of the Settings & config story: when an operator edits a value, your process re-reads the changed config live — no restart, no redeploy — and invalidates anything it cached at boot. Declaring the hook publishes that intent so a reviewer sees the reload path.
One caveat, stated plainly: automatic dispatch on a settings save is not yet wired. CP persists the hook at publish but does not currently fire it when config changes; until that lands, trigger a reload through the operator refire route or your own config-poll. The other four phases behave as described above.
A hook without an endpoint documents intent
Only description is required; endpoint is optional. A hook entry that declares a description but no endpoint records that a lifecycle concern exists and is deliberately handled elsewhere (or not yet wired) — CP simply has nothing to call. It is a documentation signal, distinct from omitting the hook entirely.
Hooks vs events
Both let your app respond to platform activity, but they are different mechanisms — reach for the right one:
- Lifecycle hooks are direct CP → app calls at a small, fixed set of lifecycle moments (provision, plan change, …). Their failure semantics differ by phase, as described above: provision is required and retried, deprovision is fail-closed, and the rest are best-effort.
- Events are asynchronous, at-least-once fan-out over Kafka for domain facts, decoupled from any single caller.
- Resource claims deliver a paid, provisioned platform resource (a database, compute) to your app on the product fulfillment path. They are durable and retried with the same
claim_refuntil your handler returns the typed acknowledgement, so never handle a purchased resource in a lifecycle hook.
Use a hook when the platform should call your app at a lifecycle moment; use an event when something happened in your domain that others may react to.