Flexa FormFlow

Technical reference · v1.3.3

Schema, REST routes and extension points

This page is for developers. It covers the tables the plugin owns, the REST surface the admin app talks to, the merge tags available to a design, and the filters an add-on hooks to extend any of it. Flexa FormFlow Pro is one such add-on, built entirely on the filters listed here.

Reference

Overview

Two plugins, one data model. The free plugin owns everything: the tables, the REST namespace, the admin React app, the renderer. Pro adds no tables and no screens of its own beyond one settings page.

FreePro
Slugflexa-formflowflexa-formflow-pro
Version1.3.31.0.0
NamespaceFlexa\\FormFlowFlexa\\FormFlowPro
Text domainflexa-formflowflexa-formflow-pro
REST namespaceflexa-formflow/v1flexa-formflow-pro/v1
Custom tables7none

Reference

Requirements

RequirementMinimumNote
WordPress6.5Declared in the plugin header.
PHP8.1The plugin disables itself and prints a notice below this version.
WooCommerceOptionalWithout it the Woo email takeover and the order blocks stay off. Everything else runs.
MySQL / MariaDBWhatever WordPress supportsTables are created with dbDelta and the site charset and collation.

Pro needs Free

Pro checks for the free plugin before it boots. If it is missing or too old, Pro prints an admin notice and stops, so none of its filters are registered.

Reference

Architecture

A single PSR-4 autoloaded src/ tree, one React admin app built with Vite, and a front end that ships a small script per form.

LayerWhereWhat it does
Bootsrc/Plugin.phpLoads the text domain, registers the REST router, the shortcode and block, the mailer, the workflow engine, and the admin screens.
Domainsrc/Domain/Forms, Entries, EmailTemplates and Workflows, each an entity plus a repository over its table.
Emailssrc/Emails/The element registry, the renderer, the token resolver, the global header and footer layout, and the notification mailer.
Workflowssrc/Workflows/The engine that walks a workflow config, runs actions and writes a run log.
Spamsrc/Spam/Honeypot, minimum fill time, rate limits and CAPTCHA, with counters in a throttle table.
WooCommercesrc/WooCommerce/Registered only when WooCommerce is active. Intercepts Woo email templates and adds order tokens and blocks.
Packssrc/Packs/Industry packs: bundles of forms, emails and workflows installed and updated as a unit.
Admin appapps/admin/React 18, TanStack Query, Zustand, Tailwind 4, dnd-kit, built with Vite into assets/dist.

The email tree

An email template stores one JSON tree in its tree column. Elements are flat objects with a type and a props bag, so a new block type is a registry entry rather than a schema change.

Shape of an email template tree
{
  "version": 1,
  "settings": { "addons": {} },
  "elements": [
    { "id": "el-1", "type": "logo",        "props": {} },
    { "id": "el-2", "type": "heading",     "props": { "text": "Thanks!" } },
    { "id": "el-3", "type": "fields_table","props": { "title": "What you sent" } },
    { "id": "el-4", "type": "footer_text", "props": {} }
  ],
  "origin": null
}

origin marks a template that is not a plain email: a WooCommerce design, a synced pattern fragment, or a language version. Those rows live in the same table but are filtered out of the Emails list.

Reference

How Pro extends Free

Pro ships no React into the free admin bundle. Every Pro screen element is declared as data on a filter, and the free app renders it with its own controls.

The pattern is the same for each feature: a describe filter that returns a descriptor (label, icon, fields, defaults, a locked flag), and a run filter that handles the type at runtime. The workflow node below is the worked example; the field types, the email blocks and the integration cards all follow it.

Adding a workflow action from an add-on
add_filter( 'flexa_formflow.workflows.action_types', function ( array $types ): array {
    $types[] = [
        'type'    => 'my_action',
        'label'   => __( 'Send to my service', 'my-addon' ),
        'icon'    => 'send',
        'group'   => 'pro',
        'summary' => __( 'POST the entry to my endpoint.', 'my-addon' ),
        'fields'  => [ /* control descriptors */ ],
    ];

    return $types;
} );

