# Form Builder — Improvement & Growth Plan

> Package: capell-app/form-builder · Kind: package · Tier: premium · Product group: Capell FormBuilder · Bundle: form-builder · Status: Draft

## 1. Snapshot

Form Builder lets a Capell site define forms (name, handle, JSON field `schema`, JSON `settings`, `is_active`) scoped per `site_id`, render them on the public frontend via a Livewire component, and capture **encrypted** submissions into an admin inbox with status workflows (new / read / archived / spam), spam scoring, and email reply. Domain logic is correctly factored into Actions under `src/Actions`; the public surface is a single non-cacheable Livewire form; the admin surface now includes a conventional Filament Form CRUD resource plus the read/triage submissions resource.

- **Surfaces:** `admin`, `frontend` (manifest `surfaces`).
- **Key Actions:** `CreateSubmissionAction`, `BuildFormValidationRulesAction`, `ResolveVisibleFormFieldsAction`, `EvaluateFormFieldVisibilityAction`, `CalculateFormFieldValuesAction`, `CalculateSubmissionSpamScoreAction`, `BuildFormStepsAction`, `BuildSubmissionPayloadEntriesAction`, `SendSubmissionNotificationAction`, `ReplyToSubmissionAction`, `ResolveSubmissionReplyAddressAction`, plus `Archive/MarkRead/MarkSpam`.
- **Models / tables:** `Form` → table `forms`; `Submission` → table `submissions`. Both `belongsTo Site`. Submission `payload`/`meta` cast through `EncryptedDataCast`.
- **Dependencies:** requires `capell-app/admin`, `capell-app/core`, `capell-app/frontend`; supports `capell-app/payments`; no conflicts.
- **Marketplace summary (verbatim):** "FormBuilder adds form definitions, encrypted submissions, frontend Livewire rendering, conditional logic, multi-step forms, calculations, file upload rules, spam scoring, payment fields, and submission workflows to Capell."
- **Screenshots:** manifest `marketplace.screenshots` currently declares only the extension-card preview. The prior runtime PNGs were demoted because they showed Form Mappings rather than Form Builder schema, submissions, submission detail, or frontend output.

## 2. Improvements (existing functionality)

