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
Section titled “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(manifestsurfaces). - Key Actions:
CreateSubmissionAction,BuildFormValidationRulesAction,ResolveVisibleFormFieldsAction,EvaluateFormFieldVisibilityAction,CalculateFormFieldValuesAction,CalculateSubmissionSpamScoreAction,BuildFormStepsAction,BuildSubmissionPayloadEntriesAction,SendSubmissionNotificationAction,ReplyToSubmissionAction,ResolveSubmissionReplyAddressAction, plusArchive/MarkRead/MarkSpam. - Models / tables:
Form→ tableforms;Submission→ tablesubmissions. BothbelongsTo Site. Submissionpayload/metacast throughEncryptedDataCast. - Dependencies: requires
capell-app/admin,capell-app/core,capell-app/frontend; supportscapell-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.screenshotscurrently 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)
Section titled “2. Improvements (existing functionality)”- Honeypot still persists a stored submission when
storeSubmissionsis on, double-running spam logic. InFormComponent::submit()the honeypot branch callsCreateSubmissionAction::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:
FormSubmittednow carries a normalized payload contract. Stored and non-stored submissions both exposeFormSubmissionDataplus backwards-compatiblepayloadandmetadataproperties. Stored submissions derive the payload from encryptedSubmissionPayloadData; non-stored submissions go throughDispatchUnstoredFormSubmissionAction, 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.
ResolveSubmissionReplyAddressActionand the table’sreplyaction don’t excludeSubmissionStatus::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
Newsubmission with no rate/aggregation control.CreateSubmissionActioncallsSendSubmissionNotificationActioninline 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 bynotificationEmail, 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()returns0.0for/0anddefault, 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 packageuploads.disk/uploads.directoryconfig, records server-readmime_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 EncryptedDataCastreturns[]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 theencrypted-submissionshealth check. —src/Casts/EncryptedDataCast.php:85-94— S- Done/Shipped:
SubmissionSiteAccessmemoises 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)
Section titled “3. Missing Features (gaps)”- Admin form builder UI now exists as a conventional Filament Form CRUD resource.
FormResourceprovides 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 toform-builder,form-builder-admin. - Done/Shipped: frontend multi-step rendering.
FormComponentnow usesBuildFormStepsAction, 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:FormComponentTestcovers step navigation, validation gating, and final stored payload. — ties toform-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/paymentsis 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 toform-builder-spam-scoring. - Done/Shipped: CSV export and outbound webhook handoff.
BuildSubmissionsCsvActionexports stored submissions with base metadata plus schema/payload field columns,capell:form-builder:export-submissionswrites CSV to stdout or--path, and successful stored submissions can POST a compact JSON payload tosettings.webhook_urlwithout 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
FormSubmissionAutoresponderMailto 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_activeis 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
Section titled “4. Issues / Risks”- Public-output safety — leaked stable record id in DOM ids.
form.blade.phpbuilds field ids fromformInstanceId, but when no instance id is providedFormComponent::resolveInstanceId()falls back to a random UUID per render — acceptable — yetFormComponentTestasserts 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. forceFillonSubmissionbypasses$fillablebut also the encryption guard path is the only protection.CreateSubmissionAction::createSubmission()force-fillspayload/meta; correctness depends entirely on the cast. There is no test asserting the at-rest column is actually ciphertext (theencrypted-submissionshealth check is declaredwarning/admin but coverage is asserted only indirectly). Add a DB-level assertion thatsubmissions.payloadis 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.CalculateFormFieldValuesActionhas 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
FormElementComponentblock path under anonymous users, beyond id existence. Capell requires tests proving anonymous/non-admin output reveals no authoring markers; given the package iscacheable:falseand 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: 20andadminQueryBudget: 40. The admin list path still has real Filament/table query work, but repeatedSubmissionSiteAccesscalls now memoise scoped permission lookups for the same actor and ability set. FrontendRecordExtensionRenderContributionActionis still hard-coded toelapsedMilliseconds: 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) andstr_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 isforms(only the migration filename saysform-builder); a doctor/health check keyed onform-buildertable 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 inmetaand would surface untranslated if shown;BuildSubmissionPayloadEntriesActionvalue formatting and the file-metadata array keys (original_nameetc.) 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-partypackage. —CHANGELOG.md— polish.
5. Marketplace & Selling
Section titled “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
Section titled “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. |