modularize-rbac / laravel
Laravel bridge for modularize-rbac/core: Eloquent repositories, HTTP controllers, migrations, and optional Spatie permissions adapter.
Fund package maintenance!
Requires
- php: ^8.2
- laravel/framework: ^11.0|^12.0
- modularize-rbac/core: ^1.9
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.8
- phpbench/phpbench: ^1.2
- spatie/laravel-permission: ^6.24
- zircote/swagger-php: ^4.10
Suggests
- spatie/laravel-permission: ^6.0 — opt-in adapter that mirrors permissions into Spatie's `role_has_permissions` pivot so hosts using the HasRoles trait keep working with `$user->can()`. Enable via `config('access.spatie.enabled')`.
- dev-main
- v2.8.1
- v2.8.0
- v2.7.0
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.0
- v2.2.0
- v2.1.0
- v2.0.1
- v2.0.0
- v1.0.0
- dev-docs/npm-badges
- dev-fix/admin-react-deps-v0.1.1
- dev-fix/postman-drift-deterministic-ids
- dev-fix/ci-pest-phpunit-config
- dev-feat/admin-react-access-guard
- dev-feat/admin-react-audit-viewer
- dev-feat/admin-react-languages-admin
- dev-feat/admin-react-modules-tree
- dev-feat/bridge-v2.8.1-docs
- dev-feat/npm-publish-workflow
- dev-feat/admin-react-roles-page
- dev-feat/admin-react-storybook
- dev-feat/admin-react-vitest
- dev-feat/admin-react-sdk-types
- dev-feat/rename-admin-react
- dev-feat/sdk-postman-drift-gate
- dev-feat/sdk-ts-scaffold
- dev-docs/finalize-v2.8
- dev-feat/binding-history
- dev-feat/soft-delete-roles-languages
- dev-docs/finalize-v2.7
- dev-feat/audit-hash-chain
- dev-feat/audit-pii-redaction
- dev-docs/telemetry-recipes
- dev-feat/telemetry-events
- dev-feat/audit-log-level
- dev-docs/finalize-v2.6
- dev-feat/install-command
- dev-feat/access-seeder-stub
- dev-feat/eloquent-factories
- dev-docs/finalize-v2.5
- dev-feat/openapi-spec
- dev-feat/api-version-header
- dev-feat/bulk-rate-limit
- dev-feat/paginated-list-endpoints
- dev-docs/v2.4.0-finalize
- dev-perf/inheritance-module-cache
- dev-perf/preload-role-parents
- dev-feat/role-id-index
- dev-feat/phpbench-scaffold
- dev-docs/finalize-v2.3
- dev-feat/cached-repos
- dev-docs/finalize-v2.2
- dev-feat/import-export
- dev-feat/role-hierarchy
- dev-feat/permission-inheritance
- dev-feat/bulk-operations
- dev-feat/clone-role
- dev-docs/upgrading-quickstart
- dev-chore/ci-improvements
- dev-chore/composer-hygiene
- dev-feat/audit-purge-command
- dev-feat/i18n-exceptions
- dev-chore/form-request-validations
- dev-feat/missing-endpoints
- dev-chore/release-v2.0.1
- dev-test/adapter-coverage
- dev-chore/log-silenced-exceptions
- dev-chore/governance-docs
- dev-pr/v2.7-release
- dev-pr/v2.6-commands-policy
- dev-pr/v2.5-audit-log
- dev-pr/v2.4-has-access-permissions
- dev-pr/v2.3-decouple-spatie
- dev-chore/tighten-core-constraint
- dev-chore/rename-vendor
- dev-pr/7-tighten-core-constraint
- dev-pr/6-release-v1
- dev-pr/5-spatie-adapter
- dev-pr/4-http-adapter
- dev-pr/3-eloquent-adapter
- dev-pr/0-rename-and-packaging
This package is auto-updated.
Last update: 2026-05-26 14:02:43 UTC
README
Laravel bridge for modularize-rbac/core. Ships Eloquent repositories, HTTP controllers, FormRequests, migrations, an audit log pipeline, console commands, and an optional Spatie permission adapter.
What v2.0 ships
A drop-in admin RBAC layer with:
- Modules — feature catalog with hierarchy, soft-delete, sort order, i18n.
- Roles — guard-scoped, tenant-aware, level-ordered, system-flag protected.
- Permissions —
{slug}.{action}names, package-owned (Spatie is optional). - Role × Module matrix — flag-based UI translated to action names by a domain service.
- Languages + Translations — polymorphic translations with locale fallback.
- REST API —
/api/admin/modules,/roles,/languages,/audit. - Audit log — every domain event is auto-persisted to
access_audit_log. HasAccessPermissionstrait — drop on your User to make$user->can('events.view')work without Spatie.AccessAdminPolicy— turn-key Gate::before for the package'sadmin.*abilities.- Console commands —
access:diagnose,access:sync-spatie,access:audit. - Spatie integration is opt-in — the package works whether or not
spatie/laravel-permissionis installed.
Architecture
┌──────────────────────────────────────────────────────────────┐
│ Infrastructure (this package) │
│ Eloquent · Mappers · Controllers · Resources · Audit │
│ Console commands · Policies · Spatie adapter (opt-in) │
└──────────────────────────┬───────────────────────────────────┘
│ implements ports
┌──────────────────────────▼───────────────────────────────────┐
│ modularize-rbac/core (framework-agnostic, PHP 8.2+) │
│ Use-cases · Domain entities · Ports · Events · Read models │
└──────────────────────────────────────────────────────────────┘
Quickstart
From a fresh Laravel 11 / 12 host to a first authorized request in roughly five minutes.
1. Install
composer require modularize-rbac/laravel
php artisan vendor:publish --tag=access-config
php artisan migrate
# Optional: get an example seeder you can edit + run
php artisan vendor:publish --tag=access-seeder
php artisan db:seed --class=AccessSeeder
The seeder demonstrates the canonical flow (CreateModule → CreateRole → SyncRoleModules use-cases) and creates three modules + admin/viewer roles wired up correctly. Edit it to match your app, or read it as documentation and write your own.
2. Wire the User model
// app/Models/User.php use ModularizeRbac\Laravel\Concerns\HasAccessPermissions; class User extends Authenticatable { use HasAccessPermissions; }
3. Seed a module, a role, and a binding
// database/seeders/DatabaseSeeder.php (or a tinker session) use ModularizeRbac\Core\Application\Module\CreateModule\CreateModule; use ModularizeRbac\Core\Application\Module\CreateModule\CreateModuleInput; use ModularizeRbac\Core\Application\Role\CreateRole\CreateRole; use ModularizeRbac\Core\Application\Role\CreateRoleInput; use ModularizeRbac\Core\Application\RoleModulePermission\SyncRoleModules\SyncRoleModules; use ModularizeRbac\Core\Application\RoleModulePermission\SyncRoleModules\SyncRoleModulesInput; $module = app(CreateModule::class)->execute(new CreateModuleInput( slug: 'events', name: 'Events', redirect: '/events', icon: 'calendar', rootModuleId: null, sortOrder: 10, )); $role = app(CreateRole::class)->execute(new CreateRoleInput( name: 'event_viewer', displayName: 'Event Viewer', guardName: 'web', level: 100, )); app(SyncRoleModules::class)->execute(new SyncRoleModulesInput( roleId: $role->id, modules: [ ['module_id' => $module->id, 'is_reading_allowed' => true], ], )); DB::table('role_user')->insert([ 'role_id' => $role->id, 'user_id' => 1, 'organization_id' => null, 'created_at' => now(), 'updated_at' => now(), ]);
4. Use it
// In any controller / Gate / Blade if ($request->user()->can('events.view')) { // ✓ allowed via role_user → role_module_permission → module }
5. (Optional) Hit the admin API
The admin REST surface lives under config('access.route_prefix') (default api/admin). With a bearer token whose User has admin.modules.view:
curl -H "Authorization: Bearer $TOKEN" https://app.test/api/admin/modules
That's the full path. The rest of this README is configuration knobs, the full REST table, and architecture details.
Install
composer require modularize-rbac/laravel php artisan vendor:publish --tag=access-config php artisan migrate
Edit config/access.php and point tenant_model at your tenant class or leave null for single-tenant setups.
Host wiring
config/auth.php
Define the admin guard the package defaults to:
'guards' => [ 'admin' => [ 'driver' => 'sanctum', 'provider' => 'admin_users', ], ],
HasAccessPermissions on your User
use ModularizeRbac\Laravel\Concerns\HasAccessPermissions; class User extends Authenticatable { use HasAccessPermissions; }
Provides:
$user->rbacRoles()BelongsToMany via therole_userpivot$user->canAccess('events.view')— direct lookup against the package schema
The AccessServiceProvider registers Gate::before so $user->can('events.view') works through Laravel's normal authorization flow.
Tenant context (optional)
Multi-tenant hosts bind the current tenant id in the container from their tenant-resolution middleware:
$app->instance('access.current_tenant_id', (string) $request->user()->organization_id);
TenantContext::currentTenantId() reads this value. Single-tenant hosts never bind the key.
Spatie integration (optional)
spatie/laravel-permission is in suggest since v2.0. Install it alongside if you want role_has_permissions kept in sync (so Spatie's HasRoles trait keeps working on a different User model):
composer require spatie/laravel-permission
// config/access.php 'spatie' => [ 'enabled' => null, // null = auto, true = force on, false = force off ],
REST API
All routes under config('access.route_prefix') (default api/admin):
| Method | URL | Action |
|---|---|---|
| GET | /modules | List modules |
| POST | /modules | Create |
| GET | /modules/{id} | Show |
| PUT | /modules/{id} | Update |
| DELETE | /modules/{id} | Soft delete |
| GET | /roles | List roles |
| GET | /roles/{id} | Show + matrix |
| PUT | /roles/{id} | Update display_name + translations |
| PUT | /roles/{id}/modules | Sync the role's permission matrix |
| GET | /languages | List |
| POST | /languages | Create |
| GET | /languages/{id} | Show |
| PUT | /languages/{id} | Update |
| DELETE | /languages/{id} | Delete (rejects default) |
| PUT | /languages/{id}/default | Mark as default |
| GET | /audit | List audit entries (?event=&actor_id=&tenant_id=&since=&until=&limit=&offset=) |
Frontend & SDK
The bridge ships an openapi.json at the repo root that is the source of truth for two companion npm packages and a Postman collection.
TypeScript SDK — @modularize-rbac/sdk-ts
Spec-derived types + a thin openapi-fetch wrapper. Zero runtime cost when imported type-only.
npm i @modularize-rbac/sdk-ts
import { createClient } from '@modularize-rbac/sdk-ts'; const client = createClient({ baseUrl: 'https://app.test/api/admin', headers: { Authorization: `Bearer ${token}` }, }); const { data, error } = await client.GET('/roles', { params: { query: { guard: 'admin', limit: 25 } }, });
Or use types only:
import type { paths, components } from '@modularize-rbac/sdk-ts'; type Role = components['schemas']['Role'];
React admin components — @modularize-rbac/admin-react
Drop-in admin UI built on Radix Themes + React Query: <RolesPage />, <ModulesTreeEditor />, <LanguagesAdmin />, <AuditViewer />, <AccessGuard />. Each component renders against the same openapi.json so they always match the API the bridge exposes.
npm i @modularize-rbac/admin-react @modularize-rbac/sdk-ts @tanstack/react-query @radix-ui/themes
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Theme } from '@radix-ui/themes'; import { createClient } from '@modularize-rbac/sdk-ts'; import { RbacProvider, RolesPage } from '@modularize-rbac/admin-react'; import '@radix-ui/themes/styles.css'; const queryClient = new QueryClient(); const apiClient = createClient({ baseUrl: '/api/admin' }); export default function AdminApp() { return ( <QueryClientProvider client={queryClient}> <Theme accentColor="indigo" radius="medium"> <RbacProvider apiClient={apiClient}> <RolesPage limit={25} /> </RbacProvider> </Theme> </QueryClientProvider> ); }
Storybook with mock data lives in frontend/ — every component has a story.
Postman collection
Regenerated from the same spec, committed at postman.json. Drag it into Postman or Insomnia to get all endpoints with example bodies. The sdk-ts-drift CI gate keeps both the TS types and the Postman collection in lockstep with openapi.json.
Console commands
php artisan access:diagnose— pre-deploy health check.php artisan access:sync-spatie [--dry-run]— force resync of every role-module binding into Spatie's pivot.php artisan access:audit [--event= --actor= --tenant= --since= --until= --limit= --format=table|json]— query the audit log.
Authorization model
Two layers:
-
User layer —
Gate::before(registered by the ServiceProvider) calls$user->canAccess($ability)when the User has theHasAccessPermissionstrait. Resolvesevents.view-style abilities directly fromrole_user+role_module_permission+module_permissions. -
Admin layer —
AccessAdminPolicy(the defaultconfig('access.policies.admin')) wraps the samecanAccess()check but scoped toadmin.*abilities the package's use-cases consult (admin.modules.view,admin.audit.view, ...). Hosts override via config.
To grant admin.modules.view, create a module with slug admin.modules, bind it to a role with is_reading_allowed = true, and assign the role to the user via role_user.
Calling use-cases directly
Every use-case is container-resolvable:
use ModularizeRbac\Core\Application\Module\CreateModule\CreateModule; use ModularizeRbac\Core\Application\Module\CreateModule\CreateModuleInput; $module = app(CreateModule::class)->execute(new CreateModuleInput( slug: 'billing', name: 'Billing', redirect: '/billing', icon: 'receipt', rootModuleId: null, sortOrder: 10, ));
Telemetry recipes
The package dispatches two Laravel events for hosts that want observability hooks without patching the bridge:
ModularizeRbac\Laravel\Events\Telemetry\AbilityResolved— fires at the end of every$user->can(...)call withability,allowed,source(direct|ancestor|inheritance|none|malformed), anddurationMicros.ModularizeRbac\Laravel\Events\Telemetry\CacheLookup— fires on every read through the language + module read-cache decorators withnamespace,key,hit, andversion.
Listener exceptions are caught by the package, so a faulty telemetry listener can't break authorization or cache reads.
Sentry spans
// app/Providers/EventServiceProvider.php use ModularizeRbac\Laravel\Events\Telemetry\AbilityResolved; use Sentry\State\Scope; Event::listen(AbilityResolved::class, function (AbilityResolved $e): void { \Sentry\configureScope(function (Scope $scope) use ($e): void { $scope->setExtra('rbac.ability', $e->ability); $scope->setExtra('rbac.source', $e->source); $scope->setExtra('rbac.duration_us', $e->durationMicros); }); if ($e->durationMicros > 10_000) { \Sentry\captureMessage('Slow access check', \Sentry\Severity::warning()); } });
Prometheus via spatie/laravel-prometheus
use ModularizeRbac\Laravel\Events\Telemetry\AbilityResolved; use ModularizeRbac\Laravel\Events\Telemetry\CacheLookup; use Spatie\Prometheus\Facades\Prometheus; Event::listen(AbilityResolved::class, function (AbilityResolved $e): void { Prometheus::addHistogram('access_check_duration_us') ->labels(['source', 'allowed']) ->observe($e->durationMicros, [$e->source, $e->allowed ? '1' : '0']); }); Event::listen(CacheLookup::class, function (CacheLookup $e): void { Prometheus::addCounter('access_cache_lookups_total') ->labels(['namespace', 'hit']) ->incBy(1, [$e->namespace, $e->hit ? '1' : '0']); });
Structured JSON log (Logstash / OpenSearch)
use Illuminate\Support\Facades\Log; use ModularizeRbac\Laravel\Events\Telemetry\AbilityResolved; Event::listen(AbilityResolved::class, function (AbilityResolved $e): void { Log::channel('telemetry')->info('rbac.ability.resolved', [ 'ability' => $e->ability, 'allowed' => $e->allowed, 'source' => $e->source, 'duration_us' => $e->durationMicros, ]); });
Audit log failure level
The audit listener catches persistence failures (DB down, encoding quirk) so the main domain flow always completes. The level at which those failures land in the Laravel log is configurable:
// config/access.php 'audit' => [ 'enabled' => true, 'log_failures' => 'error', // warning (default) | error | critical | false ],
Set to false to swallow the failure silently for hosts that already
trap audit issues upstream.
Upgrading
- UPGRADING.md — consolidated upgrade guide for v2.0 → v2.1, v1.x → v2.0, and
casamento/rbac→ v1.0. - CHANGELOG.md — full history with all additive changes and bugfixes.
Layout
.
├── composer.json
├── config/access.php
├── database/migrations/ # v2.0 schema (idempotent)
├── routes/api.php
├── src/
│ ├── AccessServiceProvider.php
│ ├── Audit/ # AuditingListener
│ ├── Authorization/ # GateAuthorizer, AccessAdminPolicy
│ ├── Concerns/ # HasAccessPermissions trait
│ ├── Console/ # diagnose / sync-spatie / audit
│ ├── Eloquent/
│ │ ├── Mappers/ # Entity <-> Eloquent
│ │ └── Repositories/ # Implement core ports
│ ├── Events/ # LaravelEventDispatcher
│ ├── Http/ # Controllers / FormRequests / Resources
│ ├── Localization/ # LaravelLocaleResolver
│ ├── Models/ # Persistence DTOs
│ ├── Persistence/ # Clock / IdGenerator / UnitOfWork
│ ├── Spatie/ # Optional permission gateway
│ ├── Tenant/ # LaravelTenantContext
│ └── Translations/ # TranslationApplier
└── tests/ # Pest + Testbench (matrix: with/without Spatie)