NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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');  // ?Role

Register 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']);  // true

An empty permission name never passes, even with *.

Role drivers

roles.providerClassBehaviour
arrayInMemoryRoleProviderDefault. Roles registered at boot live in memory for the whole process — fast and version-controlled.
databaseDatabaseRoleProviderPersists roles as team_roles rows (upsert by key); the role map is read at most once per request or queued job.
database + cacheCachedRoleProviderWraps 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 key

register() 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        # seconds

Revocations 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 | Permissions

Show 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 crypto

By 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.