lara-zeus/bolt
Bolt is a form builder for Laravel that lets you define dynamic forms in code or store them in the database, then render them anywhere in your app with rich fields, validation, and simple customization.
Bolt is a Laravel package that lets you describe forms in PHP (or in the DB, via a Filament admin surface) and have it handle rendering, validation, and submission persistence. The day-to-day value proposition is narrow and real: you stop hand-writing field markup, validation wiring, error display, and submission storage for every new intake/admin form in the app.
Install and publish:
composer require lara-zeus/bolt
php artisan vendor:publish --tag="bolt-migrations"
php artisan migrate
Publish config and views too if the tags exist (bolt-config, bolt-views). You will want to override the Blade templates eventually; if a publish tag doesn't exist, read the service provider to find out what actually ships.
Read these first, in this order:
form_id, field_id, value) or a JSON blob per entry? Normalized rows are queryable and support per-field deletion; JSON is fast to capture and painful to report on. You cannot change this later without a migration project.First use case: one internal, authenticated form end-to-end. A request form, a waitlist, an internal intake. Do not start public-facing. Define three or four fields, render it, submit it, then query the resulting record directly. That single loop tells you empirically whether the storage shape supports the reporting anyone will inevitably ask for, and whether the fluent API's mental model matches how you think about forms.
Then make one explicit decision: are form definitions authored in PHP (committed, code-reviewed, deployed) or in the database (edited live by ops in the admin panel)? Bolt supports both, and DB-authored is the whole point of the package — but author your first form in PHP so it's readable in a diff. Move to DB-authored only for forms that genuinely need non-engineering changes.
Keep form definitions in dedicated classes, never in controllers or Blade.
// app/Forms/OnboardingIntakeForm.php
namespace App\Forms;
use LaraZeus\Bolt\...\Form;
class OnboardingIntakeForm
{
public static function definition(): Form
{
return Form::make('onboarding-intake')
->schema([
// fields + rules, colocated
]);
}
}
One place to diff, one place to test, one place to change when you migrate off the package. Definitions scattered across controllers and views are why form-builder integrations rot.
Wrap submissions behind your own service and your own event. This is the highest-leverage pattern for this integration:
// app/Services/FormSubmissionService.php
class FormSubmissionService
{
public function handle(Form $form, array $payload): Submission
{
$submission = /* call into Bolt */;
event(new FormSubmitted($submission, $form));
return $submission;
}
}
Nothing outside this class imports a LaraZeus\Bolt\* class. Slack notifications, CRM pushes, audit rows, webhooks — all become listeners on your event. If Bolt emits its own events, listen to those internally, but re-emit yours so a package swap is one adapter change, not a codebase-wide grep.
One Livewire shell component per public surface, not one per form. Bolt renders forms dynamically; your job is a stable shell — layout, page chrome, SEO meta, rate limiting, auth policy, spam mitigation. A single PublicFormPage component that takes a form identifier and delegates rendering to Bolt keeps the package's rendering concerns isolated and gives you exactly one place to add protections.
Colocate validation with the field, and mirror it at the boundary. Bolt's built-in validation handles the happy path. Any form accepting data from outside your org needs a second layer you control — a FormRequest or an explicit Validator::make — so rules survive a package upgrade and so you're not debugging validation failures inside vendor code.
Build custom field types on demand, not as a catalog. The extensible architecture is real and it's tempting to build a library up front. Don't. Extend when a form lands that needs something built-ins don't cover: a signature pad, a conditional reveal, a locale-formatted currency input. The abstraction is a cost center until there's demand.
Multi-tenant scoping, if applicable, is yours to enforce. Decide immediately whether forms are global or per-tenant, then enforce it in your own query layer and policies rather than trusting the package. Tenant leakage in a form builder means one customer's intake form appearing in another's admin — cheap to prevent, expensive to explain.
Ship an export on day one. A scheduled command dumping form definitions and entries to JSON/CSV in object storage. It's your exit ramp, your backup, and the fastest way to discover whether the storage shape supports real reporting.
Write one integration test per form. Four assertions: rendering returns 200, a valid payload persists, an invalid payload produces the expected error keys, and the submission event fires. Twenty minutes; catches most upgrade regressions.
Schema evolution is the under-scoped risk. What happens to existing entries when a field is renamed, retyped, or deleted? Typically the old data keeps its shape and the new definition doesn't know about it. Before letting ops edit a live form, set a policy: additive changes only (new optional fields); never retype or delete a field that has submissions. If the package doesn't snapshot field definitions per submission, do it yourself — otherwise a historical entry becomes uninterpretable in six months.
JSON payloads look fine until someone asks for a report. "All submissions where budget > 50k, grouped by month" becomes a PHP loop over every row instead of a SQL GROUP BY. Fine at hundreds of rows, catastrophic at scale. Decide now which forms are capture-only (JSON is acceptable) and which need reporting (maintain a projection table written on submission).
Pin the exact version and treat the Filament major as your real constraint. "lara-zeus/bolt": "5.0.7", not ^5.0. A form builder touches routing, auth, storage, mail, and the admin panel, so it sits on the critical path of every framework upgrade. Record the supported Laravel/PHP/Livewire/Filament matrix in your upgrade docs and add a Bolt smoke test to your Filament upgrade checklist.
Verify the metadata before trusting any of it. The listed last-release date is in the future, which means the release signals you're reading are unreliable. Check tags, commit cadence, and open-issue responsiveness directly. At ~243 stars and zero public dependents, plan for single-maintainer, best-effort maintenance. Read the source for anything you depend on — the community answer pool is thin enough that you'll be writing the Stack Overflow post.
Escape user-authored content everywhere. Labels, help text, and option values are user input rendered in both the admin panel and the public front end. Confirm Blade auto-escaping covers it; anything rendered with {!! !!}, injected into wire: attributes, or passed to JS needs manual review. This is the highest-probability security bug in any form builder.
Public endpoints need hardening you own. Per-IP and per-form rate limits, honeypot or captcha, submission size caps, and a queue for downstream processing so a burst doesn't block the request. Assume Bolt ships none of this and add it at your Livewire shell. Same for uploads: validate MIME and extension independently, cap size, store outside the public root, never trust the client filename.
Guest vs. authenticated submissions behave differently. Auth middleware, CSRF, session lifetime, and user_id capture are your responsibility. Test both paths explicitly — the guest path is where form builders typically go thin.
Debug from the rendered output downward. Field not rendering? First check whether you've published and overridden a view that's now stale (look in resources/views/vendor/). Submission silently failing? Check the validation bag and the transaction boundary before assuming the package swallowed it. Admin resource missing? You probably need to register the plugin on the correct Filament panel in your panel provider — the most common first-run stumble.
Treat forking as a budgeted contingency, not a free option. MIT means you can fork. Keeping a forked Filament plugin current is a real ongoing cost. The wrapper-and-export patterns above are what make "replace" a week rather than a quarter — invest in them before you need them.
How can I help you explore Laravel packages today?