I had an is_admin column in my users table for two years. It worked. Nobody complained. Then I asked a simple question: what does it actually do?
The answer led me to remove the entire concept from Growth Method — no super admin role, no special bypass, no replacement pattern. Just the existing role system, used as designed.
The scattered check problem
When you add is_admin to your users table, it starts small. One check in a middleware. Then a policy. Then a Blade template. Before long, you have || $user->is_admin scattered across your codebase:
// Middleware
if (! empty(auth()->user()) && ! auth()->user()->is_admin) {
// team permission checks...
}
// Policy
return $authUser->hasPermissionTo(Permissions::EDIT_USERS)
|| $authUser->is_admin;
// Livewire component
$this->canEdit = auth()->user()->hasRole('Manager')
|| auth()->user()->is_admin;
// Blade template
@if (auth()->user()->teams->count() > 1 || auth()->user()->is_admin)
In Growth Method, this check appeared in six files. Every new feature needed to remember the admin bypass.
The deeper problem: is_admin lives outside your role system. It’s a boolean column on the users table — a platform-level superuser flag that sits above Spatie Permissions. You end up with two parallel authorisation systems: Spatie roles for normal users, plus a boolean column for the admin. Two sources of truth for the same question: “can this user do this thing?”
Five ways to handle admin access
Before settling on an approach, I looked at five options. Here’s how they compare:
| Approach | Complexity | Visible to customers | Can be locked out | Works with Spatie teams |
|---|---|---|---|---|
is_admin boolean (scattered checks) | Low to start, grows over time | Depends on implementation | No | Bypasses it entirely |
is_admin + Gate::before() | Low (one line) | No — admin can be invisible | No | Bypasses it cleanly |
| Impersonation (“Login As”) | Medium (package required) | No — you log in as another user | No | Works alongside it |
| Laravel Nova | Medium-High (separate admin panel) | No — separate UI entirely | No | Separate system |
| Just be a Manager on every team | Lowest | Yes — you appear on every team | Possible (but unlikely) | Uses it as designed |
is_admin boolean — The quick fix. Add a column, sprinkle || $user->is_admin wherever you need it. Works fine at first, but the checks spread across your codebase and you end up with two parallel authorisation systems.
is_admin + Gate::before() — The proper Laravel fix. Centralises the bypass in one line. Every policy and gate check automatically respects it. Still requires the is_admin column, but removes the scattered checks.
Impersonation — Packages like laravel-impersonate let you “log in as” another user. You see exactly what they see. Good for support and debugging, but you’re not yourself — you’re pretending to be someone else, which makes admin-level changes awkward.
Laravel Nova — Laravel’s first-party admin panel gives you a separate interface with its own authorisation. Powerful, but it’s a whole separate application. Overkill if you just need to switch between teams and do Manager-level work.
Just be a Manager — No new concepts, no packages, no bypass. You’re a regular user with the Manager role on every team. The role system works exactly as designed. The trade-off: customers can see you on their team, and in theory a customer Manager could change your role (though you control the app, so you can fix it).
The “proper” fix
Laravel has a built-in solution for this. Gate::before() lets you intercept every authorisation check before it reaches your policies. One line in AppServiceProvider:
Gate::before(function (User $user) {
return $user->is_admin ? true : null;
});
The Laravel documentation recommends this pattern. Taylor Otwell built it for this exact use case. It centralises the bypass, removes the scattered checks, and every new policy automatically respects it.
I implemented this. It worked. Then I asked a better question.
Three questions that changed my mind
Before reaching for Gate::before(), I asked myself:
- Are you the only admin? Yes — I’m a solo founder.
- Is your admin work just Manager-level work? Yes — editing strategy, managing users, nothing beyond what any Manager does.
- Is Manager the top level of access? Yes — there’s nothing above Manager in the app.
If the answer to all three is yes, you’re not a super admin — you’re just a Manager who happens to be on every team.
The one-word fix
Growth Method already had code that added me to every new team when a customer signed up. It just assigned me as Member instead of Manager:
$stuart = User::find(2);
if ($stuart) {
$stuart->assignRole('Manager'); // was 'Member'
}
That’s the entire change. One word.
After that, every || $user->is_admin check became redundant — hasRole('Manager') already returns true for me on every team. I removed the scattered checks, deleted the SuperAdminMiddleware, and created a migration to drop the column:
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
Six files simplified. One middleware deleted. One database column dropped. One concept removed from the codebase entirely.
The real questions I asked
These are the actual questions from the session:
My super admin role isn’t really a role. I’m a solo founder, so it’s just for one user (me). So I could add myself to every team as a Manager as part of the account creation process?
And later, after the full analysis:
The most useful thing I can do as a super admin is switch between all teams. Other than that I am effectively acting as a team manager — I can edit strategy, edit users, delete users etc. Is there a simpler alternative here?
The answer was: yes, the simplest alternative is to stop pretending you’re a different kind of user.
When you DO need a super admin
Gate::before() is the right tool when:
- You have multiple admins with different access levels (support staff, billing admin, engineering admin)
- The admin shouldn’t appear in team member lists — customers seeing an unknown “Stuart” on their team might be confusing
- You need access above what any role provides — deleting teams, exporting all data, viewing billing across accounts
- You can’t risk a customer changing your role — if a team Manager could downgrade or remove your access
If any of these apply, use Gate::before(). It’s one line, it’s in the docs, and it handles all of these cleanly.
But if none of them apply — if you’re a solo founder doing Manager-level work — you’re adding complexity for a problem you don’t have.
YAGNI applies to patterns too
This might change. If I hire support staff who need limited cross-team access, I’ll need to reintroduce a super admin concept. If customers start asking why I appear on their team, I’ll need to rethink visibility.
But YAGNI applies to architectural patterns, not just features. Building for hypothetical future requirements is the same mistake whether you’re adding a feature nobody asked for or adding a pattern nobody needs yet.
For now, the simplicity of removing is_admin wins. The codebase has one fewer concept, one fewer source of truth, and zero special cases. If the requirements change, Gate::before() will still be there — one line of code away.
The best code is code you don’t write. Sometimes that includes the patterns everyone tells you to implement.
Related: What Would Taylor Otwell Do? explores the decision-making framework behind choices like this. Updating My Codebase to Follow the Laravel Way covers a broader refactoring session driven by the same principles.