Search by

mrjthedifferent / laravel-foundation

mrjthedifferent

The shared base of a Laravel admin application as one updatable package: sign-in, users, roles, settings, notifications, activity logs, backups and a Tailwind CSS admin UI.

Package info

github.com/mrjthedifferent/laravel-foundation

pkg:composer/mrjthedifferent/laravel-foundation

Statistics

Installs: 264

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v3.5.1 2026-10-09 23:24 UTC

README

Laravel Foundation

tests MIT license

Laravel Foundation

The part of an admin application you would otherwise rebuild, or copy, for every project: sign-in, users, roles and permissions, settings, notifications, activity logs, backups, import and export jobs, and a Tailwind CSS admin UI.

It ships as one Composer package. A project requires it and never copies its files, so a fix made here reaches every project with composer update. That is the point: copied starter kits drift apart; a package does not.

  • Laravel 13, PHP 8.3+, built on nwidart/laravel-modules
  • Server-rendered Blade, Tailwind CSS 4 (compiled into the package's stylesheet), no build step required for the admin theme
  • Light and dark mode, RTL, 8 accent palettes (or your own brand colour), collapsible sidebar
  • Everything a project needs to change is overridable without forking

What is inside

Module Provides
User Sign-in by email or phone, password reset by email, profile, user management, bulk upload, documents, login history, impersonation, API session endpoints
RolePermission Roles and permissions (spatie/laravel-permission) with a management UI
Settings Database-backed settings, mail and SMS gateways, social sign-in keys, theme, and the privacy policy and terms, served publicly at /privacy-policy and /terms-conditions
Notification In-app, email, SMS and push (FCM) notifications with per-channel switches
ActivityLog Audit trail (owen-it/laravel-auditing), request, email and SMS logs, log viewer
BackupCleanup Database and file backups, scheduled clean-up
ImportDownloadManager Queued import and export jobs with a download centre
Otp (optional) One-time codes by email or SMS
ErrorReport (optional) Captures exceptions and notifies you

Plus the core: helpers, a JSON response factory, an exception handler, middleware, a library of Blade components (page header, filter bar, tables, forms with a file drop zone, modals, charts, empty states…), layouts, error pages and base migrations.

Quick start

The fastest way is the project skeleton, which is a fresh Laravel app already wired up:

git clone https://github.com/mrjthedifferent/laravel-foundation-skeleton my-project
cd my-project
composer install
composer run setup
php artisan foundation:super-admin
composer run dev

composer run setup creates .env, migrates and seeds the database, and builds the assets. The default database is SQLite; set DB_* in .env for MySQL or PostgreSQL first. Sign in at /login with the Super Admin you just created.

Install in a Laravel 13 app

laravel new my-project && cd my-project

composer config allow-plugins.wikimedia/composer-merge-plugin true
composer require mrjthedifferent/laravel-foundation
php artisan foundation:install
php artisan migrate --seed
php artisan foundation:super-admin
npm install && npm run build

foundation:super-admin asks for a name, email and password and creates the first Super Admin; sign in with it at /login.

Super Admin is not a role. It is a flag on the user that passes every permission check, so roles and permissions can be edited freely without anyone locking themselves out. Only the console grants or removes it: php artisan foundation:super-admin someone@example.com promotes an existing user, --revoke removes the flag, --list shows who has it. In the admin panel no one can deactivate, delete, reset or impersonate a Super Admin, and ordinary admins cannot edit one. Seeders never create one.

foundation:install does the rest of the wiring. Run it with --dry-run first to see the list:

  • App\Models\User extends the foundation user; the user factory creates active users
  • bootstrap/app.php gets the foundation's middleware, exception handling and handler
  • DatabaseSeeder calls the foundation's seeder; / redirects to the dashboard
  • Laravel's stock users, cache and jobs migrations are removed (it asks first; the foundation ships its own)
  • the Composer scripts that keep the theme and shared files current, the Vite @foundation alias, and the four npm packages the admin UI needs
  • the module scanner points at the package, and the core modules are enabled

It only replaces a file that is still exactly as Laravel generated it. A file you have changed is left alone, and the command prints what to add by hand. Running it again changes nothing.

The dashboard route (admin.dashboard) and the sidebar's parent groups come from the package. To change the groups, php artisan vendor:publish --tag=foundation-sidebar; to use your own dashboard page, create resources/views/dashboard.blade.php.

The dashboard is a grid of widgets: headline stats with sparklines, a trend chart (7, 14, 30 or 90 days, optionally against the period before), quick actions, system health, a recent-activity feed and charts, then one card per module. Each viewer can press Customize to drag, resize, hide and reorder the cards; the arrangement is saved per user and survives modules being enabled or disabled. A module joins in without touching the page: a StatComposer in $dashboardStats adds a headline stat, a ChartComposer in $dashboardCharts supplies the trend (lowest priority wins), a DashboardWidget in $dashboardWidgets adds a card, a QuickActionComposer in $dashboardActions adds shortcuts, a HealthCheck in $dashboardHealth adds a line to system health, and a partials/dashboard-widget.blade.php view still adds a card as before. Each is permission-gated and cached the same way, and contributes nothing when the viewer may not see it.

Your own modules

php artisan foundation:make-module Invoice --group=administration
composer dump-autoload && php artisan migrate && php artisan db:seed

This creates Modules/Invoice with a model, migration, factory, policy, controller, routes, an index page, a sidebar entry, permissions and a feature test. A module's provider extends Mrj\Foundation\Support\ModuleServiceProvider and declares what it contributes:

class InvoiceServiceProvider extends ModuleServiceProvider
{
    protected string $name = 'Invoice';
    protected string $nameLower = 'invoice';

    protected array $morphMap = ['invoice' => Invoice::class];
    protected array $policies = [Invoice::class => InvoicePolicy::class];
}

Also available: $composers, $commands, $middlewareAliases, $prependToGroups and $appendToGroups. Nothing about a module is registered in bootstrap/app.php or AppServiceProvider.

One database per tenant (opt-in)

For a multi-tenant app on a tenancy library such as stancl/tenancy, the foundation can run the same modules in the central app and inside every tenant. It is off by default, and while off nothing behaves differently. The package never depends on a tenancy library: the project connects its library through config and one contract.

  • Each module declares where it runs in module.json: "context": "universal" (the default: central and every tenant, as all foundation modules are), "central" or "tenant". foundation:make-module Invoice --context=tenant writes it.
  • foundation.tenancy in config/foundation.php turns it on and names the library's middleware per context, the central domains, and the events after which the tenant changed.
  • Bind Mrj\Foundation\Contracts\TenancyContext to a small adapter over the library.
  • Set 'auto-discover' => ['migrations' => false] in config/modules.php, and point the library's tenant migrator at app(MigrationPaths::class)->for(ModuleContext::Tenant).

Routes, migrations, the sidebar, the dashboard, permission seeding, settings (including the mailer), the settings cache and the permission cache then follow the current tenant. The full guide is .ai/guidelines/foundation/tenancy.md after foundation:sync.

Security settings

Settings → Security holds the security policy, so an administrator can change it without a deploy:

  • Two-factor authentication (off by default). Once it is on, users can turn on TOTP two-factor authentication from their profile. They scan a QR code in any authenticator app, confirm a code, and receive recovery codes. Sign-in (password or social) then asks for a code, and the API's login takes two_factor_code or recovery_code. Each code works once. The page also chooses who must set it up before they can use the panel (any roles, and super admins) and the issuer name. For another rule, override requiresTwoFactor(): bool on App\Models\User. An administrator who may reset passwords can also reset a user's two-factor authentication from the user's page.
  • Passwords: minimum length; whether upper- and lower-case letters, numbers or symbols are required; and whether breached passwords are rejected. The rule is Laravel's Password::defaults(), used by every password form. A project that calls Password::defaults() in its own provider overrides it.
  • Sign-in and sessions: failed attempts before a login is locked out, panel session lifetime, and how long an unused mobile/API token lasts.

Until the page is first saved, foundation.two_factor.*, foundation.passwords.*, foundation.login.* and session.lifetime (config and .env) decide. The API token lifetime is already a setting, 30 days by default. After that, the page does.

Maintenance mode (Settings → General) closes the panel to everyone but super admins. Other signed-in users see the maintenance message and can sign out. The date formats and the default page size are under Settings → General too.

Phone sign-in for mobile apps

With the Otp module on (php artisan module:enable Otp), API clients can sign in with a phone number and a one-time SMS code instead of a password. Turn it on under Settings → OTP:

  • OTP login opens POST v1/auth/otp/request {phone} and POST v1/auth/otp/verify {phone, code, device_name?, name?}. The verify step returns {token, user, is_new_user}, a Sanctum token like POST v1/login.
  • OTP self-registration creates the account on first sign-in for a number that has none, with the role chosen in OTP registration role. When it is off, only existing users can sign in.

Phones may be typed in any form (01712345678, 8801712345678, +8801712345678) and are stored as E.164. The request step answers the same way whether or not the number has an account. Codes are stored only as a keyed hash, and SMS logs record them masked.

Sign-up with a password. With OTP self-registration on, a client can also register with a form:

  1. POST v1/auth/register/request {name, phone, email?, password, role?, terms?} sends a code to the phone. No account exists yet; the form waits in the cache until the code expires.
  2. POST v1/auth/register/verify {phone, code, device_name?} creates the account, with the phone already verified, and returns {token, user}.

The person then signs in with POST v1/login {id, password}. To let them pick a role, list the allowed role names in foundation.registration.roles. Set foundation.registration.require_terms to make accepting the terms mandatory.

Password reset by code. With OTP password reset on, POST v1/auth/password/forgot {phone} sends a code, and POST v1/auth/password/reset {phone, code, password, password_confirmation} sets the new password and signs out every device.

Offline sync for mobile apps

An app that works offline keeps a local copy of some tables and exchanges changes with the server. The foundation provides the server side.

  1. Make the model syncable. Its table uses a ULID key, which the client generates:

    Schema::create('farms', function (Blueprint $table) {
        $table->ulid('id')->primary();
        $table->string('name');
        $table->timestamps();
        $table->syncable(); // version, deleted_at, pull index
    });
    
    class Farm extends Model
    {
        use \Mrj\Foundation\Sync\Syncable;
    
        protected $fillable = ['name'];
    }
  2. Write a handler. It decides which rows a user sees, validates pushed data, and sets server-owned attributes. Writes are authorised by the model's policy (create, update, delete), and only $fillable attributes are taken from the client.

    final class FarmSyncHandler extends \Mrj\Foundation\Sync\ModelSyncHandler
    {
        protected function model(): string { return Farm::class; }
    
        protected function scope(Builder $query, Authenticatable $user): void
        {
            $query->whereIn('id', $user->farms()->select('farms.id'));
        }
    
        protected function rules(Authenticatable $user, ?Model $existing): array
        {
            return ['name' => [$existing ? 'sometimes' : 'required', 'string', 'max:100']];
        }
    }
  3. Register it in config/foundation.php: 'offline_sync' => ['handlers' => ['farms' => FarmSyncHandler::class]]. The endpoints exist only once a handler is registered.

Pull: GET api/v1/sync/pull?cursor=&limit=&only=farms,batches returns {changes: {farms: [...]}, tombstones: {farms: [ids]}, cursor, has_more, server_time}. Store the cursor and send it back. While has_more is true, pull again. Rows are returned only once they were last changed at least settle_seconds ago, so a slow transaction cannot slip behind the cursor.

Push: POST api/v1/sync/push with an Idempotency-Key header and {ops: [{name, op: "upsert"|"delete", id, version?, data}]}. Each operation is applied in its own transaction, and each gets a result:

  • applied returns the new version.
  • conflict means the server row changed since the client's version. The result includes the current row as server, or null if it was deleted.
  • rejected returns errors (validation or permission). Don't retry it unchanged.
  • error means the server failed to save it, for example because the database was unavailable. Nothing changed; keep the change and retry it later.

Idempotent requests

The idempotent middleware makes a write safe to retry. The client sends an Idempotency-Key header, a ULID or UUID per operation. The first response is stored, and a retry with the same key gets that response again, marked Idempotent-Replayed: true. The same key with a different request is refused with 422, and a key still in progress answers 409. Server errors are not stored. Use idempotent:required to reject requests that have no key. Schedule php artisan model:prune to remove expired keys.

Customising without forking

To change Do this
A layout, component or error page Create the same path under resources/views, e.g. resources/views/components/page-header.blade.php
A module's view Create resources/views/modules/{alias}/{same path}
A module's menu, permissions or settings php artisan vendor:publish --tag={alias}-module-config, then edit config/{alias}/
Which modules are on modules_statuses.json, or php artisan module:enable Otp
Who may sign in Override accessDenialMessage(): ?string on App\Models\User
Who must use two-factor authentication Settings → Security, or override requiresTwoFactor(): bool on App\Models\User
Where phone numbers live Users have a phone column, stored in E.164 form, and can sign in with it. To keep phones in your own table instead, override scopeWherePhone($query, ?string $phone) on App\Models\User
Colours, dark mode, RTL, sidebar style Settings → Theme, or the settings seeder
Let administrators change a config value Declare the setting with 'config' => 'the.config.key' in a module's config/settings.php (see .ai/guidelines/foundation/permissions-settings.md)
PDF fonts Put a .ttf in resources/fonts and list it under pdf.fonts in config/foundation.php
Files foundation:sync overwrites List them under sync.except in config/foundation.php
Any text, or the language php artisan vendor:publish --tag=foundation-lang, or a copy of a module's file at resources/lang/modules/{alias}/{locale}/{alias}.php; menu, permission and setting names in lang/{locale}.json
The admin URL prefix, domain or middleware routing in config/foundation.php (route names stay admin.*)

Registration is closed by default: an administrator creates accounts.

Documentation for people and AI assistants

foundation:install and foundation:sync copy the guidelines into the project at .ai/guidelines/foundation/, where coding assistants (Claude Code, Laravel Boost, Cursor…) read them. They are the reference for building on the package without re-inventing it:

File Covers
foundation-overview.md Start here: the rules, where to look, the commands
components-reference.md Every Blade component, with props and slots
ui-components.md UI conventions: page structure, tables, forms, buttons, badges, cards, icons
views.md Page templates: index, create, edit, show
frontend-js.md data-fd-* behaviour, events, the Foundation API, confirmations, toasts, Select2
dashboard.md Adding stats, charts, widgets, shortcuts and health checks to the dashboard
theming.md Tokens, theme settings, which utilities exist, dark mode and RTL
module-creation.md, module-architecture.md, models-enums.md, patterns.md, permissions-settings.md, testing.md, tenancy.md Back-end conventions

Commands

Command Purpose
foundation:install Wire a Laravel app to the foundation (--dry-run to preview; safe to run again)
foundation:super-admin {login?} Create a Super Admin, or promote an existing user (--revoke, --list)
foundation:make-module {Name} Create a module on the foundation conventions (--group, --context)
foundation:publish Copy theme assets to public/assets (skips when current; --force, --link)
foundation:sync Update shared tooling files: AI coding guidelines, pint.json, .scripts/laravel.sh (--check for CI)

Updating

composer update mrjthedifferent/laravel-foundation --with-dependencies
php artisan migrate
php artisan foundation:publish --force   # the compiled theme assets
php artisan foundation:sync              # the guidelines and shared tooling

--with-dependencies lets Composer update or add the packages a new release needs. Read the CHANGELOG before updating across several versions; its Upgrading sections list anything a project must do.

Public API and versioning

The package follows semantic versioning. A breaking change to any of these waits for the next major version:

  • classes and interfaces marked @api: Foundation, the base classes a project extends (Models\User, Support\ModuleServiceProvider, WidgetComposer, StatComposer, ChartComposer, DashboardWidget, QuickActionComposer, HealthCheck, QueryBuilder, ExportJob, Http\Controllers\Controller, Exceptions\Handler), every interface in Contracts, JsonResponseFactory, Roles, Email, PhoneNumber, FileManagerService, Tenancy, MigrationPaths, Enums\ModuleContext, Events\TenancyContextChanged, the validation rules and HasImageAttribute
  • the global helper functions, config keys, route names (admin.*, api.*), Blade component tags, translation keys, permission names and the database schema
  • the Artisan commands and their options

A module's public surface is its routes, views (overridable by path), config, permissions, translation keys and events; its actions, controllers, jobs and other classes are internal. Anything marked @internal may change in a minor release. Classes not meant to be extended are final.

Contributing

See CONTRIBUTING.md. Security issues: SECURITY.md.

Credits and license

Laravel Foundation is open-source software by Md. Mahfuzur Rahman, released under the MIT license. Bundled front-end libraries keep their own licenses: see THIRD-PARTY-NOTICES.md. Laravel is a trademark of Taylor Otwell; this project is not affiliated with Laravel.