Darstellung
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.
| Switch | Question |
|---|---|
creation | May a new instance of this be added? |
editing | May an existing instance be changed? |
frontend_execution | May an already-published form still use it for visitors? |
background_execution | May a cron job or queued task still use it? |
The table
| Category | creation | editing | frontend | background |
|---|---|---|---|---|
forms (multistep, conditional logic, calculations, repeater, templates) | no | no | yes | yes |
fields | no | no | yes | yes |
layout | no | no | yes | yes |
validation | no | no | yes | yes |
surveys | no | no | yes | yes |
styling | no | no | yes | yes |
integration (Elementor, Gutenberg) | no | no | yes | yes |
dynamic | no | no | yes | yes |
notifications | no | no | no | no |
actions | no | no | no | no |
payments | no | no | no | no |
pdf | no | no | no | no |
integrations | no | no | no | no |
wordpress | no | no | no | no |
| undeclared | no | no | no | no |
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:
| Context | Unentitled behaviour |
|---|---|
| Frontend request | Widget and block are registered. Existing pages render normally |
| Editor / admin request | Not 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 downgrade | Result |
|---|---|
| Rename the form | allowed |
| Change its status | allowed |
| Re-save the identical definition | allowed |
| Edit a Free field's label | allowed |
| Rename a locked field or step | allowed |
| Add a second conditional field | refused |
| Add a new payment action | refused |
| Retune a locked calculation formula | refused |
| Change a locked condition's rule | refused |
| Move a field between steps | refused |
| Re-enable a disabled payment action | refused |
| Duplicate the form | allowed, 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(), aslockedFeaturesinwindow.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:
- the platform issues a new signed payload with the larger entitlement list
- the next validation stores it
EntitlementManagerresolves the wider set- 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.