coringawc/filament-single-record-resource
Filament resource pattern that replaces list pages with a single resolved record per user, ideal for My Profile, My Wallet, or My Settings pages. Redirects index navigation to view, supports nested resources, and works with Filament 4 and 5.
Install
composer require coringawc/filament-single-record-resource
Requires Filament ^4.0 || ^5.0. No service provider, no config file, no migration — it ships two traits and one interface.
Where to look first
SingleRecordResolvableResource (added v1.1.0) — implement it on every resource; it is what makes the page trait resolve the record via the Resource contract instead of duck-typing.viewAny denied but view allowed) landed there. If you are on 1.0.0–1.0.3, upgrade before debugging permissions.workbench/ and tests/ folders in the repo for a runnable example (MyWallet → Companies → Products).Your first use case: "My Wallet"
HasSingleRecordResource, implements SingleRecordResolvableResource, and registers only view (optionally edit) — no index.class MyWalletResource extends Resource implements SingleRecordResolvableResource
{
use HasSingleRecordResource;
public static function getPages(): array
{
return [
'view' => ViewMyWallet::route('/'),
'edit' => EditMyWallet::route('/edit'),
];
}
}
ViewMyWallet and EditMyWallet use HasSingleRecord.User hasOne Wallet, Wallet belongsTo User) — the default builder does whereBelongsTo(filament()->auth()->user()).That's it: sidebar item appears, clicking it lands directly on the wallet view instead of a list.
Default 1:1 resolution (do nothing). For belongsTo(User::class)-shaped models the package resolves the record for you. This is the case you should optimize for; only deviate when business rules force it.
Pattern A — narrow the builder on the Resource (recommended for filters). Keeps authorization and page loading on the same query:
public static function resolveSingleRecordBuilder(Builder $query): Builder
{
return parent::resolveSingleRecordBuilder($query)->where('active', true);
}
Use for: soft "current" selection, where('tenant_id', ...), status filters, subscription expiry checks.
Pattern B — full resolver on the Resource (firstOrCreate / business rules).
public static function resolveSingleRecord(): ?Model
{
$user = filament()->auth()->user();
return $user?->wallet()->firstOrCreate([]);
}
Use for: lazy-provisioning records ("My Settings" row created on first open), tenant-aware resolution, data coming from a non-user() relation (override owner()/account() by replacing the builder with whereBelongsTo($user, 'owner')).
Pattern C — page-level override. Legitimate but rare: only when the Edit page needs a different record than the View page. Keep in mind the Resource-level resolver still drives the viewAny fallback, so a divergent page override can produce a page that loads but whose navigation/permission check disagrees. Prefer A or B.
Nested chains (MyWallet → Companies → Products).
HasSingleRecordResource on every resource in the chain that follows the single-record flow.HasSingleRecord on the deep ViewRecord/EditRecord pages.where('company_id', $parent->id) in the child resource's getEloquentQuery() or use Filament's getParentResource/relation-manager scoping. The plugin will happily render a record the user should not see if your query is unscoped.Policies that deny listing. Explicitly viewAny(): false + view(): true is now a supported stance — the Resource stays in the sidebar and is reachable. This is the pattern for "users may open their own resource but there is no collection to browse."
Multiple single-record resources per panel. Nothing special: one trait-bearing resource each (MyWallet, MySettings, CurrentSubscription). Group them under a navigation group and set $navigationLabel to the possessive form.
Explicit contract everywhere. Implement SingleRecordResolvableResource on every resource using the trait. It costs one implements clause and keeps PHPStan/Larastan from complaining about the static calls.
viewAny() denial is a real trap. Filament gates navigation and resource access on viewAny(). On versions before 1.0.4, a policy with viewAny() = false makes the whole thing invisible or 403s. The package now falls back to view() on the resolved record — but only if resolution actually returns a record, so a null-returning resolveSingleRecord() will silently hide your menu item. Test with a fresh user who has no wallet row yet.
Edit page save redirect. EditRecord redirects to the resource index by default. There is no index here. Override getRedirectUrl() on the edit page (point at the view page or null) and smoke-test "Save changes" as part of setup — this is the most common first-day bug with this pattern.
Breadcrumbs and "back to list" links. Filament still generates index-facing URLs in some places (breadcrumbs, page headers, global search result URLs). If you add ->breadcrumb() overrides or use getGlobalSearchResultUrl(), make sure they resolve to the view route.
Resolution runs per request. There is no caching. If your resolver hits an expensive query or an external API, wrap it or cache the result — otherwise every sidebar render plus every page load fires it. The authorization fallback triggers resolution on navigation rendering too, so lazy-provisioning firstOrCreate runs on panel boot, not just on page visit.
Resolver returning null. Decide deliberately: let the page 404, or return a freshly created record. Returning null from a full resolver yields confusing behaviour (empty page / hidden nav), so prefer firstOrCreate when a record is required.
Tenancy. The default whereBelongsTo(auth()->user()) ignores filament()->getTenant(). In a tenant panel, override the builder with a tenant-scoped query — otherwise a user switching tenants may see a record from another tenant.
Custom relation names. The default assumes a user() relation exists on the model. A model with owner() or account() needs resolveSingleRecordBuilder() with whereBelongsTo($user, 'owner').
Don't register index. Some generators scaffold it by default. If you leave it, the trait redirects it, but you now have a dead route and a confusing page class. Delete ListXxx and its page registration.
Static analysis. Calling MyWalletResource::resolveSingleRecord() from a page without the interface implemented will fail Larastan. The interface exists precisely for this — use it.
Package maturity. v1.x, ~12 stars, zero dependents, small surface (two traits, one interface, README + changelog + CI on Filament 4 and 5). Vendor risk is low because everything is trait-composed inside your own resource classes — if the package stalls, copying the two traits into your app is a viable fallback. Pin a minor version anyway and re-run your panel smoke tests when crossing a Filament major.
Testing tip from the maintainers' own release notes (v1.1.1): Testbench-based tests need a persistent SQLite DB_DATABASE path; the in-memory testing connection breaks HTTP-level feature tests. If you write your own tests around these pages, mirror that setup.
How can I help you explore Laravel packages today?