Seed Roles and Permissions
Use seeders for two different jobs:
- Sync the role and permission definitions from
config/access.phpinto theaccess_*tables. - Assign those roles or direct permissions to real users, either globally or inside a scope.
Keep those jobs separate. access:sync creates and updates definitions. Your seeders decide which users receive those definitions.
First-Time Setup
For a scoped app, install the package, scaffold your scope, migrate, then sync:
php artisan access:install --enum
php artisan access:scope --name=company
php artisan migrate
php artisan access:syncFor a global-only app, skip the scope scaffold:
php artisan access:install --enum
php artisan migrate
php artisan access:syncUse --dry-run before changing an existing database:
php artisan access:sync --dry-run
php artisan access:syncDefinitions vs Assignments
Definitions live in code:
// app/Enums/Permission.php
enum Permission: string
{
case CompanyMembersView = 'company.members.view';
case CompanyMembersInvite = 'company.members.invite';
case BillingManage = 'billing.manage';
}2
3
4
5
6
7
// config/access.php
'permission_enums' => [
Permission::class,
],
'roles' => [
CompanyRole::Owner->value => [
Permission::CompanyMembersView,
Permission::CompanyMembersInvite,
Permission::BillingManage,
],
],
'global_roles' => [
'Platform Admin' => [
Permission::BillingManage,
],
],2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Assignments live in the database:
$user->in($company)->assignRole(CompanyRole::Owner);
$user->assignGlobalRole('Platform Admin');2
When you change permissions or role definitions, update the enum/config and run access:sync. When you change who should have access, update your app logic or assignment seeders.
Scoped Apps
In scoped apps, roles are assigned inside a model such as a company, team, workspace, or tenant. The same user can have different roles in different scopes.
// config/access.php
'default_scope_model' => Company::class,
'roles' => [
CompanyRole::Owner->value => [
Permission::CompanyMembersView,
Permission::CompanyMembersInvite,
Permission::BillingManage,
],
CompanyRole::Member->value => [
Permission::CompanyMembersView,
],
],
'global_roles' => [
'Platform Admin' => [
Permission::BillingManage,
],
],2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Seed both app membership and Laravel Access assignment when using the access:scope scaffold:
php artisan access:seeder --name=company --class=DemoCompanySeederThat command generates an editable starter seeder with an owner lookup placeholder. Replace it with the email of an existing trusted account provisioned by your app before running the seeder. The core shape should stay the same:
namespace Database\Seeders;
use App\Enums\CompanyRole;
use App\Models\Company;
use App\Models\User;
use Illuminate\Database\Seeder;
class DemoCompanySeeder extends Seeder
{
public function run(): void
{
// Replace this reserved placeholder with an existing trusted, app-provisioned owner account.
$owner = User::query()->where('email', 'replace-with-existing-owner@example.invalid')->firstOrFail();
$company = Company::query()->firstOrCreate(
['slug' => 'acme-corp'],
['name' => 'Acme Corp']
);
$company->users()->syncWithoutDetaching([
$owner->getKey() => ['role' => CompanyRole::Owner->value],
]);
$owner->in($company)->assignRole(CompanyRole::Owner);
$owner->switchCompany($company);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
The membership write and access assignment are intentionally separate:
$company->users()->syncWithoutDetaching([
$owner->getKey() => ['role' => CompanyRole::Owner->value],
]);
$owner->in($company)->assignRole(CompanyRole::Owner);2
3
4
5
The first line says the user belongs to the company. The second line says what the user may do in that company.
For guidance on scoped assignments and other common authorization mistakes, see Authorization Boundaries.
Global-Only Apps
In global-only apps, leave default_scope_model as null, leave roles empty, and put app-wide roles in global_roles.
// config/access.php
'default_scope_model' => null,
'roles' => [],
'global_roles' => [
'Admin' => [
Permission::UsersView,
Permission::UsersInvite,
Permission::BillingManage,
],
'Viewer' => [
Permission::UsersView,
],
],2
3
4
5
6
7
8
9
10
11
12
13
14
15
Then seed global assignments:
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class DemoAdminSeeder extends Seeder
{
public function run(): void
{
// Replace this reserved placeholder with an existing trusted, app-provisioned admin account.
$admin = User::query()->where('email', 'replace-with-existing-admin@example.invalid')->firstOrFail();
$admin->assignGlobalRole('Admin');
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Global roles have no scope_type or scope_id. Use them for single-tenant apps or platform-wide administration, not for company membership.
Future Updates
Use this workflow when roles or permissions change after launch:
- Add or rename permission enum cases carefully. Permission values are stored in the database.
- Update
rolesorglobal_rolesinconfig/access.php. - Preview the change with
php artisan access:sync --dry-run. - Run
php artisan access:sync. - Deploy code and config together.
- Run assignment seeders only when specific users or scopes need new assignments.
When removing permissions, prune only after you confirm the old permission is no longer used:
php artisan access:sync --dry-run
php artisan access:sync --prune--prune deletes stale definitions and related role-permission rows. It does not decide which users should receive replacement roles.
Idempotent Seeders
Seeders should be safe to run more than once:
- Use existing trusted, app-provisioned account lookups for users, and
firstOrCreateorupdateOrCreatefor scopes. - Use
syncWithoutDetachingfor generated membership pivots. - Use
assignRoleorassignGlobalRolerepeatedly instead of manually inserting assignment rows. - Do not use raw IDs for scopes in reusable seeders.
- Run
access:syncbefore assignment seeders so role and permission definitions exist.
A typical DatabaseSeeder order is shown below. Configure every .invalid email placeholder in the assignment seeders first; only app-provisioned accounts that are trusted to hold these roles should receive assignments.
public function run(): void
{
$this->call([
DemoCompanySeeder::class,
DemoAdminSeeder::class,
]);
}2
3
4
5
6
7
After configuring those placeholders to identify existing trusted app accounts, run sync before the database seeder in deployment scripts. The deployment seeder must never create an Owner or Admin account from fixed credentials:
php artisan migrate --force
php artisan access:sync --force
php artisan db:seed --class=DatabaseSeeder --forceCommon Mistakes
Do not put scoped company roles in global_roles:
// Avoid for company membership
'global_roles' => [
'Owner' => [Permission::BillingManage],
],2
3
4
Use scoped roles instead:
'roles' => [
CompanyRole::Owner->value => [Permission::BillingManage],
],2
3
Do not assign a scoped role without a scope:
// Avoid for scoped apps
$user->assignGlobalRole(CompanyRole::Owner);2
Assign it in the scope:
$user->in($company)->assignRole(CompanyRole::Owner);Do not assume membership grants permissions. In apps scaffolded with access:scope, membership and access are separate. Attach the user to the scope and assign the access role when you want both.