mrjthedifferent / laravel-foundation
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
Requires
- php: ^8.3
- bacon/bacon-qr-code: ^3.1
- google/apiclient: ^2.19
- guzzlehttp/guzzle: ^7.8|^8.0
- laravel-notification-channels/telegram: ^8.0
- laravel/framework: ^13.0
- laravel/sanctum: ^4.0
- laravel/slack-notification-channel: ^3.10
- laravel/socialite: ^5.23
- mpdf/mpdf: ^8.2
- nwidart/laravel-modules: ^13.0
- opcodesio/log-viewer: ^3.12
- owen-it/laravel-auditing: ^14.0
- pragmarx/google2fa: ^9.1
- rap2hpoutre/fast-excel: ^5.5
- spatie/laravel-backup: ^10.3
- spatie/laravel-data: ^4.18
- spatie/laravel-permission: ^8.0
Requires (Dev)
- driftingly/rector-laravel: ^2.6
- larastan/larastan: ^3.12
- laravel/pint: ^1.13
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.5
- rector/rector: ^2.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- v3.5.1
- v3.5.0
- v3.4.0
- v3.3.0
- v3.2.0
- v3.1.0
- v3.0.1
- v3.0.0
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.0
- dev-main / 1.x-dev
- v1.13.3
- v1.13.2
- v1.13.1
- v1.13.0
- v1.12.0
- v1.11.0
- v1.10.0
- v1.9.1
- v1.9.0
- v1.8.0
- v1.7.1
- v1.7.0
- v1.6.0
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.0
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.1
- v1.2.0
- v1.1.0
- v1.0.1
- v1.0.0
- dev-fix/navbar-notification-open
- dev-feat/push-notifications
- dev-feat/account-deletion-requests
- dev-feat/account-deletion-page
- dev-feat/public-legal-pages
- dev-feat/sidebar-url-active
- dev-fix/asset-cache-busting
- dev-feat/latest-bundled-libraries
- dev-fix/ship-bootstrap-converter
- dev-fix/minify-js-and-setting-image
- dev-fix/post-2.0-hardening
- dev-design/tailwind-dashboard-redesign
- dev-release-1.13.3
- dev-release-1.13.2
- dev-update-deps
- dev-release-1.13.1
- dev-claude/awesome-wozniak-p0az7r
This package is auto-updated.
Last update: 2026-10-09 23:26:58 UTC
README
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\Userextends the foundation user; the user factory creates active usersbootstrap/app.phpgets the foundation's middleware, exception handling and handlerDatabaseSeedercalls 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
@foundationalias, 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=tenantwrites it. foundation.tenancyinconfig/foundation.phpturns 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\TenancyContextto a small adapter over the library. - Set
'auto-discover' => ['migrations' => false]inconfig/modules.php, and point the library's tenant migrator atapp(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
logintakestwo_factor_codeorrecovery_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, overriderequiresTwoFactor(): boolonApp\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 callsPassword::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}andPOST v1/auth/otp/verify {phone, code, device_name?, name?}. The verify step returns{token, user, is_new_user}, a Sanctum token likePOST 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:
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.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.
-
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']; }
-
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$fillableattributes 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']]; } }
-
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:
appliedreturns the newversion.conflictmeans the server row changed since the client'sversion. The result includes the current row asserver, ornullif it was deleted.rejectedreturnserrors(validation or permission). Don't retry it unchanged.errormeans 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 inContracts,JsonResponseFactory,Roles,Email,PhoneNumber,FileManagerService,Tenancy,MigrationPaths,Enums\ModuleContext,Events\TenancyContextChanged, the validation rules andHasImageAttribute - 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.