liberusoftware/roles-permissions-api
A Laravel package that exposes roles and permissions over a REST API. Manage role assignments, permission checks, and access control from any frontend or service, with ready-made endpoints and configurable middleware for your application's authorization layer.
This package is a presentation adapter — it exposes roles and permissions over HTTP/JSON. It is not a standalone authorization engine; it sits on top of a domain module (typically a liberusoftware/roles-permissions core package) and assumes a host Liberu application. Treat it as the transport layer, not the source of truth.
Minimal setup
composer require liberusoftware/roles-permissions-api
Then, in the host app:
php artisan about or check bootstrap/providers.php). If the package ships a provider that is not auto-discovered, register it manually.php artisan vendor:publish --provider="Liberu\RolesPermissionsApi\RolesPermissionsApiServiceProvider"
php artisan route:list --path=api --columns=method,uri,name,middleware
Where to look first
routes/api.php (or src/Routes/) — the definitive list of endpoints and their middleware.src/Http/Controllers/ — shows how thin the controllers are and which domain actions/queries they call.src/Http/Requests/ — validation rules tell you the exact payload shape, which is often more reliable than the README.src/Http/Resources/ — the JSON contract you'll be consuming or overriding.config/roles-permissions-api.php (if present) — route prefix, middleware stack, and binding overrides.First use case
The fastest proof of life is reading roles for an authenticated user, then granting a permission to a role:
# as an authenticated user with a Sanctum token
curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
https://app.test/api/roles
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
-d '{"permissions":["posts.publish"]}' \
https://app.test/api/roles/editor/permissions
If those two calls work end-to-end, the adapter, guard, and domain module are wired correctly. Everything else is variation on this pattern.
Consume the API rather than re-implementing it. The strongest reason to install this package is to avoid hand-writing CRUD endpoints for roles and permissions in every Liberu-based app. If you find yourself writing a RoleController, stop and check whether the adapter already covers it.
Thin controller → form request → domain action → API resource. Every endpoint in the package follows this shape. When you need to extend behavior, mirror it rather than breaking it:
class GrantPermissionsController
{
public function __invoke(
GrantPermissionsRequest $request,
GrantPermissionsToRole $action, // domain-layer action
Role $role
): RoleResource {
$action->handle($role, $request->validated('permissions'));
return new RoleResource($role->fresh('permissions'));
}
}
Client-side integration. When the API is consumed by a separate frontend or another service, centralize the calls. A small typed client beats scattering Http:: calls:
class RolesClient
{
public function __construct(private PendingRequest $http) {}
public function assign(string $role, array $permissions): array
{
return $this->http
->post("/api/roles/{$role}/permissions", ['permissions' => $permissions])
->throw()
->json();
}
}
Register it as a singleton binding so tests can swap in a fake.
Authorization at the route layer, not in controllers. The package's routes typically carry can: or a permission middleware. When you add your own endpoints that touch roles, use the same middleware stack — otherwise you end up with two authorization models that drift:
Route::middleware(['auth:sanctum', 'can:roles.manage'])
->prefix('api/admin')
->group(base_path('routes/admin-roles.php'));
Seeding and bootstrap. Roles and permissions need to exist before the API is useful. Keep a seeder in your app that calls the same domain actions the adapter calls — not raw inserts — so events, caches, and validation all fire:
public function run(RoleCreator $creator, PermissionCreator $permissions): void
{
$permissions->handle(['posts.view', 'posts.publish', 'users.manage']);
$creator->handle('editor', ['posts.view', 'posts.publish']);
$creator->handle('admin', ['*']);
}
Testing. Because the adapter is HTTP-facing, test through the HTTP layer with Sanctum::actingAs and the package's own factories if shipped. Assert on the JSON contract, not on internal calls:
Sanctum::actingAs(User::factory()->create());
$this->getJson('/api/roles')
->assertOk()
->assertJsonStructure(['data' => [['id', 'name', 'permissions']]]);
Versioned override via container bindings. If you need a different payload (e.g. add description or nest permissions differently), bind your own resource class over the package's in a service provider rather than editing vendor code:
$this->app->bind(
\Liberu\RolesPermissionsApi\Http\Resources\RoleResource::class,
\App\Http\Resources\RoleResource::class
);
Only reliable if the package resolves resources from the container — verify before relying on it.
Integration with Spatie's package. If the underlying domain module is backed by spatie/laravel-permission, the adapter is a façade over it. Do not mix raw $user->givePermissionTo() calls with API mutations without clearing caches — see gotchas.
License is NOASSERTION. That means the repository did not declare a recognized license. Before shipping this to production, check the actual LICENSE file in the repo and confirm with the maintainer. For an internal Liberu monolith this is usually fine; for redistribution it is a legal blocker.
Zero stars, zero dependents, and a future-dated release. Treat this as pre-1.0 internal infrastructure. Pin the exact version in composer.json ("liberusoftware/roles-permissions-api": "0.1.*" or a commit hash) and read the source of every endpoint you depend on. The README, if it exists, will likely lag the code — trust routes/api.php and the form requests.
"Presentation adapter" means no business logic belongs here. If you find yourself adding rules inside a controller, the change probably belongs in the upstream domain module. Forking the adapter to add behavior is a strong signal you're solving the problem in the wrong layer.
Route collisions are the most common install failure. A bare /api/roles path will clash with anything your app already registers. Check the config for a prefix (e.g. api/access), and if there isn't one, wrap the package's routes in your own group:
Route::prefix('api/access')->group(function () {
$this->loadRoutesFrom(base_path('vendor/liberusoftware/roles-permissions-api/routes/api.php'));
});
Guard mismatch produces 401s that look like package bugs. If your app authenticates with web sessions and the package routes use auth:sanctum, every request fails silently. Align the guard in the package config with your actual setup, and always send Accept: application/json when curling — without it Laravel redirects to a login page and you get HTML back instead of a 401.
Permission cache staleness. Nearly every implementation of this stack caches the permission map. After any role or permission mutation, the cache must be flushed. If the package does not do this automatically for your auth driver, hook it into the model events or run the flush in a terminate middleware. Symptom: a permission grant "works" but the user sees no change until the next deploy.
Eager-load permissions. Listing roles without with('permissions') produces an N+1 that only shows up in production. If the package's index endpoint does not eager-load, override the resource or add a scope in your own endpoint.
Validation errors and status codes. The package will return Laravel's standard 422 shape (message + errors). Normalize this in your client — don't assume error or detail keys. Likewise, check whether a missing role returns 404 or 403; that distinction matters for frontend behavior and is often inconsistent in young packages.
Super-admin bypass. Many Liberu modules grant a blanket pass via a Gate::before in the domain module, not the adapter. If your API calls return 403 for a user who is clearly an admin, check the gate definition in the core package before debugging routes.
Extension points, in order of safety: container bindings for resources and requests → published config for prefix and middleware → published routes file for structural changes → forking. Anything below the third option will be wiped on composer update.
Debugging checklist when an endpoint misbehaves: route:list to confirm the route exists and which middleware wraps it; php artisan route:list --path=api -v to see the full stack; check the bound domain action in the container; and finally add a log line in the controller to confirm your request reaches it. Because the adapter is thin, most failures are wiring — auth, binding, or config — not business logic.
How can I help you explore Laravel packages today?