- **Honeypot still persists a stored submission when `storeSubmissions` is on, double-running spam logic.** In `FormComponent::submit()` the honeypot branch calls `CreateSubmissionAction::run(...)`, which _again_ checks the honeypot and recomputes the spam score; the component already knows it is spam. This is duplicated work and the component path bypasses the action's own early-return shape. Collapse to a single decision point so the component always delegates to the action and never branches on honeypot itself. — `src/Livewire/FormComponent.php:75-88`, `src/Actions/CreateSubmissionAction.php:38-46` — **M**
- **Shipped 2026-06-08: `FormSubmitted` now carries a normalized payload contract.** Stored and non-stored submissions both expose `FormSubmissionData` plus backwards-compatible `payload` and `metadata` properties. Stored submissions derive the payload from encrypted `SubmissionPayloadData`; non-stored submissions go through `DispatchUnstoredFormSubmissionAction`, preserving spam checks while avoiding a row insert. Public Actions and Campaign Studio listener tests now exercise the real event shape.
- **Spam-scored submissions still resolve a reply-to and surface a Reply button.** `ResolveSubmissionReplyAddressAction` and the table's `reply` action don't exclude `SubmissionStatus::Spam`, so an admin can be invited to reply to a spam entry's attacker-supplied address. Gate reply/notification on non-spam status. — `src/Filament/Resources/Submissions/Tables/SubmissionsTable.php:137-141`, `src/Actions/ResolveSubmissionReplyAddressAction.php` — **S**
- **Notification is sent for every `New` submission with no rate/aggregation control.** `CreateSubmissionAction` calls `SendSubmissionNotificationAction` inline on each store; a high-traffic form floods the notification inbox and runs a sync resolve+queue per submit inside the request. Add a per-form notification toggle already implied by `notificationEmail`, plus optional digest/throttle. — `src/Actions/CreateSubmissionAction.php:51`, `src/Actions/SendSubmissionNotificationAction.php` — **M**
- **Rate-limit key trusts a field literally named `email`.** `FormComponent::rateLimitKey()` reads `$this->data['email']`; forms whose email field uses a different key (e.g. `contact_email`) silently lose the per-email dimension and fall back to IP-only throttling. Derive the email dimension from the resolved Email field key instead of a hard-coded `'email'`. — `src/Livewire/FormComponent.php:143` — **S**
- **Calculation evaluator silently swallows divide-by-zero and unknown tokens as `0`.** `evaluateReversePolish()` returns `0.0` for `/0` and `default`, so a misconfigured expression yields a plausible-looking wrong number with no signal. Surface a validation/log path or NaN sentinel for authoring feedback. — `src/Actions/CalculateFormFieldValuesAction.php:153-159` — **S**
- **Done/Shipped: file-upload submissions persist files to a configured disk.** `CreateSubmissionAction::storedValue()` now stores uploaded files through Laravel Storage using the package `uploads.disk`/`uploads.directory` config, records server-read `mime_type`/`size`, and keeps the encrypted submission payload as a downloadable disk/path reference for admins. — `src/Actions/CreateSubmissionAction.php`, `src/Actions/BuildSubmissionPayloadEntriesAction.php`, `tests/Feature/FormComponentTest.php` — **L**
- **`EncryptedDataCast` returns `[]` on a JSON decode failure, masking corruption.** `decodeStoredPayload()` falls back to an empty array, so a tampered/garbled ciphertext renders as an empty submission rather than an error. Log/flag undecodable rows for the `encrypted-submissions` health check. — `src/Casts/EncryptedDataCast.php:85-94` — **S**
- **Done/Shipped: `SubmissionSiteAccess` memoises permitted site IDs per actor and ability set.** Repeated admin list/table/filter/policy calls now reuse the first role/direct permission lookup for the same actor, ability set, team column, and permission table config. A test pins that a second lookup does not re-query the scoped permission pivot tables. — `src/Support/SubmissionSiteAccess.php`, `tests/Feature/Filament/SubmissionsResourceTest.php` — **M**
- **No admin list column or filter for spam score / reasons,** even though they are computed and stored in `meta`. Triagers can't sort by spam likelihood. Add a score column + "likely spam" filter. — `src/Filament/Resources/Submissions/Tables/SubmissionsTable.php:34-69` — **S**

## 3. Missing Features (gaps)

