devtical/filament-sanctum
Manage Laravel Sanctum API tokens directly from your Filament panel. Create, list, and revoke personal access tokens, define custom abilities and expiration presets, restrict access with gates, and display the page in the sidebar or user menu.
1. Install the package and Sanctum together — they're listed as one require in the README, and the package is useless without Sanctum's table:
composer require devtical/filament-sanctum laravel/sanctum
2. Publish and run Sanctum's migrations — skip this if the app already issues API tokens:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
3. Add HasApiTokens to the model that authenticates into your panel. For most apps that's App\Models\User:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
}
If your panel authenticates a different model (e.g. Admin), the trait goes there — the package renders the page for whatever the panel's authenticated user is.
4. Register the plugin in the panel provider — this is the only wiring step that touches Filament:
use Devtical\Sanctum\SanctumPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(SanctumPlugin::make());
}
5. Publish the config (you will edit it, so don't skip):
php artisan vendor:publish --tag=filament-sanctum-config
6. First use case — end to end in five minutes. Log into /admin, open the user menu (top right), click API Tokens, create a token, and copy the plaintext value from the one-time modal. Verify it works:
curl -H "Authorization: Bearer 1|abc123..." https://app.test/api/user
That round trip is the whole value proposition: a non-technical admin can now mint and revoke their own API credentials without a developer running php artisan tinker.
Where to look first, in order:
config/filament-sanctum.php — everything tunable lives here: navigation, abilities, expiration, authorization.vendor/devtical/filament-sanctum/src/ — the surface is small (a page/resource class plus the plugin and a Sanctum URL helper). When the README is silent on a behavior, read the class rather than guessing.Default is the user menu, which is the right call for most apps — it signals "these are my credentials." Sidebar placement suggests a management surface, which invites the question "whose tokens am I managing?"
// config/filament-sanctum.php
'navigation' => [
'slug' => 'sanctum',
'sidebar_menu' => ['enabled' => true],
'user_menu' => ['enabled' => false],
],
If you run multiple panels, remember the config is global — registering the plugin on both an admin panel and a customer-facing portal gives both panels the same slug, abilities list, and gate. If they need to diverge, that's a fork or a service-provider-level config override, not a plugin option.
This is the highest-leverage config block. Define scopes that mirror the middleware on your API routes, and label them in the user's language:
'abilities' => [
'columns' => 4,
'allow_expiration' => true,
'default_expiration_days' => 30,
'expiration_presets' => [7, 30, 60, 90],
'list' => [
'posts:read' => 'Read posts',
'posts:write' => 'Write posts',
'webhooks:manage' => 'Manage webhooks',
],
],
Then enforce them server-side — the package's UI is advisory, your routes are the actual boundary:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/posts', [PostController::class, 'index'])
->middleware('abilities:posts:read');
Route::post('/posts', [PostController::class, 'store'])
->middleware('abilities:posts:write');
});
Keep the ability list and the middleware list in one place (a config or enum) so a scope can't exist in the UI without a corresponding guard. An ability offered in the picker but never checked is a false promise; one checked but not offered is a support ticket.
Three settings interact here, and they should agree:
allow_expiration / default_expiration_days / expiration_presets — the package UI and the expires_at it writes at creation time.config/sanctum.php → 'expiration' — Sanctum's own enforcement and its own default at creation.Set the Sanctum expiration to at least as strict as the shortest preset you offer. If Sanctum's expiration is null and the package writes no expires_at, you've shipped immortal tokens with a UI that implied otherwise. For third-party integrations especially, prefer a finite default (30–90 days) plus a visible "last used" column to tell you which tokens are actually alive.
// config/filament-sanctum.php
'authorization' => [
'enabled' => true,
'gate' => 'manage-api-tokens',
],
// AppServiceProvider::boot()
Gate::define('manage-api-tokens', fn (User $user) => $user->isAdmin());
The gate answers "can this user reach the page." It does not answer "which rows do they see." For multi-tenant or mixed-privilege panels, do the second half explicitly:
isAdmin()), and log revocation actions.Sanctum::actingAs in testsYou don't need to click through the panel to test your API. Once abilities are wired:
Sanctum::actingAs($user, ['posts:read']);
$this->getJson('/api/posts')->assertOk();
Sanctum::actingAs($user, []);
$this->postJson('/api/posts')->assertForbidden();
Reserve panel-level feature tests (Filament's livewire()/callAction() helpers) for the create-and-revoke flow itself — the package ships its own tests for that, so your coverage should focus on your gate and your scoping.
Service accounts, CI, and long-lived integrations shouldn't be minted through a human's user menu — the token would be attached to that person, and it dies when their account is deactivated. Issue those via a console command or an admin-only Filament custom page, and use this package for what it's good at: self-service credentials for humans.
The plaintext token is shown exactly once. Sanctum stores only a SHA-256 hash — there is no "reveal" and no recovery, only revoke-and-recreate. If your support docs don't already say "copy it now," they need to.
Revocation is destructive and non-recoverable. Combined with the fact that the page reads the existing personal_access_tokens table, this means every token ever issued in your app appears in the panel the first time the plugin is enabled, revocable by anyone who can reach it. That's usually what you want, but audit first: check personal_access_tokens for tokens belonging to service accounts or legacy integrations, and make sure the gate is on before you deploy rather than after.
A misconfigured gate is a security incident, not a bug. The package doesn't decide visibility for you. Shipping with authorization.enabled = false on a panel that non-admins can log into means any panel user can list and revoke tokens. Treat "gate defined + verified with a non-admin user + listing confirmed scoped" as part of the definition of done, not a follow-up ticket.
Pin the version and record the tested matrix. The docs disagree with themselves: v1.1.0's changelog claims Filament 5 / Laravel 13 support, v1.0.0 claims Filament 3/4 + Sanctum v4, and the README still describes the older flow. Pre-1.0 pins won't work on a Filament 4/5 panel. Pin an exact version in composer.json and note the verified combination in your repo — this package's README is not a reliable version oracle.
Require v1.1.0 or later if you're not on a root-path panel. The duplicate-path bug (/admin/admin/sanctum) in the user menu was fixed in v1.1.0, as was an abilities-column rendering bug that repeated "None" / "5 abilities". Both are exactly the kind of thing that costs an afternoon of debugging if you assume your config is wrong.
last_used_at is lazy. Sanctum updates it on authenticated requests, so a token that hasn't been used since the timestamp was introduced may read as never-used. Don't delete tokens purely on a "never used" sort without checking with the integrator.
Clear caches after every config change. The config is read at panel boot. php artisan config:cache (or php artisan optimize:clear in dev) between edits, or you will spend time debugging a change that simply hasn't been loaded.
Publishing translations freezes them. --tag=filament-sanctum-translations copies EN/ID into lang/vendor/...; after that, package updates won't reach your files. Only publish if you're actually editing (the package ships EN and ID only), and add your own locale by copying an existing file.
Wildcard abilities. If the abilities list is empty or a token is created with ['*'], that token bypasses abilities: middleware entirely. In the UI, make sure at least one ability is selected by default, or you've quietly created an admin-level credential with a friendly name.
Config is the extension point; beyond it, read the source or fork. There's no documented hook for customizing the page's table columns or adding fields to the create form. The code surface is small (page/resource + plugin + a Sanctum::getUrl() helper), so a subclass or a fork is a day's work at most — but budget for it rather than assuming a config key exists. Zero dependents and a single maintainer means you should be comfortable owning that fork if you need it.
Debugging checklist when the page doesn't appear: plugin registered on this panel → HasApiTokens on this panel's user model → gate returns true for the current user → config cache cleared → version pinned to one that supports your Filament major. That ordering resolves nearly every "I installed it and nothing happened" report.
How can I help you explore Laravel packages today?