add_filter( 'flexa_formflow.workflows.run_action', function ( $result, string $type, array $config, $form, $entry, $ctx ) {
    if ( 'my_action' !== $type ) {
        return $result;
    }

    // ... do the work, return a log array.
}, 10, 6 );

Locked, not hidden

A descriptor can carry locked: true and a lockedNote. The free app still draws the card so the feature can be explored, but its configuration is read-only. That is how Pro features appear before a license is added.

Reference

Database schema

Seven tables, all prefixed {$wpdb->prefix}flexa_formflow_. They are created by dbDelta on activation and re-migrated on admin_init whenever Schema::DB_VERSION moves, so an update never needs a manual re-activation.

TableHoldsKey columns
flexa_formflow_formsOne row per form.uuid (unique), title, status, config LONGTEXT
flexa_formflow_entriesOne row per submission.form_id, status, data, meta, created_at
flexa_formflow_email_templatesEmails, Woo designs, patterns and language versions.title, tree LONGTEXT
flexa_formflow_workflowsOne row per workflow.title, status, config LONGTEXT
flexa_formflow_librarySaved reusable items and pack provenance.uuid, type, kind, payload, source_pack, source_hash
flexa_formflow_workflow_runsRun history, one row per fired workflow.workflow_id, entry_id, status, log
flexa_formflow_throttleShort-lived spam counters.bucket CHAR(64) primary key, hits, expires_at

The throttle table holds no identifiers

Rate limits and one-time CAPTCHA tokens are keyed by a hashed bucket. No IP address and no raw token is written to the database. Expired rows are swept as new hits arrive.

Pro adds no tables. Synced patterns, language versions and WooCommerce designs are all rows in flexa_formflow_email_templates with an origin set, which is why they copy, export and roll back with the same code as a plain email.

Reference

Options

OptionAutoloadedHolds
flexa_formflow_settingsYesBrand color, email defaults, CAPTCHA keys, AI provider and key, uninstall behavior.
flexa_formflow_email_layoutYesThe global header and footer sets and their assignments.
flexa_formflow_connectionsYesSaved integration connections, by integration id.
flexa_formflow_installed_packsYesWhich industry packs are installed, and at what version.
flexa_formflow_onboardingYesWelcome guide progress.
flexa_formflow_reviewYesReview prompt state.
flexa_formflow_db_versionYesSchema version, compared against Schema::DB_VERSION.
flexa_formflow_render_revNoRender cache revision, bumped to invalidate cached markup.
flexa_formflow_pro_licenseYesPro. The key, its instance token, status and the last server snapshot.
flexa_formflow_pro_variablesYesPro. Custom variables, with per-language values.
flexa_formflow_pro_paletteYesPro. Brand colors, by key.
flexa_formflow_pro_languagesYesPro. Language settings, per-language footers, provider choice.
flexa_formflow_pro_woo_emailsYesPro. The WooCommerce emails other plugins register.
flexa_formflow_pro_email_varsYesPro. Argument names Woo emails have passed, for the Dynamic data list.

Secrets are encrypted at rest

The AI API key and the CAPTCHA secret are encrypted before they go into flexa_formflow_settings and decrypted only for an outbound call. The REST representation masks them to a sentinel, so plaintext never reaches the browser. A partial save merges over the stored ciphertext rather than overwriting it.

Reference

REST API

The admin app is a REST client. Every route below requires manage_options by default (see Capabilities) and a valid X-WP-Nonce. The one public route is the submission endpoint.

Forms and entries