- **Admin form builder UI now exists as a conventional Filament Form CRUD resource.** `FormResource` provides list/create/edit pages for core form details, field schema, and settings. It is not yet a drag-and-drop visual builder, but forms no longer require factory/seed/code creation. — ties to `form-builder`, `form-builder-admin`.
- **Done/Shipped: frontend multi-step rendering.** `FormComponent` now uses `BuildFormStepsAction`, tracks the current step, validates the active step before advancing, renders step progress/navigation, and only submits on the final step while preserving payload storage across all visible fields. Evidence: `FormComponentTest` covers step navigation, validation gating, and final stored payload. — ties to `form-builder-multi-step`.
- **Shipped 2026-06-08: payment fields hand off to Payments checkout.** Fixed payment fields now render as a read-only payment summary with the configured amount stored in form data, successful stored submissions redirect to Payments' signed form-checkout URL when `capell-app/payments` is installed, and Payments continues to create provider checkout sessions from the encrypted Form Builder submission payload. Variable payment fields remain supported for donation-style amounts.
- **Done/Shipped: file persistence and retrieval reference for uploads.** Uploaded files are now stored on the configured disk and submission details render the original filename plus disk/path reference so operators can retrieve the stored file. A richer signed download button remains possible later, but the file is no longer discarded. — ties to `form-builder-file-upload-rules`.
- **CAPTCHA / external spam provider (table-stakes alongside honeypot).** Spam defence is honeypot + heuristic keyword/link scoring only (`CalculateSubmissionSpamScoreAction`). Competitors (Gravity Forms, Fluent Forms, Formidable) offer reCAPTCHA/hCaptcha/Turnstile. — ties to `form-builder-spam-scoring`.
- **Done/Shipped: CSV export and outbound webhook handoff.** `BuildSubmissionsCsvAction` exports stored submissions with base metadata plus schema/payload field columns, `capell:form-builder:export-submissions` writes CSV to stdout or `--path`, and successful stored submissions can POST a compact JSON payload to `settings.webhook_url` without failing the stored submission when the remote endpoint errors. XLSX remains future polish; the table-stakes export/automation handoff is now present. — competitor norm.
- **Done/Shipped: submitter autoresponder email.** Form settings now include autoresponder subject/body fields; successful non-spam stored submissions resolve the configured Email field and queue `FormSubmissionAutoresponderMail` to the submitter. Missing email, missing subject/body, and spam submissions do not send. — competitor norm.
- **Save-and-resume / partial submissions, and per-form submission limits / scheduling (open/close window) (differentiators).** None present; `is_active` is a binary on/off with no date window or cap.
- **Done/Shipped: redirect-on-success option.** Form settings now include `success_redirect_url`; the public Livewire form redirects after a successful submission when configured, while preserving the existing inline success message fallback.

## 4. Issues / Risks

- **Public-output safety — leaked stable record id in DOM ids.** `form.blade.php` builds field ids from `formInstanceId`, but when no instance id is provided `FormComponent::resolveInstanceId()` falls back to a random UUID _per render_ — acceptable — yet `FormComponentTest` asserts ids of the form `#capell-form-{handle}-…`/`#capell-form-{instanceId}-…`. The honeypot field key is author-defined and rendered verbatim as a DOM id/`wire:model` (`data.{key}`), so a poorly named honeypot ("honeypot"/"spam_trap") advertises itself to bots. Recommend rendering spam-trap fields with a neutralised, non-guessable name attribute. — `resources/views/livewire/form.blade.php:100-109` — public-output-safety.
- **`forceFill` on `Submission` bypasses `$fillable` but also the encryption guard path is the only protection.** `CreateSubmissionAction::createSubmission()` force-fills `payload`/`meta`; correctness depends entirely on the cast. There is no test asserting the at-rest column is actually ciphertext (the `encrypted-submissions` health check is declared `warning`/admin but coverage is asserted only indirectly). Add a DB-level assertion that `submissions.payload` is not plaintext. — `src/Actions/CreateSubmissionAction.php:59-76` — security.
- **Thin/missing test coverage for three advertised field types.** `tests/Feature/FormComponentTest.php` (719 lines) covers honeypot/spam ordering, conditional visibility, select+checkbox validation, rate limiting, notification mail, and instance-id rendering — but there is **no test** exercising a **file** upload submission, a **payment** field, or a **calculation** field through the Livewire component. `CalculateFormFieldValuesAction` has a unit test, but the rendered/stored path is unverified. — `tests/Feature/FormComponentTest.php`, `tests/Unit/Actions/CalculateFormFieldValuesActionTest.php` — test debt.
- **No frontend rendering-safety test for the `FormElementComponent` block path under anonymous users**, beyond id existence. Capell requires tests proving anonymous/non-admin output reveals no authoring markers; given the package is `cacheable:false` and renders author-named fields, an explicit "no admin markers / no model id leak for anonymous" test is warranted. — `tests/Feature/FormComponentTest.php` — public-output-safety.
- **Performance budget risk reduced.** Manifest sets `frontendRenderBudgetMs: 20` and `adminQueryBudget: 40`. The admin list path still has real Filament/table query work, but repeated `SubmissionSiteAccess` calls now memoise scoped permission lookups for the same actor and ability set. Frontend `RecordExtensionRenderContributionAction` is still hard-coded to `elapsedMilliseconds: 0.0`, so the 20ms budget is not actually measured/enforced. — `src/Support/SubmissionSiteAccess.php`, `src/Livewire/FormComponent.php:359-372`, `src/Livewire/FormElementComponent.php:46-61` — performance.
- **Spam keyword scan is O(keywords × full submission text) and unbounded.** `CalculateSubmissionSpamScoreAction::submittedText()` flattens _all_ values (including large textareas up to 10000 chars) and `str_contains`-scans per keyword on every submit, inside the request. Fine at small keyword lists; document/cap it. — `src/Actions/CalculateSubmissionSpamScoreAction.php:44-49,96-101` — performance.
- **Manifest table mismatch.** `database.requiredTables` = `["form-builder","submissions"]` but the real table is **`forms`** (only the migration _filename_ says `form-builder`); a doctor/health check keyed on `form-builder` table existence would mis-report. The marketplace screenshot set is now reconciled to the shipped PNGs. — `capell.json` (`database.requiredTables`), `database/migrations/..._01_create_form-builder_table.php` — tech debt / health-check correctness.
- **i18n gaps.** Field-type labels, status labels, and form chrome are translated, but spam `reasons` (`'honeypot'`, `'too_many_links'`, `'blocked_keyword:…'`) are raw English strings stored in `meta` and would surface untranslated if shown; `BuildSubmissionPayloadEntriesAction` value formatting and the file-metadata array keys (`original_name` etc.) are not localised. — `src/Actions/CalculateSubmissionSpamScoreAction.php:31-58` — i18n.
- **CHANGELOG is effectively empty** ("Unreleased — Prepared package metadata…"), so buyers have no version history signal for a `paid`/`first-party` package. — `CHANGELOG.md` — polish.

