silvanite/brandenburg
Brandenburg is a lightweight Laravel package for managing user roles and permissions. Assign roles to users, define permissions, and gate access across your application using familiar Laravel authorization patterns—keeping access control simple, flexible and maintainable.
Brandenburg is a Nova tool that wraps spatie/laravel-permission with a UI for roles and permissions. It is not a replacement for Spatie — it is a management surface on top of it, so the install order matters.
Prerequisites (do these first, in this order):
php artisan nova:install).composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
User model (and any other authenticatable model):use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
Install Brandenburg:
composer require silvanite/brandenburg
php artisan vendor:publish --provider="Silvanite\Brandenburg\BrandenburgServiceProvider"
php artisan migrate
Register the tool in app/Providers/NovaServiceProvider.php:
use Silvanite\Brandenburg\Brandenburg;
public function tools()
{
return [
new Brandenburg,
];
}
Then hit nova/resources/brandenburg (or the menu entry Nova adds) as an authenticated Nova user.
Where to look first: the published config/brandenburg.php. That is where the guard, the user model, and the role/permission resource wiring live. Read it before you touch anything else — most “it doesn’t work” reports trace back to a guard or model mismatch there.
First use case — the 10-minute smoke test: create a Role called editor, attach a permission like view posts, assign the role to yourself from the users screen, then in tinker confirm:
auth()->user()->can('view posts'); // true
auth()->user()->getAllPermissions()->pluck('name');
If that returns true, the plumbing is correct and everything after this is naming discipline.
Seed your permission catalogue, manage assignments in the UI. Treat permission names as code (they appear in Gate::authorize, policies, Blade @can, and Nova canSee callbacks), and treat role→permission mappings as data. A seeder makes this reproducible across environments:
// database/seeders/PermissionSeeder.php
foreach (['view posts', 'create posts', 'publish posts'] as $permission) {
Permission::firstOrCreate(['name' => $permission, 'guard_name' => 'web']);
}
$editor = Role::firstOrCreate(['name' => 'editor', 'guard_name' => 'web']);
$editor->syncPermissions(['view posts', 'create posts']);
Run it on every deploy. firstOrCreate keeps it idempotent; Brandenburg then becomes the place where humans attach roles to users, not where the schema of permissions is invented ad hoc.
Guard discipline is the whole ballgame. Spatie scopes every permission to a guard. Nova’s default guard is usually web, so keep Brandenburg’s config pointed at the same guard Spatie uses:
// config/brandenburg.php
'guard' => 'web',
If you run a separate nova guard (common when admins and end users are different tables), create both sets of permissions explicitly and never mix them. A role with web permissions silently does nothing on a nova-guarded request.
Gate everything through permissions, not roles. Roles are a bundle; permissions are the contract. Code should almost always check the permission:
// app/Policies/PostPolicy.php
public function update(User $user, Post $post): bool
{
return $user->can('edit posts') && $post->author_id === $user->id;
}
Then reuse the same name in Nova resources so the admin panel and the app stay consistent:
public static function authorizedToCreate(Request $request)
{
return $request->user()->can('create posts');
}
// or per-field
Text::make('Internal Notes')
->canSee(fn (Request $request) => $request->user()->can('view internal notes')),
Super-admin escape hatch. Rather than assigning every permission to an admin role, intercept authorization once:
// AuthServiceProvider::boot()
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
Returning null (not false) is essential — false short-circuits every other gate.
Nova navigation that follows permissions. Use the permission names from the UI to drive menu visibility, so a role change in Brandenburg changes the admin UI without a deploy:
public static function availableForNavigation(Request $request): bool
{
return $request->user()->can('view posts');
}
Testing pattern. Seed permissions in the test base class and assert against them, not against Nova clicks:
protected function setUp(): void
{
parent::setUp();
$this->seed(PermissionSeeder::class);
}
public function test_editor_can_create_posts(): void
{
$user = User::factory()->create();
$user->assignRole('editor');
$this->actingAs($user)->get('/posts/create')->assertOk();
}
Bulk operations. Brandenburg’s UI is fine for tens of users, painful for hundreds. For bulk role changes, script it with Spatie directly (User::role('pending')->each->assignRole('member')) and remember the cache step in the next section.
The package is effectively frozen at 2020. Last release September 2020 means it predates Nova 4 in practice. Expect to pin Nova 3.x, or budget a fork-and-patch. Check composer.json constraints before you upgrade anything, and do not assume the Nova tool boots cleanly on a current stack. If you are greenfield on Nova 4+, consider using Spatie directly with a thin Nova resource instead — the value Brandenburg adds is UI, and it is UI you can rebuild in an afternoon.
Permission cache will bite you constantly. Spatie caches roles/permissions. Changes made through Brandenburg’s UI handle the flush; changes made in seeders, tinker, or migration scripts often do not appear until you reset:
php artisan permission:cache-reset
Or programmatically, after a bulk update:
app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
If a permission “doesn’t work” and the database clearly has it, this is the first thing to check — before you go looking at guards.
Guard mismatch is the #1 silent failure. Role::create(['name' => 'admin']) without guard_name falls back to the configured default guard, which may not be the guard your authenticated user is resolved against. Always pass guard_name explicitly in seeders. Then verify with:
$user->getAllPermissions()->pluck('name', 'guard_name');
Deleting permissions breaks code silently. Brandenburg lets you delete a permission that a policy references in a string. Nothing errors — can() just returns false forever. Keep permission names in a PHP enum or constants class, and reference those constants in both seeders and policies so a rename is a search-and-replace, not a hunt.
Authorize the tool itself. By default anyone who can reach Nova can manage roles — which means anyone who can reach Nova can grant themselves anything. Lock it down in the tool class:
public function canSee(Request $request): bool
{
return $request->user()->hasRole('super-admin');
}
This is the most commonly skipped step and the most consequential one.
Migration ordering on fresh installs. Brandenburg’s migrations assume Spatie’s tables exist. On a fresh migrate from a clean database, confirm Spatie’s publish timestamp sorts earlier; if not, rename the published Spatie migration file so it runs first. CI is where this surfaces, not local dev.
Extension points worth knowing. Publish the config and views to customise the UI rather than forking. If you need extra fields on roles (description, colour, tenant id), extend the Spatie Role model and point config/permission.php at it — Brandenburg will read through the configured model, so the UI picks up your fields. Test that path early; model swapping is the least-exercised part of the package.
Debugging checklist when authorization misbehaves: (1) permission:cache-reset, (2) print $user->getAllPermissions() with guards, (3) confirm config/auth.php default guard matches config/brandenburg.php, (4) check Gate::before isn’t returning false, (5) confirm the policy is actually registered in AuthServiceProvider. In that order — it resolves roughly 95% of cases.
How can I help you explore Laravel packages today?