Appearance
Administration
The administration panel ships as laraspring/admin, a Composer package your application installs. It answers the three questions every SaaS asks itself within a week of launching: who are my users, why can this one not sign in, and what is the customer on the phone actually looking at.
It is a panel inside your product rather than a second application: ordinary Inertia pages, in the same layout as everything else, behind one ability.
What you get out of the box
| Feature | Where |
|---|---|
| One ability that opens the panel | Laraspring\Admin\Support\Admin::ACCESS |
| The first administrator | php artisan laraspring:admin you@example.com |
| Users, paged and searchable in the database | admin.users |
| One user, with everything your application knows | admin.users.show |
| Suspending an account, with a reason and an optional end | admin.users.ban |
| Signing in as a user, safely | admin.impersonate.start |
| An audit trail of every action taken | admin_actions |
| Counters that never call a payment provider | admin.overview |
| Sections, counters and badges your application adds | AdminResourceRegistry |
Making the first administrator
There is no way to promote somebody from inside the panel, because a panel that could do that would need somebody already in it. The first one is made from a shell on the server:
bash
php artisan laraspring:admin ada@example.com
php artisan laraspring:admin ada@example.com --revokeThat flips is_admin on the user's row. The column belongs to your application, not to the package, and the reference edition ships the migration that adds it. is_admin is deliberately absent from the model's $fillable: a flag a form can set is a flag a registration form can grant.
Deciding who is an administrator yourself
Nothing in the package reads the column. Everything asks the gate, so one definition in your application moves every check at once, the middleware on the panel, the entry in the user menu, and the rule that says an administrator may never be impersonated:
php
use Illuminate\Support\Facades\Gate;
use Laraspring\Admin\Support\Admin;
Gate::define(Admin::ACCESS, function (User $user): bool {
return $user->hasRole('support') || str_ends_with($user->email, '@yourcompany.com');
});Your definition wins over the package's, because application providers boot after discovered ones. It is the same arrangement laraspring/billing uses for manageBilling.
An installation whose users table has no is_admin column at all answers "nobody is an administrator" rather than throwing, so the window between installing the package and running your migrations is a closed door rather than a broken application.
What the panel knows about your product
Nothing, deliberately, and that is the design rather than a limitation. A panel is about the whole application by definition, and laraspring/admin depends on none of it: no laraspring/auth, no laraspring/organizations, no laraspring/billing. Naming them would put three optional packages in the panel's composer.json and make an installation without them fail to boot instead of showing a shorter screen.
So the panel holds a registry and your application fills it, the same shape laraspring/mail, laraspring/storage and laraspring/i18n use. Four kinds of thing, because the panel has four holes in it:
php
use Laraspring\Admin\Panel\{AdminResourceRegistry, AdminSection, AdminStat, AdminUserBadge, AdminUserPanel};
public static function register(AdminResourceRegistry $registry): void
{
// A screen of your own in the panel's navigation. Guard its routes with
// the panel's middleware alias, `laraspring.admin`.
$registry->section(new AdminSection(
key: 'invoices',
titleKey: 'app.admin.nav.invoices',
routeName: 'admin.invoices',
order: 40,
));
// A counter on the overview. Local queries only.
$registry->stat(new AdminStat(
key: 'invoices',
labelKey: 'app.admin.stats.invoices',
value: fn (): int => Invoice::query()->count(),
));
// A label next to a person's name.
$registry->userBadge(new AdminUserBadge(
key: 'two_factor',
labelKey: 'app.admin.badges.two_factor',
applies: fn (Model $user): bool => TwoFactor::isRequiredFor($user),
tone: 'positive',
));
// A block of facts on their detail screen.
$registry->userPanel(new AdminUserPanel(
key: 'invoices',
titleKey: 'app.admin.panels.invoices',
rows: fn (Model $user): array => [/* label, value, url */],
));
}The reference edition does exactly this in app/Admin/LaraspringAdminPanel.php, called from AppServiceProvider::boot(). That is where the organizations section, the per-plan counters and the two-factor badge come from: every one of them is a fact belonging to a package the panel may not name.
A section whose route is not registered is filtered out when the navigation is drawn, so a screen behind a feature flag disappears rather than leading to a 404.
The overview never calls Stripe
Every counter is a query against your own database, and that is a rule rather than an implementation detail. The overview is the screen an administrator opens when something is wrong, and a counter that waits on a payment provider would make it the screen that is down whenever the provider is. A counter that throws renders as a dash; the rest of the page still draws.
The reference edition counts active subscriptions per plan by reading the local subscriptions table by price identifier, not by asking the provider.
How long the audit trail is kept
admin_actions is written to on every ban, every lift and both ends of every impersonation, and nothing in the package ever edits a row: a trail an application can rewrite proves nothing. It does grow forever, though, so age is the one thing allowed to remove a line.
dotenv
LARASPRING_ADMIN_AUDIT_RETENTION=365php
// routes/console.php
Schedule::command('laraspring:prune-audit')->daily();There is deliberately no default. Without a configured period the command refuses rather than guessing, because guessing wrong here deletes the answer to "who signed in as that customer, and when". Check what your own retention policy and your jurisdiction require before picking a number.
--days overrides the configured period for one run, and --pretend reports what would go without deleting anything:
bash
php artisan laraspring:prune-audit --days=365 --pretendDeletion happens in chunks, because the first run on an installation that has never pruned is the one big enough to hold a lock worth worrying about.
What is next
- Suspending accounts — what a ban does, and to which doors.
- Impersonation — the rules that make it safe to ship.