## 5. Marketplace & Selling

**Critique.** The manifest `marketplace.summary` and composer `description` are the _same_ sentence and read as a comma-separated feature dump ("…conditional logic, multi-step forms, calculations, file upload rules, spam scoring, payment fields…"). Two problems: (1) it's a list, not a value proposition, and (2) it over-claims — multi-step isn't rendered, payment isn't processed, file uploads aren't stored (§3). The composer `description` ("FormBuilder package for Capell CMS") is uninformative. Lead with the encrypted-at-rest + site-scoped triage inbox, which is what actually ships and works well.

- **Improved 1-sentence summary:** "Build site-scoped forms in Capell and capture spam-filtered, encrypted submissions into a per-site triage inbox with one-click email replies."
- **Improved listing description (3–4 sentences):** "Form Builder gives each Capell site its own forms with conditional fields, calculated values, honeypot + heuristic spam scoring, and configurable file-upload rules. Every submission is encrypted at rest and lands in a site-scoped admin inbox where staff can read, archive, mark spam, and reply by email without leaving the panel. Frontend forms render through a cache-safe Livewire component with built-in throttling and accessible markup. Pairs with Email Studio and Public Actions to turn submissions into automated workflows." _(Keep payment/multi-step out of the headline until §3 lands; gate those claims behind shipped functionality.)_

**Screenshot / media gaps.** The manifest now surfaces only the extension card. Recapture real Form Builder admin index, schema builder, submissions index/detail, and frontend output screens, then add a short GIF of the frontend submit-to-inbox-to-reply loop, since the triage workflow is the real value.

**Pricing / tier / bundle.** `premium` / `paid` / `first-party` is defensible for encrypted submissions + site-scoped RBAC now that a conventional form-builder UI exists. Keep it in its own `form-builder` bundle. Strong cross-sell: hard-wire the `supports: capell-app/payments` relationship into a real paid-form upsell, and lean on the declared "Best Used With" (Public Actions, Email Studio, Campaign Studio) as an Extension Suite — submissions-as-triggers is the natural bundle story.

