Permissions
A permission is a single ability such as users.delete. Roles bundle permissions, and users get permissions through their roles.
In Tachi Kit, app/Enums/Permission.php is the single source of truth. The database rows, the role editor, the TypeScript constants and the auth.can map all come from it.
The built-in permissions
Section titled “The built-in permissions”| Permission | Label | Group | Guards |
|---|---|---|---|
users.view |
View Users | Users | Users page, dashboard analytics |
users.create |
Create Users | Users | Create user |
users.edit |
Edit Users | Users | Edit user |
users.delete |
Delete Users | Users | Delete and bulk delete |
users.status |
Change User Status | Users | Activate and deactivate |
roles.view |
View Roles | Roles | Roles page |
roles.create |
Create Roles | Roles | Create role |
roles.edit |
Edit Roles | Roles | Edit role |
roles.delete |
Delete Roles | Roles | Delete role |
activity.view |
View Activity Log | Activity | Activity log page |
enum Permission: string{ // Users case UsersView = 'users.view'; // ...
// Activity case ActivityView = 'activity.view';
public function label(): string { /* 'View Activity Log', ... */ }
public function group(): string { /* 'Users', 'Roles', 'Activity' */ }
public static function values(): array { /* every value */ }}label()is the text shown in the role editor.group()is taken from the prefix before the dot (activity.view→Activity). The role editor groups checkboxes by it.
Adding a permission
Section titled “Adding a permission”Say you’re adding a Posts feature and want a posts.publish permission.
1. Add it to the enum
Section titled “1. Add it to the enum”// Postscase PostsPublish = 'posts.publish';
public function label(): string{ return match ($this) { // ... self::PostsPublish => 'Publish Posts', };}2. Regenerate the TypeScript constants
Section titled “2. Regenerate the TypeScript constants”php artisan tachi:typesThis rewrites resources/js/constants/access.generated.ts, adding PERMISSIONS.POSTS_PUBLISH, its label, and a new Posts group.
3. Seed it
Section titled “3. Seed it”php artisan db:seed --class=RolePermissionSeederThe seeder creates a database row for every enum case and gives superadmin every permission automatically. To give it to admin as well, add it to the admin list in the seeder. Any other role can get it from the Roles page.
4. Check it on the backend
Section titled “4. Check it on the backend”In a policy (preferred):
public function publish(User $user, Post $post): bool{ return $user->checkPermissionTo(Permission::PostsPublish->value);}// in a controller$this->authorize('publish', $post);Or directly on a route, using Spatie’s gate integration:
Route::post('posts/{post}/publish', PublishPostController::class) ->middleware('can:posts.publish');5. Check it on the frontend
Section titled “5. Check it on the frontend”Every page receives auth.can, a map of every permission name to a boolean for the current user. It’s built in HandleInertiaRequests from Permission::values(), so new permissions appear automatically. Read it with the usePermissions() hook:
import { PERMISSIONS } from '@/constants/permissions';import { usePermissions } from '@/hooks/use-permissions';
export function PublishButton() { const { can } = usePermissions();
if (!can(PERMISSIONS.POSTS_PUBLISH)) { return null; }
return <Button>Publish</Button>;}usePermissions() returns:
| Function | Returns true when |
|---|---|
can(permission) |
the user has the permission |
cannot(permission) |
the user doesn’t have it |
canAny([...]) |
the user has at least one |
canAll([...]) |
the user has all of them |
hasRole(name) |
the user holds the role, e.g. hasRole(ROLES.ADMIN) |
Permission names are typed (PermissionName), so a typo is a TypeScript error.
To gate a sidebar link, set permission on its nav item. See Navigation.
Customizing
Section titled “Customizing”Renaming or removing a permission
Section titled “Renaming or removing a permission”- Change or remove the enum case (and its
label()arm). - Run
php artisan tachi:typesand fix the TypeScript errors it reveals. - The seeder doesn’t delete rows. Clean up the old permission yourself in a migration:
Permission::query()->where('name', 'posts.publish')->delete(); // Spatie\Permission\Models\Permissionapp(PermissionRegistrar::class)->forgetCachedPermissions();Custom group names
Section titled “Custom group names”Groups come from the prefix, so reports.export goes into a Reports group. To name a group differently, change group() in the enum to return your own label for those cases.