Roles are named bundles of permissions. A membership stores only the role key; the role itself resolves through a role provider, which Teams::roles() returns. Register roles in a service provider’s boot() method — a key, a display name, optional permissions (strings or Permission objects) and an optional description. The * permission grants everything:
use RoundlyConsulting\Teams\Facades\Teams;
use RoundlyConsulting\Teams\Roles\Permission;
// In a service provider's boot() method:
Teams::roles()->register('owner', 'Owner', ['*']);
Teams::roles()->register('admin', 'Admin', ['*']);
Teams::roles()->register(
'editor',
'Editor',
['posts.edit', new Permission('posts.publish', 'Publish posts')],
description: 'Can edit and publish content.',
);
Teams::roles()->register('member', 'Member', ['posts.view']);
Teams::roles()->all(); // array<string, Role>
Teams::roles()->find('editor'); // ?RoleRegister every key your app assigns — including owner, admin and member, which the package itself assigns on team creation, ownership transfer and join-request approval (configurable under roles.*). A membership whose key isn’t registered resolves no role and has no permissions.
The Role object
$role = Teams::roles()->find('editor');
$role->key; // 'editor'
$role->name; // 'Editor'
$role->description; // 'Can edit and publish content.'
$role->permissions; // ['posts.edit', 'posts.publish']
$role->permissionObjects; // list<Permission>
$role->hasPermission('posts.publish'); // '*' grants everything
$role->hasAnyPermission(['posts.delete', 'posts.edit']); // true
$role->hasAllPermissions(['posts.edit', 'posts.publish']); // trueAn empty permission name never passes, even with *.
Role drivers
| roles.provider | Class | Behaviour |
|---|---|---|
array | InMemoryRoleProvider | Default. Roles registered at boot live in memory for the whole process — fast and version-controlled. |
database | DatabaseRoleProvider | Persists roles as team_roles rows (upsert by key); the role map is read at most once per request or queued job. |
database + cache | CachedRoleProvider | Wraps the database driver; caches the role map and flushes it whenever a role definition changes. |
Storing roles in the database
Set roles.provider to database and the same Teams::roles()->register() call upserts a team_roles row by key, so admins can manage roles at runtime. Permission checks don’t change:
// .env → TEAMS_ROLES_PROVIDER=database
Teams::roles()->register('editor', 'Editor', ['posts.edit', 'posts.publish'], description: 'Writes posts'); // upserts a team_roles row by keyregister() is an upsert on the key with either driver: registering a key again replaces its name, permissions and description, and a soft-deleted definition is restored rather than re-inserted. The Role it returns is a read-only result — change a role by registering it again.
Caching the database driver
Set roles.cache.enabled to cache the database role map. It’s flushed on every Teams::roles()->register() — tag-aware stores by tag, other stores by forgetting the single key. The cache wraps only the database driver:
TEAMS_ROLES_PROVIDER=database
TEAMS_ROLES_CACHE=true
TEAMS_ROLES_CACHE_STORE=redis # null = the default store
TEAMS_ROLES_CACHE_KEY=teams.roles
TEAMS_ROLES_CACHE_TTL=3600 # secondsRevocations in long-lived processes
The role provider is bound scoped: the role map is read at most once per request or queued job — queue workers and Octane start each one with a fresh provider — so a permission revoked by another process stops granting from the next request or job. Saving or deleting a role definition, through register() or straight through the RoleDefinition model from an admin screen, flushes the current process’s map and the shared cache at once, and a cache refill always reads the table, never a worker’s older copy.
Listing roles
php artisan teams:roles # Key | Name | PermissionsShow your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.