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.
| Free | Pro | |
|---|---|---|
| Slug | flexa-formflow | flexa-formflow-pro |
| Version | 1.3.3 | 1.0.0 |
| Namespace | Flexa\\FormFlow | Flexa\\FormFlowPro |
| Text domain | flexa-formflow | flexa-formflow-pro |
| REST namespace | flexa-formflow/v1 | flexa-formflow-pro/v1 |
| Custom tables | 7 | none |
Reference
Requirements
| Requirement | Minimum | Note |
|---|---|---|
| WordPress | 6.5 | Declared in the plugin header. |
| PHP | 8.1 | The plugin disables itself and prints a notice below this version. |
| WooCommerce | Optional | Without it the Woo email takeover and the order blocks stay off. Everything else runs. |
| MySQL / MariaDB | Whatever WordPress supports | Tables are created with dbDelta and the site charset and collation. |
Pro needs Free
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.
| Layer | Where | What it does |
|---|---|---|
| Boot | src/Plugin.php | Loads the text domain, registers the REST router, the shortcode and block, the mailer, the workflow engine, and the admin screens. |
| Domain | src/Domain/ | Forms, Entries, EmailTemplates and Workflows, each an entity plus a repository over its table. |
| Emails | src/Emails/ | The element registry, the renderer, the token resolver, the global header and footer layout, and the notification mailer. |
| Workflows | src/Workflows/ | The engine that walks a workflow config, runs actions and writes a run log. |
| Spam | src/Spam/ | Honeypot, minimum fill time, rate limits and CAPTCHA, with counters in a throttle table. |
| WooCommerce | src/WooCommerce/ | Registered only when WooCommerce is active. Intercepts Woo email templates and adds order tokens and blocks. |
| Packs | src/Packs/ | Industry packs: bundles of forms, emails and workflows installed and updated as a unit. |
| Admin app | apps/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.
{
"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.
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
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.
| Table | Holds | Key columns |
|---|---|---|
flexa_formflow_forms | One row per form. | uuid (unique), title, status, config LONGTEXT |
flexa_formflow_entries | One row per submission. | form_id, status, data, meta, created_at |
flexa_formflow_email_templates | Emails, Woo designs, patterns and language versions. | title, tree LONGTEXT |
flexa_formflow_workflows | One row per workflow. | title, status, config LONGTEXT |
flexa_formflow_library | Saved reusable items and pack provenance. | uuid, type, kind, payload, source_pack, source_hash |
flexa_formflow_workflow_runs | Run history, one row per fired workflow. | workflow_id, entry_id, status, log |
flexa_formflow_throttle | Short-lived spam counters. | bucket CHAR(64) primary key, hits, expires_at |
The throttle table holds no identifiers
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
| Option | Autoloaded | Holds |
|---|---|---|
flexa_formflow_settings | Yes | Brand color, email defaults, CAPTCHA keys, AI provider and key, uninstall behavior. |
flexa_formflow_email_layout | Yes | The global header and footer sets and their assignments. |
flexa_formflow_connections | Yes | Saved integration connections, by integration id. |
flexa_formflow_installed_packs | Yes | Which industry packs are installed, and at what version. |
flexa_formflow_onboarding | Yes | Welcome guide progress. |
flexa_formflow_review | Yes | Review prompt state. |
flexa_formflow_db_version | Yes | Schema version, compared against Schema::DB_VERSION. |
flexa_formflow_render_rev | No | Render cache revision, bumped to invalidate cached markup. |
flexa_formflow_pro_license | Yes | Pro. The key, its instance token, status and the last server snapshot. |
flexa_formflow_pro_variables | Yes | Pro. Custom variables, with per-language values. |
flexa_formflow_pro_palette | Yes | Pro. Brand colors, by key. |
flexa_formflow_pro_languages | Yes | Pro. Language settings, per-language footers, provider choice. |
flexa_formflow_pro_woo_emails | Yes | Pro. The WooCommerce emails other plugins register. |
flexa_formflow_pro_email_vars | Yes | Pro. Argument names Woo emails have passed, for the Dynamic data list. |
Secrets are encrypted at rest
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
| Method | Route |
|---|---|
| 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
| Method | Route |
|---|---|
| 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
| Method | Route |
|---|---|
| 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
| Method | Route |
|---|---|
| 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
| Method | Route |
|---|---|
| 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.
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.
| Token | Value | Available in |
|---|---|---|
{site_title} | Site title | All emails |
{site_tagline} | Site tagline | All emails |
{site_url} | Home URL | All emails |
{site_logo_url} | Site logo image URL, empty when none is set | All emails |
{admin_email} | Admin email address | All emails |
{year} | Current year | All emails |
{form_title} | Form title | Form emails |
{entry_id} | Entry id | Form emails |
{entry_date} | Submission date, localized | Form emails |
{page_url} | URL of the page the form was on | Form emails |
{field:ID} | One answer from the entry | Form 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_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.
{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 receiptsOrderTokens::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
| Prefix | Example | Source |
|---|---|---|
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
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
| Hook | Type | Use |
|---|---|---|
flexa_formflow.forms.field_types | filter | Add a field type descriptor (label, icon, input, fields, defaults). |
flexa_formflow.forms.item_types | filter | Add a non-field item, such as the Pro Step marker. |
flexa_formflow.forms.field_options | filter | Supply the choices for one list field at render time. |
flexa_formflow.forms.field_value | filter | Validate and normalize a submitted value. Return a WP_Error to reject it. |
flexa_formflow.forms.settings_fields | filter | Add controls to the form settings panel. |
flexa_formflow.forms.register_assets | action | Register a script or style the builder can attach to a form. |
flexa_formflow.forms.assets | filter | Decide which registered handles a given form loads. |
flexa_formflow.submission.validate | filter | Whole-submission validation, after per-field checks. |
flexa_formflow.entry.created | action | Fires once an entry is stored. |
flexa_formflow.form.saved | action | Fires when a form is created or updated. |
flexa_formflow.spam.rate_limits | filter | Change the rate-limit buckets and their windows. |
flexa_formflow.spam.min_seconds | filter | Change the minimum fill time before a submission is accepted. |
flexa_formflow.spam.client_ip | filter | Resolve the client IP behind a proxy or CDN. |
flexa_formflow.captcha.providers | filter | Register another CAPTCHA provider. |
flexa_formflow.lookup.sources | filter | Add a source the builder can pull list choices from. |
Extension points
Hooks · emails
| Hook | Type | Use |
|---|---|---|
flexa_formflow.emails.element_types | filter | Declare a block type for the inserter and the inspector. |
flexa_formflow.emails.elements | filter | Register the PHP renderer for a block type. |
flexa_formflow.emails.tokens | filter | Add resolved token values for the current render. |
flexa_formflow.emails.token_fallbacks | filter | What a token prints on a real send when it has no value. |
flexa_formflow.emails.html_tokens | filter | Mark a token as trusted markup rather than plain text. |
flexa_formflow.emails.dynamic_data | filter | Add rows to the Dynamic data list in the editor. |
flexa_formflow.emails.settings_fields | filter | Add controls to the Template design panel. Pro attaches Attachments here. |
flexa_formflow.emails.attachments | filter | Files to send with a rendered email. |
flexa_formflow.emails.patterns | filter | Add ready-made block patterns to the inserter. |
flexa_formflow.emails.pattern_categories | filter | Add a category those patterns are listed under. |
flexa_formflow.emails.block_actions | filter | Add an action to a block toolbar, such as "Save as synced". |
flexa_formflow.emails.node_visible | filter | Decide whether one block renders in the current context. |
flexa_formflow.emails.default_tree | filter | The starting tree for a newly created email. |
flexa_formflow.emails.editor_tabs | filter | Tabs above the editor canvas. Pro puts its language versions here. |
flexa_formflow.emails.locale | filter | Which language an email goes out in. |
flexa_formflow.emails.template_id | filter | Swap the template actually rendered, by id. |
flexa_formflow.emails.subject | filter | Change the subject line for a send. |
flexa_formflow.emails.layout_set | filter | Which global header and footer set applies. |
flexa_formflow.emails.footer_text | filter | The footer line, per send. |
flexa_formflow.emails.switch_locale | action | Fires as the renderer switches language; switch your translation plugin here. |
flexa_formflow.emails.restore_locale | action | The matching restore. |
flexa_formflow.emails.template_deleted | action | Clean up anything keyed to a deleted template. |
flexa_formflow.emails.site_logo_url | filter | Override the logo the Logo block uses. |
flexa_formflow.colors.palette | filter | Named colors offered by every color control. Pro fills this. |
flexa_formflow.colors.resolve | filter | Turn a palette name into a hex value at render time. |
flexa_formflow.notification.sent | action | Fires after a notification email is handed to wp_mail. |
flexa_formflow.woo.catalog | filter | Add emails to the WooCommerce page. Pro adds other plugins’ emails here. |
flexa_formflow.woo.default_tree | filter | The starting design for one WooCommerce email. |
flexa_formflow.woo.item_details | filter | Extra detail rows under a line item. |
flexa_formflow.woo.email_saved | action | Fires 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 node | Class | Tested against | Combines with |
|---|---|---|---|
visibility | Emails\Render\Visibility | The entry. Rules name a form field by id. | match is all or any. |
conditions | WooCommerce\Conditions | The WC_Order. Skipped when the render has no order. | AND only: every condition must pass. |
// 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
| Hook | Type | Use |
|---|---|---|
flexa_formflow.workflows.action_types | filter | Declare an action node as data: label, icon, group, fields, locked state. |
flexa_formflow.workflows.run_action | filter | Handle your node at runtime and return a log entry. |
flexa_formflow.workflow.ran | action | Fires once a workflow finishes, with its run log. |
flexa_formflow.integrations.catalog | filter | Add or change a card on the Integrations page. |
flexa_formflow.integrations.connect_fields | filter | Describe the connect drawer for one integration. |
flexa_formflow.integrations.bridge | filter | Route an outbound call through your own transport. |
flexa_formflow.library.catalog | filter | Add items to the Library. |
flexa_formflow.packs | filter | Register an industry pack. |
Extension points
Hooks · Pro
| Hook | Type | Use |
|---|---|---|
flexa_formflow_pro.is_licensed | filter | Final say on whether Pro features are unlocked. |
flexa_formflow_pro.license_api_base | filter | Point the license check at another host. |
flexa_formflow_pro.product_slug | filter | The product slug sent with a license check. |
flexa_formflow_pro.rest.register_routes | action | Register routes in the Pro namespace. |
flexa_formflow_pro.tracking.shipments | filter | Add shipments from a tracking plugin that is not read natively. |
flexa_formflow_pro.order_fields.labels | filter | Give an order meta key a human label. |
flexa_formflow_pro.offers.code | filter | Replace the coupon code the Offer block prints. |
flexa_formflow_pro.emails.link | filter | Change a link Pro blocks build, for tracking parameters. |
flexa_formflow.pro_active | filter | Read 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.
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| Situation | Result |
|---|---|
| Status active | Pro features unlocked. |
| Service unreachable | Status moves to grace. Features stay unlocked for 14 days from validated_at. |
| Grace window elapsed | Features lock. Placed features keep rendering; see the Pro guide. |
| Service answers expired, revoked or wrong key | Features lock immediately, with no grace. |
FLEXA_FORMFLOW_PRO_UNLOCK | Constant 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.
add_filter( 'flexa_formflow.capabilities.manage', fn() => 'manage_woocommerce' );
add_filter( 'flexa_formflow.capabilities.settings', fn() => 'manage_options' );| Filter | Guards |
|---|---|
flexa_formflow.capabilities.manage | The admin app and every REST route except the public submit endpoint. |
flexa_formflow.capabilities.settings | The Settings screen and the settings routes, including the AI and CAPTCHA keys. |
Split them deliberately
settings at manage_options while loosening manage lets an editor build forms without being able to read or change API keys.Reference
Internationalization
| Free | Pro | |
|---|---|---|
| Text domain | flexa-formflow | flexa-formflow-pro |
| Bundled translations | i18n/languages/ | i18n/languages/ |
| Loaded on | init | init |
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
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.
| Step | With the setting off | With it on |
|---|---|---|
| Deactivate | Nothing is removed. | Nothing is removed. |
| Delete the plugin | Tables 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
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.
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/**| Tool | Version | Why |
|---|---|---|
| pnpm | 9.12 | Pinned through packageManager. npm and yarn are refused by the preinstall script. |
| Vite | 6 | Builds the admin bundle into assets/dist. |
| React | 18 | With TanStack Query for server state and Zustand for editor state. |
| Tailwind | 4 | CSS-first config, prefixed so it cannot collide with WP admin styles. |
| dnd-kit | 6 | Drag 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.