MethodRoute
GET/flexa-formflow/v1/forms
POST/flexa-formflow/v1/forms
GET/flexa-formflow/v1/forms/<id>
PUT/flexa-formflow/v1/forms/<id>
DELETE/flexa-formflow/v1/forms/<id>
POST/flexa-formflow/v1/forms/<id>/duplicate
POST/flexa-formflow/v1/forms/<id>/test-submission
POST/flexa-formflow/v1/forms/preview
POST/flexa-formflow/v1/submit/<uuid>
GET/flexa-formflow/v1/entries
GET/flexa-formflow/v1/entries/<id>
PUT/flexa-formflow/v1/entries/<id>
DELETE/flexa-formflow/v1/entries/<id>

The submit route is the public one

POST /submit/<uuid> takes a form uuid rather than an id, is open to visitors, and runs the whole spam chain (honeypot, minimum fill time, rate limit, CAPTCHA) plus field validation before anything is written.

Emails

MethodRoute
GET/flexa-formflow/v1/email-templates
POST/flexa-formflow/v1/email-templates
GET/flexa-formflow/v1/email-templates/<id>
PUT/flexa-formflow/v1/email-templates/<id>
DELETE/flexa-formflow/v1/email-templates/<id>
POST/flexa-formflow/v1/email-templates/<id>/duplicate
GET/flexa-formflow/v1/email-templates/<id>/default
GET/flexa-formflow/v1/email-templates/<id>/tabs
GET/flexa-formflow/v1/email-templates/origins
POST/flexa-formflow/v1/email-templates/import
POST/flexa-formflow/v1/email-preview
POST/flexa-formflow/v1/email-test
GET/flexa-formflow/v1/emails/dynamic-data
GET/flexa-formflow/v1/emails/patterns
POST/flexa-formflow/v1/emails/patterns/preview
GET/flexa-formflow/v1/email-layout
PUT/flexa-formflow/v1/email-layout
POST/flexa-formflow/v1/email-layout/sets
PUT/flexa-formflow/v1/email-layout/sets/<id>
DELETE/flexa-formflow/v1/email-layout/sets/<id>
POST/flexa-formflow/v1/email-layout/apply

WooCommerce emails

MethodRoute
GET/flexa-formflow/v1/woo-emails
PUT/flexa-formflow/v1/woo-emails/<id>
POST/flexa-formflow/v1/woo-emails/<id>/customize
POST/flexa-formflow/v1/woo-emails/<id>/preview
POST/flexa-formflow/v1/woo-emails/<id>/test
GET/flexa-formflow/v1/woo-emails/orders

Workflows, library and settings

MethodRoute
GET/flexa-formflow/v1/workflows
POST/flexa-formflow/v1/workflows
PUT/flexa-formflow/v1/workflows/<id>
DELETE/flexa-formflow/v1/workflows/<id>
POST/flexa-formflow/v1/workflows/<id>/test
GET/flexa-formflow/v1/workflows/<id>/runs
GET/flexa-formflow/v1/library/catalog
GET/flexa-formflow/v1/library/mine
POST/flexa-formflow/v1/library/mine
DELETE/flexa-formflow/v1/library/mine/<id>
POST/flexa-formflow/v1/library/packs/<id>/import
POST/flexa-formflow/v1/library/packs/<id>/update
POST/flexa-formflow/v1/library/packs/<id>/restore
GET/flexa-formflow/v1/library/packs/<id>/diff
GET/flexa-formflow/v1/settings
POST/flexa-formflow/v1/settings
GET/flexa-formflow/v1/stats
GET/flexa-formflow/v1/integrations
POST/flexa-formflow/v1/integrations/<id>/connection
GET/flexa-formflow/v1/spam/status
POST/flexa-formflow/v1/spam/captcha/test
POST/flexa-formflow/v1/ai/form
POST/flexa-formflow/v1/ai/generate
GET/flexa-formflow/v1/lookup/<source>

Pro namespace

