mapo-89 / core-panel
A Laravel admin starter kit built with Inertia, Vue, Tailwind CSS, Fortify, Passport, Horizon, and Wayfinder.
Requires
- php: ^8.5
- darkaonline/l5-swagger: ^11.0
- erag/laravel-pwa: ^2.1
- inertiajs/inertia-laravel: ^3.0
- laravel/fortify: ^1.0
- laravel/framework: ^13.0
- laravel/horizon: ^5.0
- laravel/passport: ^13.7
- laravel/socialite: ^5.0
- laravel/wayfinder: ^0.1
- socialiteproviders/microsoft: ^4.9
- spatie/laravel-activitylog: ^5.0
- spatie/laravel-medialibrary: ^11.22
- spatie/laravel-package-tools: ^1.92
- spatie/laravel-permission: ^7.3
- spatie/laravel-query-builder: ^7.2
- spatie/laravel-translatable: ^6.14
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/boost: ^2.4
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
mapo-89/core-panel is a Laravel 13 admin package and scaffold built around Inertia v3, Vue 3, PrimeVue, Fortify, Passport, Socialite, Horizon, and Wayfinder.
Read-only split repository: this package repository is automatically synchronized from
mapo-89/core-panel-monorepo. Do not open pull requests or make direct changes here. All development happens in the monorepo.
The package is split into:
mapo-89/core-panel- optional
mapo-89/core-panel-tenancy
The core package stays tenancy-neutral. Tenant-aware behavior lives in the tenancy addon.
Stack
- PHP 8.5
- Laravel 13
- Inertia v3
- Vue 3
- Tailwind CSS v4
- PrimeVue
- Fortify
- Passport
- Socialite
- Horizon
- PostgreSQL or MySQL
- Redis
- Wayfinder
Install
Existing Laravel app:
composer require mapo-89/core-panel php artisan core-panel:install
Fresh Laravel app:
composer create-project laravel/laravel core-panel-app
cd core-panel-app
composer require mapo-89/core-panel
php artisan core-panel:install
New installations do not require any additional vendor-first migration:
core-panel:installalready configures the host so CorePanel frontend building blocks are loaded fromvendorby defaultresources/css/app.cssalready contains the required Tailwind@sourceentries forvendor/mapo-89/core-panel/resources/js- only publish
componentsorthemeif the host application really needs to own and customize those files locally
CorePanel also registers the short alias:
php artisan core:install
Generic OpenID Connect login
CorePanel ships a provider-neutral oidc Socialite driver. It uses OpenID Connect Discovery and has been validated against authentik. The same provider appears in the login page and the profile connection list; the existing account-linking and optional master-provider flow apply unchanged.
Configure the OIDC application at the identity provider with this redirect URI (replace the domain with the public application URL):
https://panel.example.com/auth/oidc/callback
Then configure the host .env:
SOCIAL_OIDC_ENABLED=true OIDC_ISSUER=https://authentik.example.com/application/o/core-panel/ OIDC_CLIENT_ID=your-client-id OIDC_CLIENT_SECRET=your-client-secret # Optional; defaults to /auth/oidc/callback and is normalized to APP_URL at runtime. OIDC_REDIRECT_URI=/auth/oidc/callback
OIDC_ISSUER must be the exact issuer advertised by the discovery document at /.well-known/openid-configuration. For authentik this is normally the application's OpenID Connect issuer URL, not just the base authentik URL.
The default claim mapping is compatible with standard OIDC UserInfo responses:
OIDC_CLAIM_ID=sub OIDC_CLAIM_EMAIL=email OIDC_CLAIM_NAME=name OIDC_CLAIM_NICKNAME=preferred_username OIDC_CLAIM_AVATAR=picture
Change these names only when the provider exposes different claims. The ID claim must be stable and unique for the issuer. An email is required for automatic user matching and creation; if email delivery is unreliable, do not select OIDC as the master provider.
The same configuration is available at runtime in Settings → Authentication. Enable “OpenID Connect login” after entering issuer and client credentials. To make OIDC authoritative for email updates during account linking, select oidc as the master social provider; otherwise leave the master-provider setting empty.
Timestamp Conversion
If an existing PostgreSQL installation still contains legacy timestamp without time zone columns from before the timestampTz() switch, CorePanel ships a one-time conversion command:
php artisan core-panel:convert-timestamps-tz --dry-run php artisan core-panel:convert-timestamps-tz --force
The command interprets legacy values in the configured source timezone and converts them directly to timestamptz instants without depending on the PostgreSQL session timezone.
Default source timezone:
- legacy timezone:
Europe/Berlin
Override them in the host application if needed:
// config/core-panel.php 'database' => [ 'timestamp_tz_conversion' => [ 'legacy_timezone' => env('CORE_PANEL_TIMESTAMP_LEGACY_TIMEZONE', 'Europe/Berlin'), ], ],
Add Project-Specific Tables
CorePanel only knows its own package tables by default. Host applications can explicitly extend the conversion lists for project-specific tables in config/core-panel.php:
// config/core-panel.php 'database' => [ 'timestamp_tz_conversion' => [ 'datasets' => [ 'central' => [ 'projects' => ['created_at', 'updated_at', 'deleted_at'], 'appointments' => ['scheduled_for', 'cancelled_at', 'created_at', 'updated_at'], ], ], ], ],
Available datasets:
centralfor the main application databasetenancyfor tenancy metadata tables when the addon is installedtenantfor tenant application databases when the addon is installed
Use the same structure for each dataset: table name => list of timestamp columns to convert.
PWA
CorePanel can now scaffold the host application for Progressive Web App support via erag/laravel-pwa.
What CorePanel Sets Up
For new installs, core-panel:install scaffolds the PWA host files automatically.
For existing installs, update the host scaffolds once:
php artisan core-panel:update --force
This brings the following host files into place when they are missing or managed by the CorePanel scaffold manifest:
bootstrap/providers.phpconfig/pwa.phppublic/manifest.jsonpublic/offline.htmlpublic/sw.jspublic/logo.png
CorePanel also renders the package Inertia root view with:
@PwaHeadinside<head>@RegisterServiceWorkerScriptbefore</body>
What You Should Adjust In The Host App
After installation or update, review these host-specific values:
- set the correct public app name and URL in
.env, especiallyAPP_NAMEandAPP_URL - review
config/pwa.phpand adjustname,short_name,description,theme_color, andbackground_color - replace
public/logo.pngwith the real app icon in at least512x512 - if the install prompt should not be shown globally, set
'install-button' => falseinconfig/pwa.php
What You Should Verify
- PWA features require HTTPS in real environments; service workers will not work correctly without it
- if you use
config:cache, rebuild the cache after changingconfig/pwa.php - after changing
config/pwa.php, regenerate the browser-facing manifest withphp artisan erag:update-manifestso updated names, colors, icons, and prompts reachpublic/manifest.json - make sure the deployed
public/directory containsmanifest.json,sw.js,offline.html, and the finallogo.png - if the host app had its own custom
bootstrap/providers.php, merge theEragLaravelPwa\EragLaravelPwaServiceProvider::classentry intentionally instead of overwriting unrelated providers
Typical rollout after enabling PWA support in an existing app:
composer update mapo-89/core-panel php artisan core-panel:update --force php artisan erag:update-manifest php artisan optimize:clear npm run build
Optional Host Customization
The scaffold gives you a working baseline, but most applications should still make a few deliberate host decisions:
- replace the default offline page in
public/offline.htmlwith project-specific branding and support text - expand
public/manifest.jsonicons or screenshots if the target platforms require more than the default single icon - rerun
php artisan erag:update-manifestwheneverconfig/pwa.phpor the referenced icon assets change - if the host application already has its own PWA strategy or service worker, consolidate that logic instead of keeping two competing implementations
Upgrade To The Unified Docker Application Image
This one-time upgrade is required when an existing installation still uses separate PHP-FPM and Nginx images. Afterwards, app, horizon, and scheduler run from the same application image. The former nginx service is removed, while PostgreSQL, Redis, and system-updater remain separate services.
1. Prepare A Maintenance Window And Backups
Plan for a short interruption while the containers are replaced. Back up at least the database, persistent storage directory, .env, and the Compose files currently in use. Example for PostgreSQL:
docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ exec -T postgres sh -lc 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \ > core-panel-before-upgrade.sql
Record the currently deployed image tags or digests as well so that a rollback remains possible.
2. Update The Package And Managed Scaffolds
Update CorePanel and then publish the new Docker and Compose structure:
composer update mapo-89/core-panel php artisan core-panel:update --force --no-interaction
With the tenancy addon installed:
composer update mapo-89/core-panel mapo-89/core-panel-tenancy php artisan core-panel:update --force --with-addon-updates --no-interaction
core-panel:update stores backups of replaced managed files in .core-panel-backups/. Review the diff afterwards and deliberately merge any project-specific customizations into the new files. Unmanaged host files are not overwritten without an existing scaffold baseline.
3. Migrate The Image Variables
Replace the previous PHP_IMAGE and NGINX_IMAGE variables in .env, Portainer, or the relevant deployment configuration with one unified application image:
APP_IMAGE=registry.example.com/core-panel/app:1.6.0 UPDATER_IMAGE=registry.example.com/core-panel/system-updater:1.6.0 SYSTEM_UPDATER_RUNTIME_SERVICES=app,horizon,scheduler
Remove obsolete values for PHP_IMAGE, NGINX_IMAGE, and PHP_UPSTREAM. UPDATER_IMAGE remains separate because the system updater is not part of the application image.
For a registry deployment, the updater must use both Compose files:
SYSTEM_UPDATER_COMPOSE_FILES=docker-compose.prod.yml,docker-compose.registry.yml
Portainer can continue to use automatic detection for SYSTEM_UPDATER_COMPOSE_FILES. Make sure that APP_IMAGE and UPDATER_IMAGE are configured as stack variables.
4. Render The New Compose Configuration
The configuration must render successfully and must no longer contain an nginx service:
docker compose --env-file .env \
-f docker-compose.prod.yml \
-f docker-compose.registry.yml \
config > /dev/null
docker compose --env-file .env \
-f docker-compose.prod.yml \
-f docker-compose.registry.yml \
config --services
The service list must contain app, horizon, scheduler, and system-updater, but no separate nginx service. app, horizon, and scheduler must all resolve to the same APP_IMAGE value.
5. Pull The Images And Replace The Stack
Pull the new images first without changing the running containers:
docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ pull app horizon scheduler system-updater
Start the updated stack afterwards. --remove-orphans removes the old Nginx container, which is no longer defined:
docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ up -d --remove-orphans
Only app receives RUN_MIGRATIONS=true by default. Horizon and Scheduler therefore do not run migrations in parallel.
For Portainer, redeploy the updated stack using docker-compose.portainer.yml instead. The reverse proxy must then target port 8080 on the app service directly; a target named nginx no longer exists.
6. Verify The Upgrade
Run the following checks immediately after the replacement:
docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ ps curl --fail --show-error http://127.0.0.1:8000/healthcheck docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ exec app php artisan migrate:status docker compose --env-file .env \ -f docker-compose.prod.yml \ -f docker-compose.registry.yml \ exec app php artisan horizon:status
Also verify a normal Laravel page, an asset under /build/assets/, uploads or the storage link, and the external reverse proxy. app, horizon, and scheduler must be healthy and use the same image or digest.
7. Future Image Upgrades
For later releases, update APP_IMAGE and UPDATER_IMAGE to the new tags or digests, then repeat the render, pull, start, and verification steps. Separate PHP and Nginx tags are no longer required.
For a rollback, restore the backed-up Compose and .env files together with the previously recorded image tags, then start the previous Compose configuration again. Restore the database only if the upgrade ran migrations that are not backward compatible.
Update
CorePanel is designed vendor-first where Laravel supports it:
- package config is loaded by default and only needs publishing when the host app wants to override it
- translations and Blade views are loaded from the package first and can be overridden through the normal Laravel vendor paths when needed
core-panel:updatekeeps frontend overlays vendor-first by default and only refreshes host scaffolds plus explicit opt-in overrides
Upgrading To 1.6.0 (Breaking)
Version 1.6.0 changes ownership boundaries between the package and host application and introduces an explicit breaking migration path.
CorePanel now owns the default implementations for:
- presence middleware and CorePanel Inertia shared props
- OpenAPI declarations and package scan paths
- backup, system-update, and Horizon schedules
- Fortify actions, Fortify registration, and Horizon authorization
- reusable user-model behavior while the concrete
App\Models\Userremains host-owned - generated Wayfinder routes and package domain migrations
Host applications continue to own composition and deployment concerns such as bootstrap/app.php, the concrete user model, host routes and Inertia props, frontend entrypoints, environment files, database configuration, and Docker or Compose files. Package behavior can still be replaced through the documented configuration, contracts, middleware bridge, gates, and host props.
Back up the application and database before the one-time upgrade, then run:
composer update mapo-89/core-panel php artisan core-panel:update --force --breaking-changes npm install php artisan wayfinder:generate --no-interaction npm run build php artisan optimize:clear
If the Tenancy addon is installed, update both packages and their managed scaffolds in one run:
composer update mapo-89/core-panel mapo-89/core-panel-tenancy php artisan core-panel:update --force --breaking-changes --with-addon-updates npm install php artisan wayfinder:generate --no-interaction npm run build php artisan optimize:clear
CorePanel domain migrations are loaded from vendor/mapo-89/core-panel/database/migrations; central Tenancy migrations come from the addon package. The host retains its users, cache, jobs, and application-specific migrations. Existing migration ledger entries remain valid because basenames are unchanged. The updater backs up and removes only recognized unchanged or managed baselines. Customized migrations and obsolete scaffolds are preserved as conflicts for manual reconciliation, and a preserved tenant migration wins over a package migration with the same basename.
Review .core-panel-backups/ and the update output before deployment. Verify login and registration, password and profile flows, Socialite, Horizon, schedules, Swagger generation, the frontend build, and tenant creation where applicable. The --breaking-changes option is only needed for the 1.6.0 ownership transition; normal later updates continue to use the standard commands below.
What To Watch For In Existing Installations
If the application previously published CorePanel frontend directories such as resources/js/components, resources/js/layouts, resources/js/composables, resources/js/plugins, resources/js/support, resources/js/types, resources/js/assets, or resources/js/theme/core-panel, you have two options:
- if you want to keep the local overrides, leave them in place and do not force the migration
- if you want to move back to vendor-first wherever possible, run
php artisan core-panel:updateonce
The default frontend migration is intentionally conservative:
- unchanged published CorePanel frontend files are removed from the host and resolved directly from
vendoragain - locally modified published files stay in place
- with
--force, even locally modified published files are removed after a backup so the vendor files take over again - rebuild the frontend afterwards, at minimum with
npm run buildornpm run dev
What To Watch For In Future Updates
Once an application has been migrated to vendor-first, the normal update flow becomes:
- update the package through Composer
- run
php artisan core-panel:update --force - rebuild the frontend
If you later publish components or theme again, the next core-panel:update will try to migrate those overlays back to vendor-first automatically.
Refresh published CorePanel assets after upgrading the package:
composer update mapo-89/core-panel php artisan core-panel:update --force
If you want to migrate previously published CorePanel frontend overlays back to vendor assets, run:
php artisan core-panel:update
Use --force only when you intentionally want to remove local overlay changes after creating a backup.
If you also have optional addons installed:
composer update mapo-89/core-panel mapo-89/core-panel-tenancy php artisan core-panel:update --force --with-addon-updates
For normal in-place updates, the command also runs outstanding migrations automatically after refreshing the published assets.
If you use --base-path to target a different application directory, migrations are skipped and must be run manually in that target application.
The update flow also synchronizes .env from .env.example in a template-first way: the .env.example structure is reused, existing values for known keys are preserved, new keys are added, and keys no longer present in .env.example are removed. Before an existing .env is rewritten, its previous contents are copied to .env.backup.
If your application owns the frontend version metadata itself, set "managed_by_application": true in config/app-version.json. In that case, core-panel:update will leave that file untouched, including --force updates.
If you want to run the environment synchronization on its own:
php artisan core-panel:env:sync
Use this when you want to refresh .env against the current .env.example without running the full package update.
If a .env already exists, the command writes its previous contents to .env.backup before applying the synchronized result.
Pass --replace-template-values only if existing values for template-managed keys should also be replaced by the current template defaults.
Typical update runbook for an existing installation:
composer update mapo-89/core-panel php artisan core-panel:update --force npm install npm run build php artisan optimize:clear
If the tenancy addon is installed, prefer:
composer update mapo-89/core-panel mapo-89/core-panel-tenancy php artisan core-panel:update --force --with-addon-updates
If generated assets such as resources/js/actions, resources/js/routes, resources/js/wayfinder, public/build, or public/hot were previously committed, remove them from the Git index once after adopting the new .gitignore:
git rm -r --cached -- resources/js/actions resources/js/routes resources/js/wayfinder public/build public/hot
The installer now asks for:
APP_URL- database driver:
pgsqlormysql - database host / port / name / user / password
- test database name
- default locale
- fallback locale
- whether an initial admin user should be created
- whether migrations and seeders should run
- whether frontend dependencies should be installed and built
- whether the tenancy addon should be installed
If tenancy is enabled, it also asks for:
- central domain
The default central domain is derived from the host part of APP_URL.
Defaults:
- API auth:
passport - light mode by default
- PrimeVue theme always included
- Horizon always enabled
- social login disabled until configured
Local Package Development
For local development with a path repository:
composer create-project laravel/laravel core-panel-app cd core-panel-app composer config repositories.core-panel '{"type":"path","url":"/home/manue/projects/packages/core-panel/packages/core-panel","options":{"symlink":true,"versions":{"mapo-89/core-panel":"dev-main"}}}' composer require mapo-89/core-panel:dev-main php artisan core-panel:install
If you are developing from the monorepo and want the addon too, register both path repositories:
composer config repositories.core-panel '{"type":"path","url":"/home/manue/projects/packages/core-panel/packages/core-panel","options":{"symlink":true,"versions":{"mapo-89/core-panel":"dev-main"}}}' composer config repositories.core-panel-tenancy '{"type":"path","url":"/home/manue/projects/packages/core-panel/packages/core-panel-tenancy","options":{"symlink":true,"versions":{"mapo-89/core-panel-tenancy":"dev-main"}}}'
If tenancy is enabled during install and the addon exists as a sibling package, the installer can add the local addon dependency automatically.
Non-interactive example:
php artisan core-panel:install \
--no-interaction \
--app-url=https://core-panel-app.test \
--db-connection=pgsql \
--db-host=127.0.0.1 \
--db-port=5432 \
--db-database=core_panel \
--db-username=core_panel \
--db-password=core_panel \
--db-database-test=core_panel_test \
--default-locale=de \
--fallback-locale=en \
--create-admin=true \
--admin-name="Admin User" \
--admin-email=admin@example.test \
--admin-password=secret \
--run-migrations=true \
--run-seeders=true \
--install-frontend=false \
--install-tenancy=true \
--central-domain=core-panel-app.test \
--sync-environment=true
If you keep the default PostgreSQL installer values, the PostgreSQL user core_panel with password core_panel must already exist before running the install command.
Example:
psql postgres CREATE ROLE core_panel WITH LOGIN PASSWORD 'core_panel' CREATEDB; CREATE DATABASE core_panel OWNER core_panel; CREATE DATABASE core_panel_test OWNER core_panel; \q
Publish Commands
Publish only the parts the host application really needs to own:
config: optional local overrides forconfig/core-panel.phpandconfig/core-panel-access.phplang: optional overrides inlang/vendor/core-panelviews: optional Blade overrides inresources/views/vendor/core-panelcomponentsandtheme: mutable frontend overlays when the host app needs to customize shipped UI building blocksstubs: internal generator stubs for advanced customization
Normal package usage does not require publishing lang or views, because both are resolved vendor-first by Laravel.
Published components and theme overrides can be migrated back to package assets later with php artisan core-panel:update.
Bei Neuinstallationen solltest du components und theme nach Möglichkeit gar nicht publishen. Solange der Host keine lokalen Änderungen an diesen Bausteinen braucht, ist vendor-first der vorgesehene Standard.
php artisan core-panel:publish --tag=config php artisan core-panel:publish --tag=lang php artisan core-panel:publish --tag=components php artisan core-panel:publish --tag=theme php artisan core-panel:publish --tag=stubs php artisan core-panel:publish --tag=views
License
CorePanel is released under the MIT license.