Skip to main content

Documentation

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):

HookCP fires it when…You typically…
on_provisionA new tenant instance of your app is being createdRun migrations, seed the instance's baseline structure
on_deprovisionAn instance is being torn downDelete or archive that tenant's data per your retention policy
on_plan_changeThe org's subscription plan changesRecompute feature overrides / quotas after an upgrade or downgrade
on_config_reloadAn operator saves a settings changeRe-read the affected config without a redeploy
on_upgradeYour app is upgraded to a new versionRun 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.

app.manifest.yamlyaml
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/upgrade

Make 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_provision prepares 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_ref until 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.