PHPackages                             codenzia/laravel-superadmin - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. codenzia/laravel-superadmin

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

codenzia/laravel-superadmin
===========================

Protected super admin account for Laravel. Zero-config authorization via Gate::before, defense-in-depth Eloquent observer, optional Filament v4 plugin, vendor-only CLI commands with friction controls. Designed for vendor-deployed applications where customer admins must not accidentally delete the vendor's support account.

v0.4.0(2w ago)049↑267.3%MITPHPPHP ^8.3CI failing

Since May 11Pushed 1w agoCompare

[ Source](https://github.com/Codenzia/laravel-superadmin)[ Packagist](https://packagist.org/packages/codenzia/laravel-superadmin)[ Docs](https://github.com/codenzia/laravel-superadmin)[ RSS](/packages/codenzia-laravel-superadmin/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (11)Versions (10)Used By (0)

Laravel SuperAdmin — Zero-friction protected admin account
==========================================================

[](#laravel-superadmin--zero-friction-protected-admin-account)

[![Latest Version](https://camo.githubusercontent.com/62de5c43149eaae047a2cc6a35724e9b4f2bd4d23619ab4ebd828543107e0306/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f636f64656e7a69612f6c61726176656c2d737570657261646d696e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codenzia/laravel-superadmin)[![PHP Version](https://camo.githubusercontent.com/91550e99ed59b82dd8ce13f1f442bcae1531c079908b6f06bc5d12be23c27e69/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f636f64656e7a69612f6c61726176656c2d737570657261646d696e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/codenzia/laravel-superadmin)[![Laravel](https://camo.githubusercontent.com/06b8e69143bdc79b83cc6856eace3dd368452c5bee12a333cced8d63705ef714/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3131253230253743253230313225323025374325323031332d6566343434343f7374796c653d666c61742d737175617265)](https://laravel.com)[![Filament](https://camo.githubusercontent.com/a4590ecd31837ed0b59c24f0e88d92d9da4b077f18d050fe8aa87d3d95154a57/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f46696c616d656e742d763425323025374325323076352d6635396530623f7374796c653d666c61742d737175617265)](https://filamentphp.com)[![Tests](https://github.com/Codenzia/laravel-superadmin/actions/workflows/tests.yml/badge.svg?style=flat-square)](https://github.com/Codenzia/laravel-superadmin/actions/workflows/tests.yml)[![License](https://camo.githubusercontent.com/3400c677f0d10ee82921b90464ce2771285abf6ee6ca0c6185feb4a1b722aae9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f636f64656e7a69612f6c61726176656c2d737570657261646d696e2e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

> Drop-in **protected super-admin account** for Laravel. Composer require, run migrate, and you have a working super-admin login. One env var (or one interactive command) overrides the defaults. No friction, no ceremony.

---

What you get
------------

[](#what-you-get)

- A **single protected user** that is auto-created on first `migrate`. Default email derived from `APP_URL` / `APP_NAME`. Default password `superadmin`.
- An **Eloquent observer** that blocks deletion, email changes, **unprotect attempts (`true → false`)**, and **mass-assignment privilege escalation (`false → true`)** on the `is_protected` flag.
- A **`Gate::before` hook** so the super admin authorizes for every ability — works without Spatie, Shield, or any policies wired up.
- **Late role assignment.** Solves the `MigrationsEnded` vs Spatie-Role-row race: when `spatie/laravel-permission` is in use, the `super_admin` role row often doesn't exist yet at auto-install time, so the role would silently fail to attach. A wildcard `eloquent.created` listener retroactively assigns the configured role the moment the row appears in a later seeder run. Idempotent and best-effort; no-ops cleanly when Spatie isn't installed.
- A **Filament plugin** that hides destructive row actions (`delete`, `suspend`, `ban`, `impersonate`, …) and disables privileged form fields (`roles`, `status`, `email`, …) on the protected user row — automatically, across every consumer app, with no per-resource code.
- A **`superadmin:ensure` command** that interactively rotates name + email + password. DB-only — never reads or writes `.env`.
- A **`superadmin:status` command** (with `--verbose` for full health diagnostics) so you can verify the install in one shot.

Quick start
-----------

[](#quick-start)

```
composer require codenzia/laravel-superadmin
php artisan migrate
# ✓ Created protected super admin: superadmin@ (password: superadmin)
# Override defaults in your seeder via SuperAdmin::ensure([...]). Change later with `php artisan superadmin:ensure`.
```

That's the whole install. The package listens to `MigrationsEnded` and creates the protected user once, if and only if no protected user exists. Re-running `migrate` is a no-op.

### Override the defaults — two paths

[](#override-the-defaults--two-paths)

v0.4.0+ keeps identity fields (name / email / password) out of `.env` and config entirely. Plaintext credentials never live on the host filesystem. Two override paths:

**(1) Pin the values in your seeder** — runs every `migrate:fresh --seed` / on first install:

```
use Codenzia\SuperAdmin\Facades\SuperAdmin;

class UserSeeder extends Seeder
{
    public function run(): void
    {
        SuperAdmin::ensure([
            'name'     => 'Super Admin',
            'email'    => 'admin@your-app.test',
            'password' => 'your-strong-password',
        ]);
    }
}
```

Pass any subset of `['name', 'email', 'password']`. Omitted keys fall back to package defaults on create; on update they're left unchanged (password specifically — omit to keep the current hash).

**(2) Rotate post-install** — DB-only artisan command:

```
php artisan superadmin:ensure
# Super admin name [Super Admin]:
# Super admin email [admin@your-app.test]:
# Super admin password (leave blank to keep current):
# ✓ Updated protected super admin: admin@your-app.test
```

Non-interactive variant:

```
php artisan superadmin:ensure --email=admin@your-app.test --password='your-strong-password'
```

`superadmin:ensure` never reads or writes `.env`. Plaintext only lives in the seeder source (committed to your repo with code) or in the operator's terminal during rotation.

> **Production password warning.** The default `superadmin` is deliberately memorable for local dev and internal use. **Always** override via the seeder or rotate via `superadmin:ensure` before exposing the app to anyone.

Default email resolution
------------------------

[](#default-email-resolution)

When the seeder doesn't pass `email`, the package derives one from your host's own config — never a vendor domain:

1. `superadmin@` where ` = parse_url(config('app.url'), PHP_URL_HOST)`
2. else `superadmin@.local` where ` = Str::slug(config('app.name'))`

So `APP_URL=https://myshop.com` → `superadmin@myshop.com`. `APP_NAME="My Shop"` with no URL → `superadmin@my-shop.local`.

Default role resolution (Filament Shield bridge)
------------------------------------------------

[](#default-role-resolution-filament-shield-bridge)

When `bezhansalleh/filament-shield` is installed, `configuredRole()` auto-discovers Shield's super-admin role name from `filament-shield.super_admin.name`. Apps don't need to set the role name in two places. When Shield is not present, the package falls back to the literal `'super_admin'`.

How protection works
--------------------

[](#how-protection-works)

The package identifies the protected row via the `users.is_protected = true` DB column. v0.4.0+ removed the secondary email-match path since identity is no longer env-driven — the flag is the single source of truth, set by `install()` / `ensure()` and defended by the observer.

Four protection layers — each independent, so tampering with one doesn't silently disable the others:

LayerBehaviorEloquent observerThrows `ProtectedAccountException` on **delete**, **email change**, **unprotect (`true → false`)**, and **promote (`false → true` outside `withoutProtection()`)**. The last is what blocks mass-assignment escalation when a consumer app puts `is_protected` in `$fillable`.`Gate::before`Returns `true` for the protected user on every `can()` / policy / `@can` check — no Spatie or Shield requiredFilament plugin (UX layer)Auto-hides destructive row actions (`delete`, `suspend`, `ban`, `impersonate`, …) and auto-disables privileged form fields (`roles`, `status`, `email`, `is_protected`, …) on the protected user row. Zero per-resource code. See [Filament](#filament) below.Late role assignmentWildcard `eloquent.created` listener that retroactively assigns the configured role to the protected user the moment the role row exists (typically after `migrate --seed`).The observer is defense-in-depth. Use the facade in your policies for proper HTTP 403s (see [UserPolicy](#userpolicy) below).

### App-side defense-in-depth (recommended)

[](#app-side-defense-in-depth-recommended)

Even with the observer guarding `false → true` promotion, you should keep `is_protected` out of the User model's `$fillable`. The observer only fires on `update`, and only inside Eloquent — raw `DB::table('users')->update(...)` calls bypass it. The two-layer pattern:

```
class User extends Authenticatable
{
    use IsSuperAdmin;

    // is_protected is intentionally NOT fillable. Only the package's
    // SuperAdmin::install() / SuperAdmin::ensure() (which wrap the
    // assignment in SuperAdmin::withoutProtection()) may set it.
    protected $fillable = ['name', 'email', 'password', 'phone', 'slug'];
}
```

Commands
--------

[](#commands)

CommandPurpose`superadmin:ensure`Create or update the protected user. **DB-only — never reads or writes `.env`.** Interactive prompts for name / email / password; pass any subset as flags to skip prompts.`superadmin:status`Summary of the protected user. Exits non-zero if missing.`superadmin:status --verbose`Adds the full health diagnostic matrix (model resolvable, column exists, protection enabled, role assigned, etc.).```
php artisan superadmin:status
# +----+--------------------+--------------------------+---+
# | #  | Setting            | Value                    |   |
# +----+--------------------+--------------------------+---+
# | 1  | Email              | superadmin@your-app.test | ✓ |
# | 2  | is_protected       | true                     | ✓ |
# | 3  | Role               | super_admin              | ✓ |
# +----+--------------------+--------------------------+---+
# ✓ Healthy.
```

Configuration
-------------

[](#configuration)

The package config is small. After `php artisan vendor:publish --tag=superadmin-config`:

```
return [
    // v0.4.0+: identity (name / email / password / role) is NOT in this
    // config and NOT in env. See "Override the defaults" above — defaults
    // are seeder-driven via SuperAdmin::ensure([...]) or derived.
    'user_model'            => null,                                              // null = resolved from auth.providers
    'auto_install'          => env('SUPER_ADMIN_AUTO_INSTALL', true),             // create user on MigrationsEnded
    'authorization'         => ['gate_before' => true],                           // super admin passes every can()
    'protection'            => ['enabled' => env('SUPER_ADMIN_PROTECTION', true)],
    'late_role_assignment'  => env('SUPER_ADMIN_LATE_ROLE_ASSIGNMENT', true),     // attach role when row appears later
    'filament' => [
        'hide_destructive_actions' => true,                                       // master switch for the Filament plugin

        // Row actions auto-hidden on the protected user row. Apps extend by
        // merging their own entries — see "Filament" section below.
        'hidden_action_names' => [
            'delete', 'forceDelete',
            'suspend', 'unsuspend', 'ban', 'unban',
            'markEmailVerified', 'verify', 'unverify',
            'impersonate', 'demote',
        ],

        // Form fields auto-disabled when editing the protected user.
        'locked_field_names' => [
            'roles', 'role', 'permissions',
            'status', 'is_protected', 'email', 'user_type',
        ],
    ],
];
```

Seeder integration
------------------

[](#seeder-integration)

`SuperAdmin::ensure()` is the seeder-safe primitive. Two modes:

```
use Codenzia\SuperAdmin\Facades\SuperAdmin;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // (a) No args — idempotent get-or-create.
        //     Returns the existing protected user, or creates one with
        //     defaultName() / defaultEmail() / defaultPassword().
        $superAdmin = SuperAdmin::ensure();

        // (b) With array — force-applies the supplied fields. Use this
        //     to pin app-specific values that survive every reseed.
        $superAdmin = SuperAdmin::ensure([
            'name'     => 'Super Admin',
            'email'    => 'admin@your-app.test',
            'password' => 'your-strong-password',
        ]);
    }
}
```

You don't strictly need the no-args call — the `MigrationsEnded` auto-install already handles fresh installs. The array form is the recommended pattern when a project wants stable, repo-tracked superadmin credentials across all of its environments.

For raw create/update with explicit credentials, use `SuperAdmin::install($password, $email, $name)`.

Integration patterns
--------------------

[](#integration-patterns)

### User model trait (optional)

[](#user-model-trait-optional)

Adds `isSuperAdmin()` plus two query scopes:

```
use Codenzia\SuperAdmin\Concerns\IsSuperAdmin;

class User extends Authenticatable
{
    use IsSuperAdmin;
}
```

```
$user->isSuperAdmin();                       // bool
User::query()->superAdmin()->first();        // WHERE is_protected = true
User::query()->exceptSuperAdmin()->get();    // WHERE NOT is_protected
```

### UserPolicy

[](#userpolicy)

The observer throws — your policy should return a proper 403 first:

```
use Codenzia\SuperAdmin\Facades\SuperAdmin;
use Illuminate\Auth\Access\Response;

class UserPolicy
{
    public function delete(User $actor, User $target): Response
    {
        if (SuperAdmin::is($target)) {
            return Response::deny('The super admin account cannot be deleted.');
        }

        return $actor->can('delete_user') ? Response::allow() : Response::deny();
    }

    public function update(User $actor, User $target): Response
    {
        if (SuperAdmin::is($target) && ! SuperAdmin::is($actor)) {
            return Response::deny('Only the super admin can modify the super admin account.');
        }

        return $actor->can('update_user') ? Response::allow() : Response::deny();
    }
}
```

### Filament

[](#filament)

```
use Codenzia\SuperAdmin\Filament\SuperAdminPlugin;

$panel->plugin(SuperAdminPlugin::make());
```

The plugin registers three defense-in-depth UX layers on the protected user row, all toggleable via `config/superadmin.php` and active by default:

1. **`DeleteAction` / `ForceDeleteAction` auto-hide** — original behavior. Admins never see a button that would only error at the observer layer.
2. **Custom destructive row actions auto-hide.** Any `Filament\Actions\Action` whose `getName()` is in `filament.hidden_action_names` is hidden on the protected user. The default list catches the verbs we ship across our consumer apps: `delete`, `forceDelete`, `suspend`, `unsuspend`, `ban`, `unban`, `markEmailVerified`, `verify`, `unverify`, `impersonate`, `demote`.
3. **Privileged form fields auto-disable.** Any `Filament\Forms\Components\Field` whose `getName()` is in `filament.locked_field_names` is disabled when the form's record is the super admin. Default list: `roles`, `role`, `permissions`, `status`, `is_protected`, `email`, `user_type`. Closes the "admin demotes the super admin via the roles Select" loophole.

Apps extend the defaults via config, no code:

```
// config/superadmin.php
'filament' => [
    'hidden_action_names' => [
        ...config('superadmin.filament.hidden_action_names'),
        'my_app_specific_destructive_action',
    ],
    'locked_field_names' => [
        ...config('superadmin.filament.locked_field_names'),
        'my_app_specific_privileged_field',
    ],
],
```

> **Caveat.** Filament's `->hidden()` and `->disabled()` setters *replace* prior conditions (they don't AND/OR). If app code chains an explicit `->hidden(false)` *after* construction, the package's auto-hide is overridden. Apps that rely on `->visible(fn () => ...)` for conditional showing (the common pattern) are unaffected because `visible` and `hidden` are separate fields and an action is hidden when *either* hides it.

To also hide the protected row from non-super-admin viewers:

```
public static function getEloquentQuery(): Builder
{
    $query = parent::getEloquentQuery();

    if (! auth()->user()?->isSuperAdmin()) {
        $query->exceptSuperAdmin();
    }

    return $query;
}
```

### Authorization modes

[](#authorization-modes)

Mode`authorization.gate_before`Behavior**Default** (zero-config)`true``Gate::before` authorizes the super admin for every ability. Role is also assigned (best-effort, if `assignRole()` exists on the User model).**Role-only**`false`Package only assigns the configured role. Authorization is delegated to your project (typically Filament Shield's own `Gate::before`).The package never creates the role row, defines permissions, or installs Shield — those remain your project's responsibility. In default mode, you don't need any of them: `Gate::before` covers authorization on its own.

What's new since 0.3.0
----------------------

[](#whats-new-since-030)

**0.3.2 (2026-05-22).** Adds **late role assignment** for the `MigrationsEnded`-vs-Spatie-Role-row race, and **Filament auto-lock** for the protected user row: every consumer app now auto-hides destructive row actions and auto-disables privileged form fields with no per-resource code. New config keys: `late_role_assignment`, `filament.hidden_action_names`, `filament.locked_field_names`. Tests grew from 84 to 105.

**0.3.1 (2026-05-21).** **Security:** the observer now blocks `is_protected: false → true` promotion via Eloquent update (mass-assignment privilege escalation defense). Previously only the downgrade direction was guarded. Also cleans up three stale `protection.block_*` config reads that were documented as removed in 0.3.0 but never deleted from the observer code.

See [CHANGELOG.md](CHANGELOG.md) for the full release notes.

Upgrading from 0.3.x to 0.4.0
-----------------------------

[](#upgrading-from-03x-to-040)

v0.4.0 moves identity (name / email / password / role) entirely out of `.env` and config. Per-app upgrade:

1. `composer update codenzia/laravel-superadmin`
2. Move any per-app overrides from `.env` into your seeder: ```
    // database/seeders/UserSeeder.php
    SuperAdmin::ensure([
        'email'    => 'admin@your-app.test',     // was: SUPER_ADMIN_EMAIL
        'password' => 'your-strong-password',    // was: SUPER_ADMIN_PASSWORD
    ]);
    ```
3. Delete `SUPER_ADMIN_PASSWORD`, `SUPER_ADMIN_EMAIL`, `SUPER_ADMIN_ROLE`, `SUPER_ADMIN_NAME` from every `.env` and `.env.example`. These env vars are no longer honored — leaving them set is harmless but stale.
4. If you publish the package config: delete the `email`, `password`, `role` keys from `config/superadmin.php`. They're no longer read.
5. Update any callers of `php artisan superadmin:setup` to `php artisan superadmin:ensure`. The old command name was removed.
6. If you use Filament Shield: nothing to do — `configuredRole()` now auto-discovers `filament-shield.super_admin.name`.

### Removed in 0.4.0

[](#removed-in-040)

RemovedReplacement`SUPER_ADMIN_PASSWORD` env varSeeder override: `SuperAdmin::ensure(['password' => '...'])``SUPER_ADMIN_EMAIL` env varSeeder override: `SuperAdmin::ensure(['email' => '...'])``SUPER_ADMIN_ROLE` env varAuto-discovered from `filament-shield.super_admin.name``config('superadmin.email' / '.password' / '.role')`Same — moved into seeder or auto-discovered`superadmin:setup` command`superadmin:ensure` (interactive prompts, but DB-only — no `.env` writes)`EnvWriter` helperRemoved entirely — the package never writes to `.env` nowUpgrading from 0.2.x
--------------------

[](#upgrading-from-02x)

v0.3.0 was a **clean break**. The vendor-friction model is gone. Per-app upgrade:

1. `composer update codenzia/laravel-superadmin`
2. `php artisan migrate` — auto-installs the protected user if none exists; no-op if one does.
3. Replace any seeder calls to `SuperAdmin::install(...)` with `SuperAdmin::ensure()` (or keep `install()` if you need explicit credentials).
4. Delete `.env` entries that are no longer recognized (see table below).

### Removed in 0.3.0

[](#removed-in-030)

RemovedReplacement`superadmin:install``superadmin:ensure` (or just run `migrate` for the default install)`superadmin:reset``superadmin:ensure``superadmin:assign-role`(automatic on `install()` / `ensure()`)`superadmin:doctor``superadmin:status --verbose``--confirm` flag, typed phrase, `VendorCommandInvoked` notificationRemoved entirely. No friction layer.`SUPER_ADMIN_NOTIFY_MAIL` / `SUPER_ADMIN_NOTIFY_SLACK` / `SUPER_ADMIN_VENDOR_PHRASE`Removed entirely.`vendor_commands.*` configRemoved entirely.`notifications.*` configRemoved entirely.`protection.block_delete` / `block_email_change` / `block_flag_change`Collapsed into `protection.enabled` — all three behaviors fire together.### Kept

[](#kept)

- `is_protected` column + Eloquent observer
- `Gate::before` authorization
- Filament destructive-action hiding
- `IsSuperAdmin` trait + query scopes
- `SuperAdmin` facade — `is()`, `user()`, `exists()`, `install()`, `email()`, `userModel()`, `isConfigured()`, `assignRole()`, `hasConfiguredRole()`, `withoutProtection()`
- Facade methods: `ensure(?array)`, `defaultEmail()`, `defaultPassword()`, `defaultName()`

Testing
-------

[](#testing)

**105 Pest tests, 173 assertions.** Covers the manager, the observer (delete + email + unprotect + promote-escalation), Gate::before, the `MigrationsEnded` auto-install hook, the late-role-assignment listener, the setup command, the env writer, and the Filament plugin (DeleteAction / ForceDeleteAction hiding, custom-named-action auto-hide, locked form-field auto-disable, master-switch kill, app-extended allowlists).

```
composer test
```

License
-------

[](#license)

MIT © Codenzia. See [LICENSE.md](LICENSE.md).

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance97

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

9

Last Release

16d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/10361843?v=4)[Mohammad El-Haj](/maintainers/mh2x)[@mh2x](https://github.com/mh2x)

---

Top Contributors

[![mh2x](https://avatars.githubusercontent.com/u/10361843?v=4)](https://github.com/mh2x "mh2x (15 commits)")

---

Tags

laravelsecurityauthfilamentrecoverycodenziasuperadminbreak-glass

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/codenzia-laravel-superadmin/health.svg)

```
[![Health](https://phpackages.com/badges/codenzia-laravel-superadmin/health.svg)](https://phpackages.com/packages/codenzia-laravel-superadmin)
```

###  Alternatives

[spatie/laravel-health

Monitor the health of a Laravel application

88011.3M149](/packages/spatie-laravel-health)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.4k51.0M7.4k](/packages/larastan-larastan)[laravel/ai

The official AI SDK for Laravel.

9782.1M153](/packages/laravel-ai)[psalm/plugin-laravel

Psalm plugin for Laravel

3325.1M337](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k28.4M134](/packages/laravel-cashier)[calebdw/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

15104.9k4](/packages/calebdw-larastan)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