MethodRoute
GET/flexa-formflow-pro/v1/license
POST/flexa-formflow-pro/v1/license
GET/flexa-formflow-pro/v1/variables
POST/flexa-formflow-pro/v1/variables
GET/flexa-formflow-pro/v1/variables/export
POST/flexa-formflow-pro/v1/variables/import
GET/flexa-formflow-pro/v1/palette
GET/flexa-formflow-pro/v1/palette/theme
GET/flexa-formflow-pro/v1/synced-patterns
PATCH/flexa-formflow-pro/v1/synced-patterns/<uuid>
POST/flexa-formflow-pro/v1/synced-patterns/actions/save
POST/flexa-formflow-pro/v1/synced-patterns/actions/edit
POST/flexa-formflow-pro/v1/synced-patterns/actions/detach
GET/flexa-formflow-pro/v1/languages
POST/flexa-formflow-pro/v1/languages/install/<locale>
POST/flexa-formflow-pro/v1/languages/versions/create/<locale>
POST/flexa-formflow-pro/v1/languages/versions/delete
GET/flexa-formflow-pro/v1/countdown

/countdown is the only Pro route a mail client touches: it serves the animated GIF for the Countdown block, drawn on request so the timer is current each time the message is opened.

Registering your own route in the plugin's namespace
add_action( 'flexa_formflow.rest.register_routes', function ( string $namespace ): void {
    register_rest_route( $namespace, '/my-thing', [
        'methods'             => 'GET',
        'callback'            => 'my_callback',
        'permission_callback' => [ \Flexa\FormFlow\Support\Capabilities::class, 'can_manage' ],
    ] );
} );

Reference

Tokens and merge tags

One resolver handles every merge tag in an email. Field values use {field:ID}; everything else is a named token.

TokenValueAvailable in
{site_title}Site titleAll emails
{site_tagline}Site taglineAll emails
{site_url}Home URLAll emails
{site_logo_url}Site logo image URL, empty when none is setAll emails
{admin_email}Admin email addressAll emails
{year}Current yearAll emails
{form_title}Form titleForm emails
{entry_id}Entry idForm emails
{entry_date}Submission date, localizedForm emails
{page_url}URL of the page the form was onForm emails
{field:ID}One answer from the entryForm emails

WooCommerce tokens

Registered only when WooCommerce is active. The order tokens need an order behind the email and resolve to an empty string without one, so a form email can safely share a template with an order email.

Order tokens, from OrderTokens::catalog()
{order_number}        {order_date}           {order_total}
{order_status}        {order_url}            {payment_url}
{refund_amount}       {payment_method}       {shipping_method}
{customer_first_name} {customer_last_name}   {customer_full_name}
{customer_email}      {order_subtotal}       {order_discount}
{order_shipping_total} {order_tax_total}
{billing_address}     {shipping_address}     {customer_note}
{shop_url}            {my_account_url}

Ten more come from the email's own context rather than the order, which is how the account emails and the security alert get their links. Each is filled for the email named beside it and left empty elsewhere.

Context tokens, from ContextTokens::catalog()
{reset_password_url}    Reset password email
{set_password_url}      New account email
{verify_email_url}      Confirm email address email
{gateway_title}         Payment gateway enabled email
{gateway_settings_url}  Payment gateway enabled email
{pos_store_name}        POS receipts (falls back to the site name)
{pos_store_email}       POS receipts
{pos_store_phone}       POS receipts
{pos_store_address}     POS receipts
{pos_refund_policy}     POS receipts

OrderTokens::catalog() merges the context list into its own, and that one array feeds both the editor's Dynamic data panel and the token strip on the WooCommerce tab. To add a token of your own, resolve it on flexa_formflow.emails.tokens and list it for the editor on flexa_formflow.emails.dynamic_data.

Tokens Pro adds

PrefixExampleSource
var:{var:hotline}Custom variables, with a value per language.
order_meta:{order_meta:delivery_date}Fields other plugins store on the order.
email:{email:email_heading}Values the WooCommerce email passes to its template.

