Skip to content

ADR 0005 — Downgrade policy

Status: accepted Date: 2026-08-21

Context

A customer's plan can shrink: a card expires, they downsize, or a licence is revoked. The plugin has to decide what happens to forms already built with features they no longer hold.

Two obvious answers are both wrong.

Deleting premium configuration is unacceptable: it destroys work, and a customer who upgrades again gets nothing back. Ignoring the downgrade entirely makes the entitlement meaningless.

There is also a subtler trap. Refusing every save that touches a premium form locks a downgraded customer out of their own admin screens — they cannot rename the form, cannot unpublish it, cannot do anything, because the definition contains a Stripe action they are not editing.

Decision

Nothing is ever deleted. Enforcement is per phase, and save-time enforcement is diff-aware.

Nothing deleted

Forms, fields, submissions, payment settings, integration settings, PDF settings, conditional logic, calculations, steps, analytics data, templates, actions, webhook settings and styles all survive every downgrade, expiry, suspension, revocation and disconnect. The only deletion path remains plugin uninstall with delete_data_on_uninstall explicitly enabled — pre-existing behaviour.

Four switches, not one

RuntimePolicy gives each feature category independent creation, editing, frontend_execution and background_execution switches.

Builder features (forms, fields, layout, validation, surveys, styling, page builders, dynamic data) deny creation and editing but allow frontend execution. A visitor filling in a five-step form should not meet a broken page because the site owner's card expired last night. The form keeps working; the owner cannot build another one.

Payments, PDF, integrations, notifications, actions and WordPress actions deny every phase including the frontend. Letting a paid gateway keep taking money indefinitely would mean paying once and charging forever.

revoked overrides the table and denies every phase for every category. It still deletes nothing.

Diff-aware save gate

FormEntitlementValidator compares the incoming definition against the stored one and refuses only capabilities that are newly added or whose configuration changed. Pre-existing premium configuration passes through.

So after a downgrade: renaming is allowed, changing status is allowed, re-saving the identical definition is allowed, editing a Free field is allowed, but adding a second conditional field or a new payment action is refused.

duplicate() passes the source definition as the baseline, so copying a form built on a higher plan keeps working.

Refusals are 403

Code feature_not_entitled. The definition is well formed, it is simply not permitted. The builder needs that distinct from a validation error for a practical reason: autosave fires 1500 ms after a change, so a permanent refusal treated as a generic error would retry on every keystroke. The builder moves to a blocked state and suspends autosave until the user saves explicitly.

Consequences

Good:

  • A downgraded customer keeps working and keeps their data.
  • Re-upgrading restores everything with no migration step.
  • Visitors are never punished for the site owner's billing.
  • Paid gateways stop, so the entitlement means something.
  • The customer sees "the configuration stays saved and works again after an upgrade" rather than "features have been removed".

Costs:

  • The save gate needs the stored definition, so assert_definition() and create() both gained a parameter. Additive with a default.
  • Diffing is by usage signature, so an unrelated edit that happens to change a premium node's id would look like a new addition. Acceptable: the id is stable across rename and reorder by design.
  • Premium configuration accumulates in the database for customers who never upgrade again. Acceptable; it is their data.

Rejected

Delete premium config on downgrade. Destroys customer work and makes re-upgrading worthless.

Refuse every save touching a premium form. Locks the customer out of their own admin screens. This is risk R-02 in the Phase 0 audit and the reason the gate is diff-aware.

Block frontend rendering of premium forms. Punishes visitors for a billing event they know nothing about.

Let payments keep running. Makes the payments entitlement decorative.

One policy for all categories. Cannot express "the form still renders but the gateway does not charge", which is the whole point.

Digitale Lösungen. Persönlich. Zukunftssicher.