Architecting Filament Tables: Pagination, Filters, Actions
Build a secure, performant Filament table with server‑side pagination, filtering, and inline actions. Follow design patterns, operational checks, and failure‑mode mitigations for Laravel admin panels.
11 Jul 2025, 17:40 UTC

Problem Statement
When building an admin panel with FilamentPHP, most teams need a data grid that can:
- Show an Eloquent collection with server‑side pagination.
- Allow column sorting and a global search box.
- Provide row‑level actions such as edit and delete.
- Stay responsive even with thousands of rows.
The goal is to implement this with minimal custom code while respecting Laravel’s authorization model and keeping memory usage low.
Requirements
- Server‑side pagination: Filament’s
Tablescomponent automatically paginates, but the query must be scoped to the authenticated user. - Sortable columns: Each column should be sortable via
Column::make(...)->sortable(). - Global search: Enable
Table::searchable()and map the search to relevant fields. - Row actions: Use
Action::makewith URLs or callbacks that respect policies. - Performance: Avoid N+1 queries and keep memory usage below ~50 MB on a 10 000‑row dataset.
Smallest Suitable Design
The minimal implementation is a Filament Table class that extends Filament\Tables\Table. The following example shows a PostTable for a Post model belonging to a team.
where('team_id', auth()->user()->team_id)
// Eager‑load heavy relationships only when needed
->with(['author']);
}
protected function getTableColumns(): array
{
return [
TextColumn::make('title')
->sortable()
->searchable(),
TextColumn::make('author.name')
->sortable()
->label('Author'),
TextColumn::make('created_at')
->sortable()
->dateTime(),
];
}
protected function getTableFilters(): array
{
return [
SelectFilter::make('status')
->options(Post::STATUS_OPTIONS)
->placeholder('All statuses'),
];
}
protected function getTableActions(): array
{
return [
Action::make('edit')
->url(fn (Post $record) => route('posts.edit', $record))
->requiresConfirmation(false),
Action::make('delete')
->action(fn (Post $record) => $record->delete())
->requiresConfirmation(true),
];
}
}
Key points:
- The
getTableQuerymethod scopes the data to the current user’s team. - Columns are defined with
sortable()andsearchable()to enable client‑side UI controls that trigger server‑side queries. - Filters are scoped queries that can be combined with the global search.
- Actions automatically invoke the model’s policy methods, preventing unauthorized edits or deletes.
Trust and Data Boundaries
Filament’s Table component is a Livewire component. Every request to the table triggers a new server‑side query. Because the query is built inside getTableQuery, you can enforce any policy or tenancy logic there. Filament automatically calls the model’s policy for each row action; if the policy returns false, the action is removed from the UI and the request returns a 403.
Example policy for Post:
team_id === $post->team_id;
}
By keeping all data‑scoping logic in the query builder and policy, you isolate the table from accidental data leaks.
Operational Checks
- Tenancy enforcement: The
where('team_id', auth()->user()->team_id)clause is mandatory for multi‑tenant apps. Test by logging in as two users with differentteam_idvalues and verifying each sees only their records. - Lazy loading: For large datasets, avoid eager‑loading all relationships. Use
with(['author'])only if the column accesses that relation. If you add a column that showsauthor.email, add the relationship to thewitharray. - Default sort: Set
defaultSort('created_at', 'desc')to avoid unpredictable ordering. - Resource limits: Laravel’s default memory limit is usually 128 M. Monitor with Telescope or Debugbar; if the table renders slowly, consider pagination limits (e.g.,
->perPage(50)).
Failure Modes
- Missing relationship: If a column references a relation that does not exist, Filament throws a
BadMethodCallExceptionduring render. Validate the relation name or use->whenLoaded(). - Policy gaps: Without a
viewAnyorviewmethod, Filament will return a403and hide the table. Ensure policies cover all actions used. - Memory bloat: Eager‑loading thousands of related models can exceed the memory limit. Keep
withminimal and use pagination. - Slow queries: Complex filters or sorting on non‑indexed columns can cause long response times. Add database indexes or move heavy logic to computed columns.
Design‑Change Conditions
When the basic table no longer meets requirements, consider the following:
- Cursor‑based pagination: Replace the default offset pagination with
cursorPaginate()for very large tables. This requires a custom Livewire component because Filament’s Table does not expose cursor pagination out of the box. - Export functionality: Add a
BulkActionthat streams CSV or Excel. This often needs a custom service class to handle large data sets without exhausting memory. - Real‑time updates: If the table must reflect live changes, enable Livewire polling or use a WebSocket driver. This may necessitate a custom component that listens for events and refreshes the query.
- Complex relationships: When a column needs to display nested data (e.g.,
post.comments.count), use->withCount('comments')in the query builder and a computed column.
Practical Verification Checklist
- Run
php artisan tinkerand confirmPost::where('team_id', auth()->user()->team_id)->count()matches the table’s row count. - Visit the admin route and ensure pagination controls appear, columns sort correctly, and the global search filters the dataset.
- Attempt to edit a record belonging to another user’s team via URL; the response should be a
403. - Use Telescope to check that the query executed on each page load contains the
WHERE team_id = ?clause and uses the correct limit/offset. - Measure memory usage while loading the table with 10 000 rows; confirm it stays under 50 MB when
withis limited.
Conclusion
By scoping the query, defining columns with built‑in sorting and searching, and relying on Filament’s policy integration for actions, you can deliver a secure, performant table with minimal custom code. Keep operational checks in place to guard against common failure modes, and be ready to pivot to cursor pagination, real‑time updates, or exports when the business demands grow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.