Escaping is decided by the resolver, not the block

In rich-text blocks every value is escaped before it reaches the markup, so a visitor's answer can never inject a tag or a link. Tokens whose value is trusted markup (a formatted address) register themselves on flexa_formflow.emails.html_tokens and pass through wp_kses_post() instead.

An editor preview shows an unknown token as-is, so a typo is easy to spot. A real send never prints a raw token: a token with no value becomes its registered fallback, which is an empty string unless something registers otherwise.

Extension points

Hooks · forms

HookTypeUse
flexa_formflow.forms.field_typesfilterAdd a field type descriptor (label, icon, input, fields, defaults).
flexa_formflow.forms.item_typesfilterAdd a non-field item, such as the Pro Step marker.
flexa_formflow.forms.field_optionsfilterSupply the choices for one list field at render time.
flexa_formflow.forms.field_valuefilterValidate and normalize a submitted value. Return a WP_Error to reject it.
flexa_formflow.forms.settings_fieldsfilterAdd controls to the form settings panel.
flexa_formflow.forms.register_assetsactionRegister a script or style the builder can attach to a form.
flexa_formflow.forms.assetsfilterDecide which registered handles a given form loads.
flexa_formflow.submission.validatefilterWhole-submission validation, after per-field checks.
flexa_formflow.entry.createdactionFires once an entry is stored.
flexa_formflow.form.savedactionFires when a form is created or updated.
flexa_formflow.spam.rate_limitsfilterChange the rate-limit buckets and their windows.
flexa_formflow.spam.min_secondsfilterChange the minimum fill time before a submission is accepted.
flexa_formflow.spam.client_ipfilterResolve the client IP behind a proxy or CDN.
flexa_formflow.captcha.providersfilterRegister another CAPTCHA provider.
flexa_formflow.lookup.sourcesfilterAdd a source the builder can pull list choices from.

Extension points

Hooks · emails

HookTypeUse
flexa_formflow.emails.element_typesfilterDeclare a block type for the inserter and the inspector.
flexa_formflow.emails.elementsfilterRegister the PHP renderer for a block type.
flexa_formflow.emails.tokensfilterAdd resolved token values for the current render.
flexa_formflow.emails.token_fallbacksfilterWhat a token prints on a real send when it has no value.
flexa_formflow.emails.html_tokensfilterMark a token as trusted markup rather than plain text.
flexa_formflow.emails.dynamic_datafilterAdd rows to the Dynamic data list in the editor.
flexa_formflow.emails.settings_fieldsfilterAdd controls to the Template design panel. Pro attaches Attachments here.
flexa_formflow.emails.attachmentsfilterFiles to send with a rendered email.
flexa_formflow.emails.patternsfilterAdd ready-made block patterns to the inserter.
flexa_formflow.emails.pattern_categoriesfilterAdd a category those patterns are listed under.
flexa_formflow.emails.block_actionsfilterAdd an action to a block toolbar, such as "Save as synced".
flexa_formflow.emails.node_visiblefilterDecide whether one block renders in the current context.
flexa_formflow.emails.default_treefilterThe starting tree for a newly created email.
flexa_formflow.emails.editor_tabsfilterTabs above the editor canvas. Pro puts its language versions here.
flexa_formflow.emails.localefilterWhich language an email goes out in.
flexa_formflow.emails.template_idfilterSwap the template actually rendered, by id.
flexa_formflow.emails.subjectfilterChange the subject line for a send.
flexa_formflow.emails.layout_setfilterWhich global header and footer set applies.
flexa_formflow.emails.footer_textfilterThe footer line, per send.
flexa_formflow.emails.switch_localeactionFires as the renderer switches language; switch your translation plugin here.
flexa_formflow.emails.restore_localeactionThe matching restore.
flexa_formflow.emails.template_deletedactionClean up anything keyed to a deleted template.
flexa_formflow.emails.site_logo_urlfilterOverride the logo the Logo block uses.
flexa_formflow.colors.palettefilterNamed colors offered by every color control. Pro fills this.
flexa_formflow.colors.resolvefilterTurn a palette name into a hex value at render time.
flexa_formflow.notification.sentactionFires after a notification email is handed to wp_mail.
flexa_formflow.woo.catalogfilterAdd emails to the WooCommerce page. Pro adds other plugins’ emails here.
flexa_formflow.woo.default_treefilterThe starting design for one WooCommerce email.
flexa_formflow.woo.item_detailsfilterExtra detail rows under a line item.
flexa_formflow.woo.email_savedactionFires when a WooCommerce design is saved.

