Skip to content

Form Builder — Improvement & Growth Plan

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

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.
  • 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-46M
  • 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.phpS
  • 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.phpM
  • 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:143S
  • 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-159S
  • 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.phpL
  • 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-94S
  • 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.phpM
  • 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-69S
  • 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.
  • 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.

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.

ItemBucketEffortImpactSection ref
Build admin Form CRUD UI (FormResource + schema editor)DoneLCritical§3
Recapture real Form Builder screenshots before promoting marketplace mediaLaterSHigh§4/§5 — deferred until styled runner recapture; no implementation blocker
Stop over-claiming summary/description; rewrite to shipped realityDoneSHigh§5
Exclude spam submissions from reply/notification + add reply gatingDoneSHigh§2/§4
Add tests for file, payment, calculation field paths via LivewireDoneMHigh§4
Done/Shipped: Persist uploaded files to a disk + downloadable submission referenceDoneLHigh§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)DoneMHigh§3
Done/Shipped: Memoise SubmissionSiteAccess permitted-site-ids per requestDoneMMed§2/§4
Shipped 2026-06-08: Wire real payment checkout via capell-paymentsDoneLHigh§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 optionDoneMMed§3
Done/Shipped: CSV export + outbound webhook on submitDoneMMed§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 optionLaterMMed§3
Shipped 2026-06-08: Normalise FormSubmitted event payload across stored/unstored pathsDoneMMed§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 testDoneSMed§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'DoneSLow§2 — FormComponent now reads the schema’s Email field key when building the rate-limit hash.