Skip to content

Feature enforcement audit

Where each paid feature is actually stopped. Produced after implementation by reading the code, not by restating the plan.

93 features are registered. This document covers every one of them: the 20 that are Free or core have no gate by design and are listed as such, the 73 paid features each name the layers that refuse them.

Legend

GateMeaning
registryFieldRegistry / ActionRegistry publish required_entitlements; the builder and GET /fields / GET /actions see the lock
scanDefinitionFeatureScanner detects the capability in a stored definition
saveFormEntitlementValidator inside FormService::assert_definition() → 403 feature_not_entitled
restAbstractRestController::deny_feature() → 403 with feature and upgrade_url
runtimeActionRunner::run() refuses execution before the handler is called
moduleModuleGate prevents registration entirely
policyRuntimePolicy decides creation / editing / frontend / background
uiBuilder palette badge and lock, or the License screen
adminAdmin screen capability plus entitlement check
noneUngated by design

Every paid feature has at least one server-side gate. ui never appears alone.

Currently enforceable

Features with code behind them today. These are enforced now.

FeatureKeyFilesEntitlementUI gateBackend gateRuntime gate
Multi-stepforms.multistepEnforcement/DefinitionFeatureScanner.php:80, Forms/FormService.php:213forms.multistepInspector lock + badgescan, savepolicy frontend allowed
Conditional logicforms.conditional_logicDefinitionFeatureScanner.php:118, FormService.php:213forms.conditional_logicInspector lock + badgescan, savepolicy frontend allowed
Calculationsforms.calculationsFields/Types/CalculationField.php:46, Fields/FieldRegistry.phpforms.calculationsPalette.tsx badge + lockregistry, scan, savepolicy frontend allowed
Conditional emailnotifications.conditionalDefinitionFeatureScanner.php:157notifications.conditionalInspectorscan, saveruntime denied
Multiple emailsnotifications.advancedDefinitionFeatureScanner.php:175notifications.advancedInspectorscan, saveruntime denied
Elementor widgetelementorElementor/ElementorIntegration.php:52,72elementorWidget absentmoduleexisting embeds render
Gutenberg blockgutenbergGutenberg/GutenbergIntegration.php:38gutenbergBlock absentmoduleexisting embeds render
Custom CSSstyling.custom_cssDefinitionFeatureScanner.php:190styling.custom_cssInspectorscan, savepolicy frontend allowed
Duplicate formforms.duplicateForms/FormService.php:196Freenone
Form scheduleforms.scheduleFrontend/FormGate.phpFreenone
Email notificationnotifications.emailActions/Types/EmailAction.phpFreenone
Redirectactions.redirectForms/FormDefinition.php settingsFreenone

payments.stripe is proven end to end through tests/Support/StripeTestAction.php, which declares the same required_entitlements the real gateway will. The runtime gate refuses it on Free, Pro and on a partial entitlement, and the save gate refuses adding it. The gate is therefore in place before the gateway exists, which is the point.

Unregistered premium types are pinned on the registries themselves (FieldRegistry::RESERVED_TYPES, ActionRegistry::RESERVED_TYPES). A crafted JSON body naming repeater or stripe before those classes exist is refused at save time on Free/Pro, and a Pro+ site may store the reserved Stripe action so it fires the day the gateway ships.

Reserved but not yet implemented

These have a registered key, a documented gate and a plan assignment. There is no code to gate yet. Recorded so the feature is born gated instead of retrofitted — the failure mode this whole document exists to prevent.

The gate column states where the check must go when the feature is built. A feature added without it will show up here as a gap.

Fields — registry, scan, save

fields.advanced, fields.address, fields.file_upload, fields.signature, fields.gdpr, fields.product, fields.order

Declared via required_entitlements() on the field class. The registry, the scanner, the save gate, the palette and the REST catalogue then pick it up with no further wiring, as CalculationField already demonstrates.

Form builder — scan, save, rest

forms.templates, forms.import_export, forms.restrictions, forms.repeater, forms.conversational

Layout — scan, save

layout.containers, layout.field_groups, layout.section_backgrounds

layout.columns, layout.sections and layout.responsive are Free. Note that definition.layout.rows is currently a persisted placeholder with no reader, so these three are Free features the plugin does not yet render.

Styling — rest, save, admin

styling.global

Page builders — module

elementor.full_style_panel

Actions — registry, scan, save, runtime

actions.webhook, actions.wp_user, actions.wp_post

Declared via required_entitlements() on the action class. ActionRunner then refuses execution automatically.

Payments — module, registry, scan, save, runtime

payments, payments.stripe, payments.paypal, payments.mollie, payments.subscriptions, payments.donations, payments.products, payments.discounts, payments.taxes, payments.shipping, payments.live_calculation

RuntimePolicy denies the payments category in every phase, including the frontend. A gateway must also never write plugin-license data into ad_form_payments.

PDF — module, scan, save, runtime

pdf, pdf.invoice, pdf.quote, pdf.contract, pdf.email

Integrations — module, runtime