Two condition systems on node_visible

Both ship in the free plugin, both hang off flexa_formflow.emails.node_visible, and they read different keys on the same block node, so a block can carry both. Neither applies in the editor or the preview: a preview render shows every block so the design stays editable.

Key on the nodeClassTested againstCombines with
visibilityEmails\Render\VisibilityThe entry. Rules name a form field by id.match is all or any.
conditionsWooCommerce\ConditionsThe WC_Order. Skipped when the render has no order.AND only: every condition must pass.
The two shapes, as stored on a block node
// visibility: operators is, is_not, contains,
// not_contains, is_empty, is_not_empty.
'visibility' => [
    'match' => 'all',          // or 'any'
    'rules' => [
        [ 'field' => 'enquiry_type', 'op' => 'is', 'value' => 'Support' ],
    ],
],

// conditions: subjects order_total, items_count, payment_method,
// shipping_country, billing_country. Operators eq, neq, gt, gte,
// lt, lte, contains.
'conditions' => [
    [ 'subject' => 'order_total', 'operator' => 'gte', 'value' => '100' ],
],

visibility has an editor UI (the Visibility panel at the bottom of the block inspector). conditions is a render-time seam with no UI of its own yet; the subject and operator lists are served on the WooCommerce emails endpoint as conditionSubjects and conditionOps, so an add-on can build the control against the same vocabulary the renderer uses. Numeric subjects compare as floats with a 0.0001 tolerance; the rest compare lowercased strings.

Extension points

Hooks · workflows

HookTypeUse
flexa_formflow.workflows.action_typesfilterDeclare an action node as data: label, icon, group, fields, locked state.
flexa_formflow.workflows.run_actionfilterHandle your node at runtime and return a log entry.
flexa_formflow.workflow.ranactionFires once a workflow finishes, with its run log.
flexa_formflow.integrations.catalogfilterAdd or change a card on the Integrations page.
flexa_formflow.integrations.connect_fieldsfilterDescribe the connect drawer for one integration.
flexa_formflow.integrations.bridgefilterRoute an outbound call through your own transport.
flexa_formflow.library.catalogfilterAdd items to the Library.
flexa_formflow.packsfilterRegister an industry pack.

Extension points

Hooks · Pro

HookTypeUse
flexa_formflow_pro.is_licensedfilterFinal say on whether Pro features are unlocked.
flexa_formflow_pro.license_api_basefilterPoint the license check at another host.
flexa_formflow_pro.product_slugfilterThe product slug sent with a license check.
flexa_formflow_pro.rest.register_routesactionRegister routes in the Pro namespace.
flexa_formflow_pro.tracking.shipmentsfilterAdd shipments from a tracking plugin that is not read natively.
flexa_formflow_pro.order_fields.labelsfilterGive an order meta key a human label.
flexa_formflow_pro.offers.codefilterReplace the coupon code the Offer block prints.
flexa_formflow_pro.emails.linkfilterChange a link Pro blocks build, for tracking parameters.
flexa_formflow.pro_activefilterRead by the free plugin to tell whether Pro is present.

Reference

License behavior

The license lives in one option and is refreshed against the hosted service on activation, then daily by WP-Cron.

