Adding a CSV import wizard to your Filament panel
Adding a CSV import wizard to your Filament panel
Filament panels already give you resource management, form builders, table views, and a full admin shell. What they do not give you is a multi-step import wizard with inline validation, relationship resolution, and queue-powered execution. Filament's built-in Import Action handles simple cases well, but when your users need to preview data, fix errors before committing, and link CSV columns to related models, you need something purpose-built.
Tapix was designed as a Filament plugin from day one. The integration adds an import wizard and an import history page to your existing panel -- same authentication, same sidebar, same tenant context. This tutorial walks through the full setup: installing the package, registering the plugin, enabling auto-discovery, and customizing the navigation to fit your panel's structure.
#Step 1: Install the package
Tapix ships as a Composer package. Require it in your project and publish the configuration file:
composer require tapix/core
php artisan vendor:publish --tag=tapix-config
This creates config/tapix.php with sensible defaults for queue configuration, chunk sizes, table prefixes, and tenant settings. You can adjust these later, but the defaults work out of the box for most applications.
If you do not already have importers in your project, generate one to have something to work with:
php artisan make:tapix-importer Contact
This creates app/Importers/ContactImporter.php with a skeleton class extending BaseImporter. The skeleton includes the two methods every importer needs: model() to declare the target Eloquent model, and fields() to define importable columns with the ImportField builder. For customizing behavior at each stage of the import -- ownership assignment, relationship resolution, cache warming -- see CSV import lifecycle hooks: running custom logic before, during, and after import.
#Step 2: Register the plugin
Open your panel provider -- typically app/Providers/Filament/AdminPanelProvider.php -- and add the TapixPlugin to your panel's plugin chain:
use Tapix\Core\Filament\TapixPlugin;
use App\Importers\ContactImporter;
use App\Importers\ProductImporter;
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
// ... your existing configuration
->plugin(
TapixPlugin::make()
->importers([
ContactImporter::class,
ProductImporter::class,
])
);
}
Three lines of actual configuration: TapixPlugin::make(), the ->importers() call, and your importer class list. That registers two pages in your Filament panel: an import page where users pick an importer and walk through the wizard, and an import history page listing past imports with their status and row counts.
The plugin follows Filament's standard plugin pattern. It implements FilamentPlugin, registers its pages and middleware during register(), and wires up navigation during boot(). If you have written or installed other Filament plugins, this works exactly the same way.
#Step 3: Auto-discovery
Listing every importer class manually works fine when you have two or three. When you have fifteen, it becomes maintenance overhead that you should not have to think about. The discoverImporters() method scans a directory and registers every class it finds that extends BaseImporter:
->plugin(
TapixPlugin::make()
->discoverImporters(
in: app_path('Importers'),
for: 'App\\Importers',
)
)
This mirrors Filament's own discoverResources() and discoverPages() methods. Drop a new importer class into app/Importers/ and it appears in the import page automatically. No registration, no config changes, no deployment ceremony.
You can combine explicit registration with auto-discovery if needed. Importers registered with ->importers([...]) take precedence over discovered ones, so you can override the label or sort order for specific importers while letting the rest auto-register.
For most applications, switch to discoverImporters() early and never think about importer registration again.
#The import page
Once the plugin is registered, your panel's sidebar shows a new navigation item: Imports. Clicking it takes users to the import page, which lists all available importers as selectable cards.
Each card shows the importer's label and a short description. Users pick the importer that matches their data and click to start the wizard. The wizard flows through four steps:
- Upload -- the user uploads a CSV file. Tapix parses headers and stores every row.
- Map -- the wizard auto-detects column matches using the
guess()aliases defined in your importer's fields. Users review and correct any mismatches. - Review -- validation runs per column. Errors appear inline, per cell. Users fix values directly in the table without leaving the application.
- Execute -- rows are processed in chunked queue jobs with live progress feedback. The user can navigate away and come back -- all state is persisted to the database.
For how the import pipeline works under the hood -- the SQLite-per-import store, the parallel validation jobs, and the job architecture -- see Tapix under the hood: how we built a 4-step import wizard.
The import page is a thin Filament page wrapper around the ImportWizard Livewire component. It inherits your panel's layout, authentication, and theme. The wizard runs inside your existing admin shell, not in a separate window or route.
#The history page
Every import creates an Import model record that tracks its status, row counts, and timestamps. The history page lists these records in a Filament table with columns for:
- Importer -- which importer class ran the import
- Status -- uploading, mapping, reviewing, importing, completed, or failed
- Total rows -- how many rows the file contained
- Created / Updated / Skipped / Failed -- breakdown of what happened to each row
- Started at / Completed at -- timing for the import run
Users can click into a completed import to see its details, including any failed rows with their error messages. Failed rows are downloadable as a CSV for cases where users need to re-import a subset after fixing data upstream.
The history page gives operations teams visibility into what has been imported, when, and by whom. No more guessing whether someone already ran the product catalog import this morning.
#Tenant context
Multi-tenant applications face a specific challenge with CSV imports: the tenant context from the HTTP request disappears when processing moves to queue workers. A queue job has no authenticated user, no session, and no Filament tenant. If your entity lookups are not explicitly scoped, a contact import for Tenant A could match companies belonging to Tenant B.
Tapix handles this automatically in Filament panels. The plugin registers SetTenantContextMiddleware, which reads the current tenant from Filament's tenant system and stores it in the TenantContextService. When import jobs are dispatched to the queue, the TenantAware trait on each job serializes the tenant ID into the job payload and restores it on the worker side before any database queries run.
The result: every query in the import pipeline -- validation checks, relationship lookups, match resolution, and row creation -- is scoped to the correct tenant. You do not need to configure any of this. If your Filament panel has tenancy enabled, Tapix picks it up through the middleware.
For applications using custom tenancy implementations outside of Filament's built-in system, the TenantContextService accepts a resolver closure:
use Tapix\Core\Services\TenantContextService;
app(TenantContextService::class)->resolveUsing(function () {
return auth()->user()?->current_team_id;
});
This is the escape hatch for non-standard setups. Most Filament applications will never need it.
For a deeper treatment of multi-tenant import architecture -- tenant serialization in jobs, scoped entity resolution, and cross-tenant leak prevention -- see Multi-tenant CSV imports in Laravel.
#Customization
The plugin exposes a fluent API for adjusting how the import pages appear in your panel's navigation.
#Navigation group
By default, the import pages appear as top-level navigation items. To group them under an existing navigation group:
->plugin(
TapixPlugin::make()
->discoverImporters(
in: app_path('Importers'),
for: 'App\\Importers',
)
->navigationGroup('Data Management')
)
This is useful when your panel already has a structure like "Content", "Users", "Settings" and you want imports to live under a relevant heading rather than floating in the sidebar.
#Navigation label and icon
Override the default label and icon for the import page:
TapixPlugin::make()
->discoverImporters(
in: app_path('Importers'),
for: 'App\\Importers',
)
->navigationLabel('CSV Imports')
->navigationIcon(Heroicon::ArrowUpTray)
The icon accepts any value from Filament's Heroicon enum. The label is what users see in the sidebar. Both affect only the navigation rendering, not the page title or breadcrumbs.
#Sorting and visibility
Control where the import navigation item appears relative to other items:
TapixPlugin::make()
->discoverImporters(
in: app_path('Importers'),
for: 'App\\Importers',
)
->navigationSort(5)
Lower numbers appear higher in the sidebar. This follows Filament's standard navigation sorting behavior.
#What you get without building
To be concrete about what the plugin setup replaces, here is what you would need to build yourself to achieve the same functionality without Tapix:
- A Filament page with a file upload form and CSV parser
- A column mapping UI that auto-detects headers and lets users correct mismatches
- Per-cell validation with inline error display and correction
- Queue jobs for row processing with chunked batches
- Progress tracking with live updates via database polling or broadcasting
- An import history table with status, counts, and failed row downloads
- Tenant context serialization into queue jobs
- Match resolution for relationship columns with configurable create/update/skip behavior
Each of these is a real component that needs to be built, tested, and maintained. The plugin registration replaces all of it with a few lines in your panel provider and an importer class that defines your fields.
#Next steps
If you are evaluating whether your import needs justify a dedicated wizard versus Filament's built-in Import Action, read Filament Import Action: when it's enough and when you need more. That post gives an honest breakdown of where the built-in action works well and where it reaches its limits.
For a broader overview of all CSV import approaches in Laravel, start with The complete guide to CSV imports in Laravel. It covers raw PHP, Laravel Excel, Filament Import Action, and Tapix side by side.
For the multi-tenancy story in detail, Multi-tenant CSV imports in Laravel covers the full problem: tenant context in queue jobs, scoped entity resolution, and preventing cross-tenant data leaks.
Tapix is available at tapix.dev. Same Filament panel, same authentication, same tenant context -- with an import wizard that handles everything the built-in action was not designed to do. See current pricing.