Skip to content
Go back

Do You Actually Need a Super Admin?

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:

ApproachComplexityVisible to customersCan be locked outWorks with Spatie teams
is_admin boolean (scattered checks)Low to start, grows over timeDepends on implementationNoBypasses it entirely
is_admin + Gate::before()Low (one line)No — admin can be invisibleNoBypasses it cleanly
Impersonation (“Login As”)Medium (package required)No — you log in as another userNoWorks alongside it
Laravel NovaMedium-High (separate admin panel)No — separate UI entirelyNoSeparate system
Just be a Manager on every teamLowestYes — you appear on every teamPossible (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:

  1. Are you the only admin? Yes — I’m a solo founder.
  2. Is your admin work just Manager-level work? Yes — editing strategy, managing users, nothing beyond what any Manager does.
  3. 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:

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.


Back to top ↑