wallacemartinss / filament-onboarding
Database-driven onboarding for Filament: progress checklists and guided spotlight tours, translatable to any locale.
Package info
github.com/wallacemartinss/filament-onboarding
pkg:composer/wallacemartinss/filament-onboarding
Requires
- php: ^8.2
- filament/filament: ^5.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.26
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
- wallacemartinss/filament-icon-picker: ^2.0
Suggests
- wallacemartinss/filament-icon-picker: Turns the icon field of flows and steps into a visual picker over every installed icon set (^2.0).
README
Database-driven onboarding for Filament v5: a progress checklist that follows the user across every page of the panel, guided spotlight tours, and steps that complete themselves โ by a condition your app registers, by reaching a page, or by watching a video.
Journeys are authored in the panel, not in code, so product people can rewrite them without a deploy. And steps bound to a condition catch up on their own, which means users who signed up long before the journey existed enter it already half-done instead of being told to do things they did years ago.
๐ธ More screenshots โ the welcome screen, the progress page, a tour, the video modal, and the side nobody has to write code on
| The welcome screen | The progress page |
|---|---|
![]() |
![]() |
| One moment to introduce itself, and three honest answers. | Where you are, and what is left. |
| A guided tour | A video, docked in a corner |
|---|---|
![]() |
![]() |
| Spotlights the field, explains it, moves on. | The page stays usable behind it โ and the watching is measured. |
| Steps, written in the panel | Conditions, written in the panel |
|---|---|
![]() |
![]() |
| Five ways for a step to finish. Three of them tick themselves. | "Have they added a client yet?" โ a form, not a commit. |
Features
- ๐ Welcome screen โ greets the user once, with "get started", "not now" and "don't show this again"
- โ Floating checklist โ a progress button on every page of the panel, with tabs when there is more than one journey
- ๐งญ Guided spotlight tours โ cross pages, walk through wizards, and wait for the form instead of pointing at nothing
- ๐๏ธ Authored in the panel โ flows, steps and the conditions they hang off are database records with a full admin resource; a new journey is not a deploy
- ๐ช Five completion modes โ by hand, by a condition (retroactively), by visiting a page, by watching a video, or from your own code
- ๐งฉ Conditions without code โ "has at least one client, that is active" is a form, not a commit; and
make:onboarding-conditioncovers the questions a form cannot ask, with nothing to register - ๐๏ธ Visibility conditions โ gate a journey, a step or a single tour stop on a plan or a feature flag
- ๐ฌ Images & videos โ upload (S3, R2, local), direct URL, YouTube or Vimeo; watch time is measured, not guessed
- ๐ Progress page & dashboard widget โ the journey laid out in cards, and the checklist as a card
- ๐ฏ Panel discovery โ destinations, pages and widgets picked from dropdowns built out of your own panel; nobody types a URL
- ๐ Any locale โ content stored per locale and read back in the reader's, with a fallback chain
- ๐ข Multi-tenant โ progress is scoped, so the same user onboards separately in each tenant
- ๐ Events & a fluent API โ
StepCompleted/FlowCompleted, andOnboarding::for($user)for everything else - ๐จ Yours to style โ CSS variables, a replaceable stylesheet, publishable views
- ๐ Server-side guards โ every browser-reachable action re-checks what the interface checked
Requirements
- PHP 8.2+
- Laravel 12+
- Filament 5.0+
- PostgreSQL or MySQL/MariaDB
| Package | Filament |
|---|---|
^2.1 |
v5 |
^1.0 |
v4 (planned) |
2.0.0 is withdrawn โ it lets any authenticated user complete any onboarding step, and a condition that throws returns a 500 on every page of the panel. Require
^2.1(Composer resolves it to 2.2+, whose migration survives the duplicate rows 2.0.0 could write).
Maintaining or extending the package? Read ARCHITECTURE.md โ the data model, panel discovery, the tour runner, the player, asset versioning, and the traps that will bite you if you "clean up" the wrong line.
Table of contents
- Quick start
- Writing a journey
- How a step is completed ยท conditions ยท visibility
- Panel discovery
- Tours ยท a "view the tutorial" button ยท wizards
- Images and videos
- Where onboarding shows up ยท welcome ยท checklist ยท widget ยท progress page
- Programmatic API
- Who onboards, and in what context
- Locales
- Making it look like your product
- Configuration
- Upgrading
Quick start
composer require wallacemartinss/filament-onboarding:^2.1 php artisan vendor:publish --tag=filament-onboarding-migrations php artisan vendor:publish --tag=filament-onboarding-config php artisan migrate php artisan filament:assets
Register the plugin on the panel your users are onboarded in:
use Wallacemartinss\FilamentOnboarding\FilamentOnboardingPlugin; $panel->plugins([ FilamentOnboardingPlugin::make() ->launcher() // floating checklist, on every page ->tours() // guided tours ->progressPage() // optional page laying the journey out ->launcherPosition('bottom-right') // bottom-left, top-right, top-left ->modalPosition('center'), // where images and videos open ]);
And on the panel where journeys are written โ usually an admin panel:
FilamentOnboardingPlugin::make() ->manageFlows() ->launcher(false) ->tours(false) ->navigationGroup(fn (): string => __('navigation.system')) ->navigationIcon('heroicon-o-map') ->navigationSort(70),
->manageFlows() is what puts the two resources in the menu โ the journeys, and the questions they hang off:
That is the whole install. Create a journey in the panel, add its steps, and it shows up โ including the steps that complete themselves. Nothing here needs a deploy.
Publishing
| Tag | What it gives you |
|---|---|
filament-onboarding-config |
config/filament-onboarding.php |
filament-onboarding-migrations |
the five tables, and the columns later releases added |
filament-onboarding-views |
every Blade view โ checklist, launcher, tour popover, media modal, progress page |
filament-onboarding-translations |
the UI strings, per locale |
filament-onboarding-styles |
the stylesheet, to restyle without forking |
Publishing views or translations takes them over: from then on your copies win, and updates to the package no longer reach those files. Publish them when you mean to own them.
Assets (CSS and the Alpine components) are served by Filament from public/, so run php artisan filament:assets after installing and after upgrading. They are versioned by content hash, so a changed file always reaches the browser โ no cache-busting on your side.
Writing a journey
A journey is a row. Its steps are rows. The questions those steps hang off are rows. Which means the person who knows what a new user needs to learn can write it, change it, reorder it and retire it โ without ever asking for a deploy.
A flow says who it is for and where it lives: the panel it belongs to (empty means all of them), the condition that decides who sees it, its place in the order, and whether the user is allowed to hide it for good. Text is written once per locale, so the same journey reads in Portuguese and in English.
The key is the one field that cannot change afterwards โ it is what every progress row points at, and renaming it orphans them. Everything else is yours to rewrite whenever the product does.
Steps are written inside the flow โ and that is where a checklist turns into onboarding, because of the one field that comes next.
How a step is completed
Every step declares how it finishes, and the checklist behaves accordingly โ a step the user cannot tick off themselves is offered as a link, not as a checkbox.
The Completed by column is this whole section. Of the steps above, three wait for the user to tick them off, three are watching the database and will tick themselves, and one is measuring how much of a video got watched.
| Mode | Finishes when |
|---|---|
| Manual | The user ticks it off. |
| Condition | A named question about the user passes โ including retroactively. |
| Page visit | The user reaches a URL (* wildcards allowed). |
| Watching the video | Enough of the step's video has been watched (90% by default). |
| Programmatic | Your code calls Onboarding::for($user)->complete('key'). |
Conditions
A condition is a question about the user โ "have they added a client yet?" โ and it is what makes a step complete itself, including for people who did the thing long before the journey existed. It is picked from a dropdown when the step is written.
Most of them are written in the panel, not in code. Onboarding โ Conditions โ New:
Two shapes cover nearly everything. Counts something they have โ a Product, at least one of them, and only the ones where is_published is true. Asks about them โ email_verified_at is filled in. Both are the same form, and the step editor lists them by name.
The author never types a table name, a column or an operator. The model comes from an allowlist you control, the column from that table's real columns, the operator from a list โ and the value they do type is bound, never interpolated into SQL. What they are really being asked is how does this thing know it belongs to the person being onboarded โ the user_id above โ and that is a question a product person can answer.
For a question a form cannot ask โ an active subscription over at Stripe, a score from a service โ write a class:
php artisan make:onboarding-condition HasActivePlan
// app/Onboarding/Conditions/HasActivePlanCondition.php โ answers to `has_active_plan` class HasActivePlanCondition implements OnboardingCondition, HasConditionLabel { public static function label(): string { return __('Has an active plan'); // what the step editor shows } public function isCompleted(Model $subject, ?Model $scope = null): bool { return $subject->subscription()?->active() ?? false; } }
There is nothing to register. Anything in app/Onboarding/Conditions is found on its own and is in the dropdown โ writing the class and then naming it in a config file would be saying the same thing twice, and the second half is the half that gets forgotten in the deploy where it matters.
You can still register one by hand, for a condition that lives elsewhere or ships in a package of your own:
Onboarding::condition('has_server', fn (User $user, ?Tenant $tenant): bool => $tenant->servers()->exists() );
Code always wins a name clash: a condition written in the panel cannot take a key a class already answers to, so a row can never quietly redefine what a step means.
Conditions are evaluated only for pending steps, and the result is persisted the first time it passes โ so an established account opens the checklist and finds its history already reflected there. A condition that is no longer registered never completes a step, and one that throws answers "no" and is reported: it goes quiet instead of taking the panel down with it.
Visibility: not every journey is for everybody
The same conditions decide who a flow, a step or a single tour stop is for. Pick one under Visibility and what it guards only exists for subjects the condition passes for.
This is how you gate onboarding on a plan. If Docker is not part of the free plan, the panel does not render the Docker card โ and a tour stop pointing at it would spotlight empty space and advertise something the account cannot buy into. Guard the stop with has_docker_feature and the subject is walked from the traditional mode straight to the next stop, as if the Docker stop had never been written.
Onboarding::condition('has_docker_feature', fn (User $user, ?Tenant $tenant): bool => $tenant?->hasFeature('docker_services') ?? false );
Three things follow from it, and they are deliberate:
- A hidden step is not pending โ it is left out of the count, so the percentage is of the journey the subject can actually walk, and they can reach 100% without it.
- A condition that is not registered hides what it guards. If the application cannot answer "is Docker on this plan?", teaching Docker is the wrong default.
- A flow whose every step is hidden is not shown at all, rather than as a card at 0% that can never be finished.
Visibility and completion are different questions asked of the same registry: has_docker_feature decides whether you see the step, has_server decides whether the step is done. A step can use both.
Panel discovery
Nobody types a URL. When a step or a tour stop needs a destination, the dropdown is built from the panel itself:
- Resources โ every list and create page, labelled the way the panel labels them. Pages that need a record (edit, view) are left out, since onboarding has no record to hand them.
- Pages โ every custom page, by its navigation label.
- Widgets โ every widget of the panel, including the ones attached to a page rather than registered on it.
What gets stored is the route name, not the URL, so renaming a resource slug does not break a journey, and {tenant} is filled in at render time.
Tours
A tour spotlights one element at a time, explains it, and moves on. It can cross pages: give a stop a page and the runner navigates there and carries on.
The page dims, the thing being talked about does not, and the popover finds a corner near it. The user can still type in the form underneath โ a tour is a guide, not a modal.
What a stop points at is picked, not typed
Pick the page the stop lives on, and the package reads that page out of your panel:
| Pick this | And it finds |
|---|---|
| Status | the field, label and all โ every field of the form, by the label it wears. A Select and a Toggle too. |
| The save button | the one that submits, on a create page and an edit one. |
| The table ยท The search box | on any list page. |
| Column: Status | the column, by the heading it carries. |
| The "New client" button | it is a link to the create page, and the package has the route. |
| A widget | any widget of the panel. |
| A CSS selector of my ownโฆ | for anything the panel cannot name. |
Nobody has to know CSS, and nobody has to know that a developer went and put a hook in the code. What gets stored is the choice (field:status), not the selector โ same reason a route name is stored and not a URL: the markup underneath is Filament's to change, and a journey should survive it changing.
๐ธ A tour with no CSS in it at all โ four stops, picked from the dropdowns above
Written as field:name, field:status and action:submit, on the create page of a resource. Nothing was added to the application to make this work โ no data-onboarding attribute, no hook, no deploy.
A form that cannot be read gives up its fields, and nothing else. Forms are built to be rendered, and one that leans on the record being edited, or on who is looking, will not survive being asked what is in it outside of a request. Its fields simply are not listed โ the panel does not fall over, and the CSS box is right there.
When you write the selector yourself
For anything the panel cannot name โ a particular card, a paragraph, a third-party widget's insides โ a stable hook on the thing you own is cheapest:
Action::make('create')->extraAttributes(['data-onboarding' => 'create-server']),
And two selectors the package understands that CSS does not:
@widget:App\Filament\...\StatsWidget a widget, by the component it is
@livewire:edit_password_form any Livewire component, by name
The last one matters more than it looks. A form section rendered by a third-party plugin has no hook of its own, and Filament builds a section's id from its heading โ which is translated. Anchoring to #form.update-password::section gives you a tour that works in English and silently spotlights nothing in Portuguese. Addressing the component sidesteps that entirely.
A selector that no longer matches does not break the tour: the page dims and the popover is centred, so the copy still reads.
Tours report the stop the user reached, so a tour abandoned half-way shows as half-way โ and can be resumed.
Finishing a tour completes its step only when the step's completion mode is Manual โ for a manual step, the tour is the task. A tour attached to a step that finishes by a condition, a visit, a video or your own code records that it was watched and leaves completion to the mode: clicking "next" to the end is not proof that two-factor got enabled.
A "view the tutorial" button on the page itself
The launcher starts tours from the checklist. For the pages that deserve their own invitation, drop a header action on them:
use Wallacemartinss\FilamentOnboarding\Actions\StartTourAction; protected function getHeaderActions(): array { return [ StartTourAction::make('servers-tour'), Actions\CreateAction::make(), ]; }
A screen can be walked more than one way. A server is created with a provider, or by bringing one you already own โ two tours, one button. Hand it several keys and it asks which, naming each by its own title, so nobody has to guess what is behind a button:
StartTourAction::make([ 'servers-tour', 'create-server-cloud-tour', 'create-server-byos-tour', ]),
With one tour there is nothing to ask, and it starts outright. Only tours the subject can actually take are offered โ an unknown key, a step with no tour, or one hidden by a visibility condition is left out, and when none survive the button is not there at all. A plan that cannot see the journey never sees the invitation.
Tours that walk through a wizard
The field a stop points at often does not exist yet: it lives on the next step of a wizard, or inside a section that is still closed. The tour handles both directions, without ever taking the mouse away from the user.
The tour follows the user. Advance the wizard and the tour advances with you โ it watches the page, and when the element of a later stop shows up, that is where it goes.
The tour brings the app along. Give a stop an "Advance with" selector โ the control that gets the application there โ and pressing Next on the tour presses it:
[wire\:click="nextStep"] the wizard's own next button
It is clicked only when the user moves on and the element is not on screen โ and it is a real press (mousedown, mouseup, click), because plenty of things do not listen for a bare click: a Filament dropdown, for one, opens on mousedown. If the application refuses (a required field is empty), the tour waits and says so, rather than pointing at nothing.
๐ธ Five stops across three wizard steps โ the user only ever pressed Next on the tour
| 1 ยท Name | 2 ยท SKU |
|---|---|
![]() |
![]() |
| Wizard step 01. | Still 01 โ the tour has no reason to move the app yet. |
| 3 ยท Price | 4 ยท Stock |
|---|---|
![]() |
![]() |
| Wizard step 02 โ the field was not on screen, so the tour pressed the wizard's own Next to get to it. | Still 02. |
| 5 ยท Published | |
|---|---|
![]() |
|
Wizard step 03, reached the same way. A Toggle is a field like any other. |
Skip when it is not there. Some stops are about something an account may not have yet โ a tag on an empty table, a chart with no data. Mark the stop optional and the tour steps aside instead of waiting for something that is never coming.
Images and videos
A step can carry an image to show or a video to watch. Both open in a modal over the panel, from the checklist, the widget or the progress page.
Images are uploaded to the configured disk or addressed by URL, and show as a thumbnail on the step.
Videos come from an upload, a direct .mp4, YouTube or Vimeo โ paste the link however it came, the id is dug out of watch, share and shorts URLs alike. Any other provider can be embedded in an iframe.
// config/filament-onboarding.php 'media' => [ 'disk' => env('FILESYSTEM_DISK', 's3'), // S3, R2, local โ any Laravel disk 'directory' => 'onboarding', 'visibility' => 'private', // signs a temporary URL instead of exposing the file 'url_ttl' => 30, ],
A private disk is signed at render time, so a bucket kept closed stays closed.
The signing decision reads
media.visibilityhere, or'visibility' => 'private'on the disk inconfig/filesystems.php. A bucket that is private on AWS but says nothing in the config gets a plain URL โ which the bucket then refuses. If the files are private, say so in one of those two places.
Watch time is real
The player reports where the subject actually is โ the <video> element for a file, the IFrame API for YouTube, the SDK for Vimeo โ every few seconds and once more on the way out. What is kept is the furthest point reached, so rewinding to rewatch does not undo the ground covered, and the video resumes where it stopped.
That makes the Watching the video completion mode possible: set the threshold (90% by default, since nobody sits through the credits) and the step completes itself when the subject gets there. Until then the card shows the partial percentage.
An iframe embed from an unknown provider cannot be measured, so nothing is invented about it: the video plays, and the step completes some other way.
Where the modal opens
Centred by default. Docked in a corner, it leaves the page usable behind it โ the point when the video is something to follow along with.
FilamentOnboardingPlugin::make()->modalPosition('bottom-right'),
center, top, bottom, top-left, top-right, bottom-left, bottom-right. Any step may override the panel's choice.
Where onboarding shows up
Three surfaces, all optional, all reading the same progress โ tick a step off in one and the others update behind it.
The welcome screen
FilamentOnboardingPlugin::make()->welcome(),
A checklist in the corner is easy to never notice, so onboarding gets one moment to introduce itself: the first page after logging in. It names the journey, says how many steps it is, and offers three answers โ
- Get started โ the progress page (or the checklist, when the panel has no progress page).
- Not now โ gone for this session. Nothing is written down: "later" is an answer about now, and the next login is a new now.
- Do not show this again โ gone for good. The floating button and the ring go with it โ hiding the welcome but leaving the badge blinking in the corner would be answering a different question than the one they were asked.
It is never a dead end: the progress page stays in the menu, and it offers onboarding back. Tours keep working either way โ a "view the tutorial" button is something the user reaches for, not something that reaches for them.
Look at the ring in the corner of that screenshot: 38%, on an account that has never seen this journey. It is an old account, and it already has clients and a published product โ so three of the eight steps were true before the journey was written, and the user is greeted with credit for work they had already done rather than a to-do list of things they finished last year. That is what a condition-backed step buys you, and it is the reason to reach for this package over a checklist you hardcode.
The floating checklist
->launcher() puts a progress button on every page of the panel โ pages, resources, widgets alike, since it hangs off the body. With more than one journey, the panel shows tabs, so a finished journey never sits in front of an unfinished one.
The dashboard widget
use Wallacemartinss\FilamentOnboarding\Widgets\OnboardingChecklistWidget; // In a Dashboard page, or the panel's widgets() OnboardingChecklistWidget::class,
The same checklist, as a card on the page โ it is the panel on the left of the screenshot above, sitting next to the floating one. It hides itself once the journey is finished or dismissed.
The progress page
The journey laid out in cards: a ring with the percentage, counters for done / left / skipped, the next step highlighted with its call to action, and a card per step showing its state and what to do about it.
Every card says what it is waiting for, and finished ones say how they finished โ "Done 8 minutes ago ยท Completes on its own" is a step nobody was asked to tick. The last one there, "Set up weekly reporting", is only shown to accounts with three clients or more: a step gated by a visibility condition is not counted against the people who never see it.
FilamentOnboardingPlugin::make() ->progressPage() ->progressPageSlug('getting-started') // default: onboarding ->progressPageNavigation( label: fn (): string => __('Getting started'), icon: 'heroicon-o-map', group: fn (): string => __('Settings'), sort: 90, ),
->progressPage(shouldRegisterNavigation: false) keeps the page reachable by URL but out of the menu. The page also removes itself from the navigation once a user has no journey left.
Finished is not the same as done with it. A completed step still offers "watch again", "replay the video", "open again" โ none of which undoes anything. Undo is offered separately, and never on a step bound to a condition: unticking it would be undone by the next render, because what it asks about is still true.
Start over clears what the user did โ ticks, skips, tours and videos watched. It cannot clear what is simply true, so steps bound to conditions come straight back, and the page says so rather than looking broken.
Programmatic API
use Wallacemartinss\FilamentOnboarding\Facades\Onboarding; $onboarding = Onboarding::for($user, $tenant); // or Onboarding::current() $onboarding->complete('first-deploy'); $onboarding->skip('invite-team'); $onboarding->uncomplete('first-deploy'); $onboarding->dismiss('getting-started'); $onboarding->restore('getting-started'); $onboarding->reset('getting-started'); $onboarding->handleVisit('/app/acme/servers/create'); $flow = $onboarding->flow('getting-started'); // or ->currentFlow() $flow->percentage(); // 43 $flow->isCompleted(); $flow->nextStep()?->title(); $flow->steps; // StepState[]
StepState answers what the UI needs: isCompleted(), isSkipped(), isPending(), percentage(), canReplay(), canUndo(), url(), tour(), media(), videoProgress(), tourProgress().
Events
use Wallacemartinss\FilamentOnboarding\Events\{StepCompleted, FlowCompleted};
Both carry the step/flow, the progress row, the subject and the scope โ enough to award a credit, send an email, or ping a channel when somebody finishes onboarding.
Command
php artisan onboarding:reset getting-started --subject=1 --scope=<tenant-id> --scope-model="App\Models\Tenant"
Wipes a subject's progress through a journey. Handy while writing one. Steps bound to a condition come straight back โ the command says so.
Who onboards, and in what context
By default the subject is the authenticated user and the scope is the current Filament tenant. Override either:
FilamentOnboardingPlugin::make() ->subject(fn () => auth()->user()) ->scope(fn () => Filament::getTenant()) ->urlParameters(fn (): array => ['tenant' => Filament::getTenant()?->slug]),
urlParameters fills {placeholders} in step URLs, so a step written as /app/{tenant}/servers/create lands on the right tenant.
Locales
List the locales you write content in:
'locales' => ['pt_BR', 'en', 'es'],
The panel then shows one tab per locale for every piece of text. At render time the package reads app()->getLocale() and falls back โ exact locale, then the base language (pt_BR โ pt), then the app fallback, then whatever is filled in. A user reading in Spanish sees Spanish; a user in a locale nobody wrote for still sees a usable checklist.
A plain string is treated as a translation key, so a journey can point at your own language files instead:
'title' => 'onboarding.journey.title', // resolved through __()
Making it look like your product
Three levels, cheapest first.
1. Retheme with variables. The stylesheet reads an override for every value it uses, so declaring these anywhere in your own CSS wins regardless of load order:
:root { --fio-theme-accent: var(--color-brand-600); --fio-theme-accent-dark: var(--color-brand-400); --fio-theme-radius: 1rem; --fio-theme-panel-width: 24rem; }
Available: --fio-theme-{accent,accent-soft,success,surface,surface-muted,border,text,text-muted,shadow,shadow-lg,radius,radius-sm,panel-width}, each with a -dark counterpart.
2. Replace the stylesheet.
php artisan vendor:publish --tag=filament-onboarding-styles
'styles' => [ 'enabled' => true, 'path' => resource_path('css/vendor/filament-onboarding/onboarding.css'), ],
Then php artisan filament:assets. Set enabled => false and no stylesheet ships at all โ the markup keeps its .fio-* classes for you to dress from your own panel theme.
3. Replace the markup.
php artisan vendor:publish --tag=filament-onboarding-views
The Livewire components keep working: they only call completeStep, skipStep, startTour, openMedia, dismissFlow, restartFlow.
Icons
The icon field takes any Blade icon name โ heroicon-o-server, phosphor-rocket. Add wallacemartinss/filament-icon-picker and the field becomes a visual picker on its own; the package detects it and needs no configuration. Without it, the field stays a text input and everything else works the same.
Configuration
config/filament-onboarding.php, in full:
| Key | What it does |
|---|---|
locales |
Locales offered when writing content. |
fallback_locale |
Used when the reader's locale has no content. Defaults to the app fallback. |
conditions |
Conditions registered by hand. Most need nothing here โ they are written in the panel, or found in app/Onboarding/Conditions. |
discovery |
Where condition classes are found. |
conditions_builder |
Which models a condition may be built over in the panel (an allowlist; empty offers app/Models). |
cache |
Journey definitions are cached and flushed on write. Progress is never cached. |
media |
Disk, directory, visibility, signed-URL TTL, accepted types and size limits. |
modal |
Default position of the media modal. |
styles |
Ship the stylesheet, replace it, or turn it off. |
tables / models |
Rename the tables, or swap the models for your own. |
resource |
Navigation of the flow resource, when the plugin does not set it. |
Upgrading
New versions can ship new migrations and new assets:
composer update wallacemartinss/filament-onboarding
php artisan vendor:publish --tag=filament-onboarding-migrations # existing files are left alone
php artisan migrate
php artisan filament:assets
Development
composer install npm install && npm run build # builds the Alpine components and copies the CSS into resources/dist vendor/bin/pint vendor/bin/phpunit
License
MIT. See LICENSE.md.





