integrations, integrations.mailchimp, integrations.google_sheets, integrations.slack, integrations.zapier, integrations.hubspot, integrations.salesforce, integrations.activecampaign

Each has its own key so the dashboard can move individual integrations between plans without a plugin release.

Surveys and quiz — scan, save, rest

surveys, quiz

Analytics — rest, admin

analytics.basic, analytics.advanced

analytics.basic is partial: the analytics_enabled setting and Database\Stats exist, but there is no analytics feature behind them.

Dynamic data and WordPress — scan, save, runtime

dynamic.prefill, wordpress.frontend_post, wordpress.user_management

Export and i18n — rest, module

exports.advanced, i18n.advanced

Security — scan, save, runtime, admin

security.recaptcha, security.hcaptcha, security.turnstile, security.rate_limiting, security.akismet, security.advanced_bot_detection

Privacy — rest, admin

privacy.gdpr

Platform and developer — admin, rest, module

white_label, team.roles, team.permissions, logging.full, developer.api, developer.addons

logging.full is partial: Logger exists as the Free logging.basic baseline; the Business tier above it does not.

Ungated by design

These 20 keys carry core: true or sit on Free and must never acquire an entitlement lookup. A revoked license leaves all of them working.

KeyReason
forms.builderBuilding a form is the product, not an upsell
forms.autosaveData-loss prevention
forms.undo_redoData-loss prevention
forms.shortcodeThe only universal embed method
forms.duplicateSee FEATURE_MATRIX_REVIEW.md
forms.scheduleAlready shipped as Free behaviour
fields.standardA form builder without fields is not one
validation.basicSecurity control
validation.liveFree per the product matrix
layout.columns, layout.sections, layout.responsiveFree per the product matrix
notifications.emailA contact form that cannot notify does not contact
actions.redirectAlready shipped as Free confirmation behaviour
gutenbergThe only block-editor embed path for a Free user
submissions.managementReading customer data must never be gated
security.honeypotSecurity control
security.nonceSecurity control
privacy.storageCompliance control
logging.basicDiagnosability

Also ungated and not registry features at all: form submission and entry storage, server-side condition evaluation during submit, nonce verification, sanitisation, escaping, database migrations, and the License screen itself — the last so a revoked license can still be repaired.

Cross-cutting enforcement

ConcernWhere
Entitlement resolutionEntitlements/EntitlementManager.php — the only authority
Free baselineEntitlements/FeatureRegistry::free_keys()
Dependency expansionFeatureRegistry::with_dependencies()
Runtime phasesEntitlements/RuntimePolicy.php
Refusal exceptionEntitlements/NotEntitledException.php, 403 feature_not_entitled
REST envelopeREST/AbstractRestController::deny_feature()
Save choke pointForms/FormService::assert_definition()
Action executionActions/ActionRunner::may_run()
Module registrationEnforcement/ModuleGate.php
Locked-form markingEnforcement/FormEntitlementValidator::locked_usages()
Builder dataAdmin/Assets::licensing_config()
Builder UIassets/src/builder/entitlements.ts, components/LockedFeature.tsx, components/Palette.tsx

Gaps and honest notes

  1. REST catalogue endpoints are not filtered. GET /fields and GET /actions return every type with its required_entitlements, and the client renders the lock. Nothing is leaked — the type catalogue is marketing information, and the save gate refuses the type regardless. Filtering the catalogue would only hide what a customer could buy.

  2. FormDefinitionValidator still does not check types against the registries. Pre-existing behaviour, recorded in the Phase 0 audit. An invented field type is inert rather than free: it declares no entitlements, so the scanner finds nothing, and at submit time it falls back to sanitize_text_field. Worth closing on its own merits, not as a licensing fix.

  3. The layout.* Free features have no renderer. definition.layout.rows is persisted and never read. The keys are registered and Free, so nothing is wrongly locked, but the matrix currently advertises three Free features the plugin does not render.

  4. notifications.email is a boolean, not a count. The scanner treats a second email action as notifications.advanced, which implements the intended rule, but EntitlementManager has no quantitative entitlement concept. If the product wants "Free gets exactly N of X", that needs a contract change. Open in FEATURE_MATRIX_REVIEW.md.

  5. fields.standard is uncapped. Eight field types exist against a matrix that promises 25. The key is a boolean, so shipping more standard fields needs no licensing change — but the count gap is real and is a scope item, not a licensing one.

  6. No network-admin licence screen on multisite. Each blog manages its own licence. Additive later; not a gap in enforcement.

Verification

composer install
./vendor/bin/phpunit   # 525 tests, 1238 assertions
./vendor/bin/phpcs     # clean
npm run build          # clean

The enforcement claims above are backed by tests/Unit/Licensing/EntitlementMatrixTest.php (plan matrix and all five downgrade paths), FormSaveEnforcementTest.php (save gate, including diff-awareness and duplication), RuntimeEnforcementTest.php (action execution and the runtime policy), ModuleGateTest.php (module registration), RestGateTest.php (refusal envelope and the privacy boundary), and BypassAttemptTest.php (thirteen bypass attempts, all failing).

Digitale Lösungen. Persönlich. Zukunftssicher.