Stored shape of flexa_formflow_pro_license
key            string  the license key
instance_token string  per-site token minted by the service on activation
status         string  active|grace|expired|revoked|disabled|inactive
license        array   last server snapshot (tier, term, expires_at, over_limit, product)
validated_at   int     unix ts of the last successful server contact
checked_at     int     unix ts of the last attempt, successful or not
last_error     string  error code from the last failed attempt
SituationResult
Status activePro features unlocked.
Service unreachableStatus moves to grace. Features stay unlocked for 14 days from validated_at.
Grace window elapsedFeatures lock. Placed features keep rendering; see the Pro guide.
Service answers expired, revoked or wrong keyFeatures lock immediately, with no grace.
FLEXA_FORMFLOW_PRO_UNLOCKConstant that force-unlocks for local development. Never define it in production.

License::is_active() is the single gate, and its result passes through flexa_formflow_pro.is_licensed so a site can take the final decision itself.

Reference

Capabilities

Two checks guard everything. Both default to manage_options and both are filterable, so a site can open the app to an editor role without touching the plugin.

Letting shop managers run forms
add_filter( 'flexa_formflow.capabilities.manage', fn() => 'manage_woocommerce' );
add_filter( 'flexa_formflow.capabilities.settings', fn() => 'manage_options' );
FilterGuards
flexa_formflow.capabilities.manageThe admin app and every REST route except the public submit endpoint.
flexa_formflow.capabilities.settingsThe Settings screen and the settings routes, including the AI and CAPTCHA keys.

Split them deliberately

Keeping settings at manage_options while loosening manage lets an editor build forms without being able to read or change API keys.

Reference

Internationalization

FreePro
Text domainflexa-formflowflexa-formflow-pro
Bundled translationsi18n/languages/i18n/languages/
Loaded oninitinit

A language pack in wp-content/languages/plugins/ takes precedence over the bundled files, so a site translation is never overwritten by an update. Both plugins load on init rather than earlier, which WordPress 6.7 requires.

Emails are translated separately

The admin UI follows the user's profile language. An email follows its reader: WordPress is switched for the duration of the render, so dates and WooCommerce's own wording match the message. Pro's language versions build on the same seams.

Reference

Uninstall and data

Deleting the plugin removes nothing by default. Forms, entries, emails and workflows are content, and content survives a reinstall.

To opt in, turn on Delete all data on uninstall in the danger zone of the Settings screen. That writes delete_data_on_uninstall into flexa_formflow_settings, which uninstall.php reads. With it off, the file returns before touching anything.

StepWith the setting offWith it on
DeactivateNothing is removed.Nothing is removed.
Delete the pluginTables and options stay. Reinstalling picks up where you left off.All seven tables are dropped and every option is deleted.

Pro leaves its own option behind on purpose

Removing Pro does not delete the license option, so reinstalling on the same site does not cost you an activation. Variables, colors and synced patterns stay too; emails that reference them fall back to their stored values until Pro is back.

Reference

Build from source

The admin app is a Vite build. The PHP side needs Composer only for its dev tooling; the shipped plugin autoloads its own classes.

Admin app
pnpm install          # pnpm is enforced by a preinstall guard
pnpm dev              # Vite dev server against a local WP install
pnpm build            # type-check, then build into assets/dist
pnpm type-check
pnpm test             # Vitest
pnpm format           # Prettier over apps/**
ToolVersionWhy
pnpm9.12Pinned through packageManager. npm and yarn are refused by the preinstall script.
Vite6Builds the admin bundle into assets/dist.
React18With TanStack Query for server state and Zustand for editor state.
Tailwind4CSS-first config, prefixed so it cannot collide with WP admin styles.
dnd-kit6Drag and drop in the form builder and the email editor.

The docs site you are reading lives in docs-site/ inside the free plugin and builds separately with pnpm build into a static out/ folder. It is excluded from the distributed zip.