Skip to content

Downgrade policy

What happens when a plan shrinks, a subscription ends, or a license is revoked.

The one absolute rule

Nothing is ever deleted.

Never removed on downgrade, expiry, suspension, revocation, offline expiry or disconnect:

  • forms and their definitions
  • fields and field settings
  • submissions and submission meta
  • payment settings and payment records
  • integration settings
  • PDF settings
  • conditional logic
  • calculations and formulas
  • steps
  • analytics data
  • templates
  • actions and their settings
  • webhook settings
  • styles and custom CSS

A Business customer who drops to Free keeps every byte. The configuration becomes unusable, not gone, and works again the moment they upgrade. Asserted in FormSaveEnforcementTest and EntitlementMatrixTest.

The only deletion path in the plugin remains what it was before licensing existed: plugin uninstall with delete_data_on_uninstall explicitly enabled.

Four switches, not one

"Locked" is not a single behaviour, so RuntimePolicy expresses it as four independent switches per feature category.

SwitchQuestion
creationMay a new instance of this be added?
editingMay an existing instance be changed?
frontend_executionMay an already-published form still use it for visitors?
background_executionMay a cron job or queued task still use it?

The table

Categorycreationeditingfrontendbackground
forms (multistep, conditional logic, calculations, repeater, templates)nonoyesyes
fieldsnonoyesyes
layoutnonoyesyes
validationnonoyesyes
surveysnonoyesyes
stylingnonoyesyes
integration (Elementor, Gutenberg)nonoyesyes
dynamicnonoyesyes
notificationsnononono
actionsnononono
paymentsnononono
pdfnononono
integrationsnononono
wordpressnononono
undeclarednononono

Why builder features keep running

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 simply cannot build another one. Anything else punishes the wrong person.

Why payments do not

Letting a paid gateway keep processing money indefinitely would make the entitlement meaningless: the customer would pay once and take payments forever. So a payment action without the entitlement is refused at execution time.

Note precisely where that refusal happens. Actions run on ad_form_after_submission, after the entry is already persisted. A denied payment action therefore leaves the submission stored, with its payment_status untouched, and only skips the charge. The visitor's data is kept; the paid execution is not performed.

How page-builder modules implement that split

Elementor and Gutenberg need their widget and block registered to render a page that already contains one — Elementor resolves the widget class at render time, and the Gutenberg block is dynamic, so its render callback is what produces the markup. Skipping registration entirely would blank out a visitor-facing page over the site owner's billing.

So registration is split by context:

ContextUnentitled behaviour
Frontend requestWidget and block are registered. Existing pages render normally
Editor / admin requestNot registered. No new instance can be inserted

ModuleGate::allows() answers the authoring question, allows_frontend() the rendering one, and the integrations gate on is_admin(). Covered by FrontendCostTest.

Why revocation is stricter

revoked overrides the table and denies every phase for every category, including the frontend leniency. A revoked license is not a lapsed subscription; it is a deliberate platform action. It still deletes nothing.

Save-time behaviour is diff-aware

This is the part most likely to be got wrong, so it is stated at length.

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 untouched.

Without that, this happens:

A Pro+ customer built a Stripe checkout form. Their card expires. They drop to Free. They now try to fix a typo in the form title — and the save is refused, because the definition contains a Stripe action. They cannot edit the title, cannot unpublish the form, cannot do anything. They are locked out of their own admin screen by a feature they are not touching.

With diff-awareness:

Action after a downgradeResult
Rename the formallowed
Change its statusallowed
Re-save the identical definitionallowed
Edit a Free field's labelallowed
Rename a locked field or stepallowed
Add a second conditional fieldrefused
Add a new payment actionrefused
Retune a locked calculation formularefused
Change a locked condition's rulerefused
Move a field between stepsrefused
Re-enable a disabled payment actionrefused
Duplicate the formallowed, premium config carried over

Where the line sits

"Newly added or changed" is decided by a usage signature: the definition path, the required feature keys, and a hash of the configuration that usage covers.

The hash deliberately excludes presentational text. A label or a step title is not the paid feature, and refusing a cosmetic rename would stop a downgraded customer tidying up their own form for no benefit to anyone. Everything else — type, key, required flag, settings, condition rules, the enabled flag, the field-to-step assignment — is configuration, and changing it is using the paid feature.

Including the enabled flag closes a gap worth naming: without it, a customer could save a disabled Stripe action while still on Pro+, drop to Free, and flip it back on. The runtime gate would still refuse the charge, but the save would have succeeded. Now both layers refuse it.

Recorded as risk R-02 in docs/EXISTING_ARCHITECTURE_AUDIT.md. Covered by FormSaveEnforcementTest::test_a_downgraded_customer_can_still_edit_an_existing_premium_form and ..._cannot_add_more_premium_config.

Duplication

FormService::duplicate() passes the source definition as the diff baseline, so copying a form built on a higher plan keeps working. The copy inherits the locked configuration and the locked-feature notice, rather than failing outright. Risk R-03 in the audit.

Marking and explaining

FormEntitlementValidator::locked_usages() describes what a stored definition uses that the current plan does not cover, with the definition path, a human label, the missing keys, the required plan badge and an upgrade URL.

That feeds two places:

  • the builder, as a banner above the canvas listing each locked capability
  • Admin\Assets::licensing_config(), as lockedFeatures in window.adFormBuilder

The wording is deliberate and worth preserving:

This form uses features that are not available on your current plan. The configuration stays saved and works again after an upgrade.

Not "features have been removed". Nothing has been removed.

Save refusals are 403, not 400

Code feature_not_entitled, HTTP 403. The definition is well formed; it is simply not permitted.

The builder needs that distinction for a practical reason: autosave fires 1500 ms after a change. A permanent refusal treated as a generic error would be retried on every keystroke. The builder recognises feature_not_entitled, moves to a blocked save state, and suspends autosave until the user saves explicitly. Risk R-04 in the audit.

Upgrade path

Re-upgrading restores everything with no migration and no repair step, because nothing was lost. Asserted by EntitlementMatrixTest::test_upgrade_restores_everything across all five downgrade paths.

The sequence is simply:

  1. the platform issues a new signed payload with the larger entitlement list
  2. the next validation stores it
  3. EntitlementManager resolves the wider set
  4. the previously locked configuration is usable again

Free is a product, not a failure state

Free has to be genuinely usable, because it is what every degraded state falls back to. On Free, with no license at all, a site can still:

  • build forms with the drag and drop builder, autosave, undo and redo
  • use the standard field types
  • validate submissions
  • receive and manage entries, including spam and trash
  • send an email notification
  • embed a form by shortcode or Gutenberg block
  • rely on honeypot, nonce and the privacy controls

The full list is the core: true and Free rows in docs/FEATURE_MATRIX.md. No entitlement lookup may ever be introduced on those paths.

Digitale Lösungen. Persönlich. Zukunftssicher.