**Top differentiators / value props / persona.** Differentiators: (1) per-site RBAC scoping of submissions via Shield permissions (`SubmissionSiteAccess`), uncommon in form plugins; (2) encrypted-at-rest payload + meta; (3) cache-safe Livewire rendering inside a CMS. Target buyer: a multi-site agency or in-house team running several Capell sites who needs GDPR-friendly, access-controlled lead capture without a third-party form SaaS.

**Suggested keywords/tags:** `form builder`, `contact form`, `submissions`, `encrypted submissions`, `lead capture`, `spam protection`, `honeypot`, `conditional logic`, `livewire forms`, `multi-site forms`, `gdpr`, `filament`.

## 6. Prioritized Roadmap

| Item                                                                                    | Bucket | Effort | Impact   | Section ref                                                                                                                                                                                                                        |
| --------------------------------------------------------------------------------------- | ------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Build admin Form CRUD UI (FormResource + schema editor)                                 | Done   | L      | Critical | §3                                                                                                                                                                                                                                 |
| Recapture real Form Builder screenshots before promoting marketplace media              | Later  | S      | High     | §4/§5 — deferred until styled runner recapture; no implementation blocker                                                                                                                                                          |
| Stop over-claiming summary/description; rewrite to shipped reality                      | Done   | S      | High     | §5                                                                                                                                                                                                                                 |
| Exclude spam submissions from reply/notification + add reply gating                     | Done   | S      | High     | §2/§4                                                                                                                                                                                                                              |
| Add tests for file, payment, calculation field paths via Livewire                       | Done   | M      | High     | §4                                                                                                                                                                                                                                 |
| Done/Shipped: Persist uploaded files to a disk + downloadable submission reference      | Done   | L      | High     | §2/§3 — `CreateSubmissionAction` stores uploads on the configured disk, records server file metadata, and formats the submission payload as a disk/path reference.                                                                 |
| Done/Shipped: Render multi-step (wire BuildFormStepsAction into the Blade)              | Done   | M      | High     | §3                                                                                                                                                                                                                                 |
| Done/Shipped: Memoise SubmissionSiteAccess permitted-site-ids per request               | Done   | M      | Med      | §2/§4                                                                                                                                                                                                                              |
| Shipped 2026-06-08: Wire real payment checkout via capell-payments                      | Done   | L      | High     | §3 — fixed payment fields render as checkout summaries, Form Builder redirects stored successful submissions to Payments' signed checkout URL when available, and existing Payments checkout session tests cover provider handoff. |
| Done/Shipped: Submitter autoresponder + success redirect option                         | Done   | M      | Med      | §3                                                                                                                                                                                                                                 |
| Done/Shipped: CSV export + outbound webhook on submit                                   | Done   | M      | Med      | §3 — CSV export ships through `BuildSubmissionsCsvAction` and `capell:form-builder:export-submissions`; per-form `webhook_url` dispatches successful stored submissions with failure isolation.                                    |
| CAPTCHA/Turnstile spam provider option                                                  | Later  | M      | Med      | §3                                                                                                                                                                                                                                 |
| Shipped 2026-06-08: Normalise FormSubmitted event payload across stored/unstored paths  | Done   | M      | Med      | §2 — `FormSubmissionData` gives listeners one typed payload/metadata contract while preserving legacy `payload` and `metadata` event properties.                                                                                   |
| Shipped 2026-06-06: Correct requiredTables (`forms`) + at-rest-encryption DB test       | Done   | S      | Med      | §4 — `capell.json` now declares `forms`/`submissions`; existing model coverage asserts ciphertext at rest and structured readback.                                                                                                 |
| Shipped 2026-06-06: Derive throttle email dimension from Email field key, not `'email'` | Done   | S      | Low      | §2 — `FormComponent` now reads the schema's Email field key when building the rate-limit hash.                                                                                                                                     |