You have a Post model with twenty fields' worth of Comment metadata — moderation flags, threading, edit history, attachments. A modal-based RelationManager felt cramped six months ago and now it's openly user-hostile. Filament v4 ships a first-class answer: nested resources. The --nested flag scaffolds a child resource that lives inside the parent's URL space with breadcrumbs, scoped queries, and policy enforcement wired in for you.
This is the feature most v3 teams hand-rolled with Folio pages and trait soup. The docs page exists but it's terse, so let's walk through a complete Post → Comment setup end to end.
Generate the nested child resource with --nested#
Start with a working parent resource and add the nested child. You only need two artisan calls — the --nested flag tells Filament to wire the child against a parent rather than as a standalone top-level resource.
php artisan make:filament-resource Post
php artisan make:filament-resource Comment --nested
The second command creates the child class at app/Filament/Resources/Posts/Resources/Comments/CommentResource.php. The nested directory structure mirrors the parent-child relationship in the filesystem — this matters when you import the parent resource later. Filament also generates ListComments, CreateComment, and EditComment pages under the same nested path.
Before you can navigate to the child resource, you need a Filament relation manager pointing at the comments relationship on the parent — that's the entry point from the post edit page into the nested list. Scaffold it with php artisan make:filament-relation-manager PostResource comments body, then when the prompt asks whether each row should link to a resource, answer yes and pick CommentResource.
Register the parent on the child resource#
The --nested flag drops a static $parentResource property at the top of the child class. This single line is what unlocks the nested URL space, the breadcrumb chain, and the record route parameter binding to the parent's primary key.
namespace App\Filament\Resources\Posts\Resources\Comments;
use App\Filament\Resources\Posts\PostResource;
use App\Models\Comment;
use Filament\Resources\Resource;
class CommentResource extends Resource
{
protected static ?string $model = Comment::class;
protected static ?string $parentResource = PostResource::class;
// ...
}
URLs that previously would have been /admin/comments/12/edit now resolve as /admin/posts/5/comments/12/edit. The breadcrumb renders Posts → Post #5 → Comments → Edit Comment #12 automatically. If your relationship naming doesn't match Eloquent conventions — say your Post model exposes discussions() instead of comments() — swap the property for a getParentResourceRegistration() method and call ->relationship('discussions')->inverseRelationship('post') on the ParentResourceRegistration builder.
Scope the child query to the parent record#
Filament uses Eloquent's relationship guessing to scope the child list to the current parent — but the moment you have non-trivial requirements (soft-deletes, joins, ordering by a pivot value) you'll override getEloquentQuery(). The parent's route key lands in the record request parameter, so reach for request('record') rather than trying to inject the parent model:
use Illuminate\Database\Eloquent\Builder;
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->where('post_id', request('record'))
->orderByDesc('created_at');
}
parent::getEloquentQuery() already applies Filament's tenant scope if you're in a tenanted panel — keep it as the first call so per-tenant scoping doesn't silently drop. If you have a panel-wide scope you want to skip on this resource (a global "published" scope on Comments, say), append ->withoutGlobalScope(PublishedScope::class) after the parent call rather than at the model level. The pattern is the same one I used in the Filament v4 multi-tenancy team panel guide for stacking tenant and resource scopes.
Wire the foreign key in the create form#
When the user clicks "Create Comment" on a post edit page, the URL already knows the parent — but the form doesn't, so saving the new comment without a post_id will fail validation. The cleanest fix is to mutate the form data before insert from the CreateComment page class.
namespace App\Filament\Resources\Posts\Resources\Comments\Pages;
use Filament\Resources\Pages\CreateRecord;
class CreateComment extends CreateRecord
{
protected static string $resource = CommentResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['post_id'] = request('record');
return $data;
}
}
If your post_id column is not null in the schema (it should be) and you have a belongsTo relationship defined on Comment, this is enough. You don't need to expose a hidden field in the form or worry about a malicious user posting a different post_id from the browser — mutateFormDataBeforeCreate() is server-side and runs after validation. For edit screens, the foreign key is already in the model so no extra step is needed.
Customise the breadcrumb labels#
Filament's default breadcrumb uses the record's primary key — "Post #5", "Edit Comment #12" — which is fine for an internal admin but awkward when posts have meaningful titles. Override getBreadcrumbRecordLabel() on the parent resource so the parent crumb shows the post title instead.
namespace App\Filament\Resources\Posts;
use App\Models\Post;
class PostResource extends Resource
{
public static function getBreadcrumbRecordLabel(\Illuminate\Database\Eloquent\Model $record): string
{
/** @var Post $record */
return $record->title;
}
}
The child resource inherits this behaviour. If you need fully bespoke breadcrumbs — adding intermediate links, swapping labels per page — override getBreadcrumbs() on the specific page class (EditComment::getBreadcrumbs()). Resist that urge for as long as you can; the default chain is good and replacing it sacrifices the auto-routing that nested resources give you.
Apply a policy to the child model#
Filament respects standard Laravel policies on the child model — no special configuration needed. Generate a CommentPolicy with php artisan make:policy CommentPolicy --model=Comment, then express your rules in terms of the parent relationship the user is allowed to touch.
namespace App\Policies;
use App\Models\Comment;
use App\Models\User;
class CommentPolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function update(User $user, Comment $comment): bool
{
return $user->id === $comment->post->user_id;
}
public function delete(User $user, Comment $comment): bool
{
return $user->id === $comment->post->user_id
|| $user->hasRole('moderator');
}
}
Register the policy in AuthServiceProvider (or via Laravel 12's auto-discovery in app/Models/Comment.php) and Filament will check it against every action surface — the row actions on the comment list, the edit page header actions, the bulk delete action on selected rows. Note the $comment->post->user_id access — that triggers a query per row unless you eager-load post in getEloquentQuery(). Add ->with('post') if you find policy checks blowing your query budget.
Confirm the policy hides forbidden actions#
The payoff for wiring policies properly: forbidden actions don't render. A user without update permission visits /admin/posts/5/comments and sees the table — but the "Edit" row action is silently dropped, the delete bulk action vanishes, and a direct hit on /admin/posts/5/comments/12/edit returns a 403. Open the comment list as a user who can't moderate and verify each surface:
// In a Pest test
test('non-moderator cannot see edit action on a foreign post comment', function () {
$post = Post::factory()->create();
$comment = Comment::factory()->for($post)->create();
$intruder = User::factory()->create();
actingAs($intruder)
->get(PostResource::getUrl('comments.index', ['record' => $post]))
->assertOk()
->assertDontSee('Edit');
});
If you've never written tests against Filament resources before, the framework ships Filament\Testing helpers — livewire()->callTableAction('edit', $comment)->assertForbidden() gives you a stronger assertion than the DOM string check above and reads more naturally. For the wider test architecture I lean on across Filament projects, Pest architecture testing for Laravel covers the constraints worth pinning down. Either way, getting policy enforcement under test is non-negotiable for an admin panel.
Nested resource vs RelationManager — when each wins#
A RelationManager belongs inside the parent edit page as a tabbed table. It's the right call when the child is shallow: two or three fields, edited inline in a modal, never deep-linked or shared. Comments on a blog admin where the only operations are "approve" and "delete" are textbook RelationManager territory — and the many-to-many pivot RelationManager guide covers that path end to end.
Nested resources take over the moment the child grows past what a modal can comfortably hold. Twenty fields, file uploads, infolists, conditional sections, breadcrumb-able URLs you can paste into Slack — that's nested resource territory. The other tell: if you're considering opening a modal that's 90% of the viewport, you want a full-page screen and that means a nested resource.
The decision isn't permanent. Start with a RelationManager when the child is small, promote to a nested resource when the form outgrows the modal. The migration is cheap because the underlying Eloquent relationship doesn't change — only the URL surface and the page class.
Gotchas and Edge Cases#
You still need the relation manager. The --nested flag generates the child resource, but the user reaches it from a relation manager or relation page on the parent. Skip that step and the nested resource has no entry point in the UI — the resource exists, the URLs resolve, but nothing in the navigation points to it. Run make:filament-relation-manager against the parent and answer "yes" when prompted to link rows to a resource.
getParentRecord() only exists on the page class. The brief-friendly snippet $this->getParentRecord()->id works inside CreateComment and EditComment page methods but not inside the static getEloquentQuery() on the resource class. Static context has no $this. Use request('record') there; reserve $this->getParentRecord() for page-class hooks like mutateFormDataBeforeCreate() if you prefer it over the request helper.
Soft-deleted parents don't auto-cascade. If the parent uses SoftDeletes, a deleted parent still exposes its /admin/posts/5/comments URL — Filament doesn't 404 on a trashed parent automatically. Add ->whereNull('deleted_at') on the parent's getRouteKeyName() or override resolveRouteBinding() on the Post model to filter trashed records out of route resolution.
Two resources for the same child model break. If you scaffold both a top-level CommentResource and a nested CommentResource for the same Comment model — say, one for moderators and one inside posts — Filament 4 throws a LogicException at boot. The fix is to put them in separate panels or to merge them into one resource with conditional $parentResource resolution.
Eager-load the parent for policy checks. Policies that reference $model->post->user_id will N+1 against the comment list. Add ->with('post') to getEloquentQuery() whenever your policy walks back up the relationship.
Wrapping Up#
Nested resources are the v4 feature most v3 codebases will gladly delete code to adopt. Three or four files instead of a trait, a base page class, and four hand-rolled Folio routes. The --nested flag scaffolds the pieces; the $parentResource property wires them together; getEloquentQuery() and policies do exactly what they do everywhere else in Laravel.
If your panel hasn't been built yet, start with the Filament v4 zero-to-production dashboard guide and add nested resources once the parent shape stabilises. For panels already running, the Filament v4 multi-tenancy team panel walkthrough shows how nested scoping stacks on top of tenant scoping when you eventually need both.
FAQ#
What is a nested resource in Filament v4?
A nested resource is a Filament resource that lives inside the URL space of a parent resource — /admin/posts/{post}/comments/{comment}/edit rather than /admin/comments/{comment}/edit. The child gets its own full-page list, create, edit, and view screens, but every page knows about the parent record via the route binding. Filament also wires breadcrumbs, foreign-key scoping conventions, and policy enforcement for you. It's the v4 replacement for hand-rolled parent-context CRUD pages that v3 teams built on top of Folio or custom resource pages.
When should I use a nested resource instead of a RelationManager?
Use a RelationManager when the child has a handful of fields and fits comfortably in a modal — short rows of moderation toggles, tag attachments, simple pivot edits. Switch to a nested resource when the child has many fields, file uploads, a complex form layout, or needs deep-linkable URLs you can share or bookmark. A useful test: if you're about to make the modal take up 80% of the viewport, you want a full-page screen, and a nested resource is the right surface. Migrating from one to the other is cheap because the underlying Eloquent relationship is unchanged.
How do I access the parent record inside a Filament nested resource?
In the resource's static methods like getEloquentQuery(), read the parent's route key with request('record') and use it in a where clause. In page-class methods like mutateFormDataBeforeCreate() or table action handlers, you can call $this->getParentRecord() which returns the hydrated parent model. The static context has no $this, which is why the request helper is the safer default — it works from anywhere in the resource. Use the page-class method when you specifically need the full parent model rather than just its primary key.
Do Filament nested resources respect Laravel policies?
Yes. Filament runs the standard policy gates on the child model — viewAny, view, update, delete, forceDelete, restore — at every UI surface in the nested resource. The Edit row action, the delete bulk action, the create button, and direct URL hits all check the policy and either hide the control or return 403. Write the policy in terms of the child model and let Filament call it; you don't need separate "nested" policy methods. Eager-load the parent in getEloquentQuery() if your policy walks back up the relationship to avoid N+1 queries on the list view.
How are breadcrumbs generated for a nested resource?
Filament builds breadcrumbs from the parent resource registration chain. For a nested CommentResource under PostResource, the chain renders Posts → Post #5 → Comments → Edit Comment #12 with each crumb linking to the appropriate index or detail page. Override getBreadcrumbRecordLabel() on the parent resource to swap "Post #5" for the post's title — the child resource inherits this behaviour. For per-page custom breadcrumbs, override getBreadcrumbs() on the specific page class, but use that escape hatch sparingly.
Can a nested resource have its own RelationManagers?
Yes — a nested CommentResource can declare its own RelationManagers in getRelations() exactly like a top-level resource. This is how you handle three-level hierarchies: Post → Comment → Reaction, where reactions live as a RelationManager inside the comment edit page. Filament doesn't impose a depth limit, but each level adds a route segment and breadcrumb crumb, so practical readability tends to cap at three or four levels deep before the URLs and page headers get unwieldy.