lauroguedes / laravel-demo-mode
Turn a Laravel installation into a public playground: seeded data, scheduled resets, rotating credentials, and a belt of restrictions that keeps a stranger from abusing the server.
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/process: ^13.0
- illuminate/support: ^13.0
- symfony/process: ^7.4|^8.0
Requires (Dev)
- larastan/larastan: ^3.12
- laravel/pint: ^1.32
- orchestra/testbench: ^11.2
- pestphp/pest: ^5.2
- pestphp/pest-plugin-laravel: ^5.0
- pestphp/pest-plugin-type-coverage: ^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- rector/rector: ^2.6
- spatie/laravel-db-snapshots: ^2.10
Suggests
- spatie/laravel-db-snapshots: Required for the snapshot reset strategy (^2.0)
Provides
None
Conflicts
None
Replaces
None
README
Turn a Laravel installation into a public playground: seeded data, a scheduled reset, rotating credentials published on the login page, and a belt of restrictions that keeps a stranger from abusing the server.
Publishing a starter kit, a boilerplate or a SaaS with a browsable demo needs the same five pieces every time — a flag, a rebuild on a cycle, credentials a stranger can use that do not become a permanent fact of the internet, a list of things the demo must not do, and a visible notice that the data is temporary. This is those five pieces, extracted from two demos that have been running in public.
This package drops tables. Read docs/security.md before you install it anywhere. A wrong configuration erases your database.
Installation
composer require lauroguedes/laravel-demo-mode php artisan demo:install
demo:install is additive: it publishes the config, appends the DEMO_ keys to
.env.example, and offers to write a DemoSeeder stub. It does not touch .env
and it does not turn the demo on — that is an act you perform on the deployment
you meant.
It asks two questions, because neither answer can be inferred from the project:
whether every visitor sees the same data or each gets
their own corner of it, and whether to start a DemoSeeder or
point the reset at a seeder you already have. Answer shared if you are not sure.
--sandbox=shared|scoped and --without-seeder answer both for a script, and a
run with nobody at the keyboard takes shared and writes the seeder.
Making an installation a demo
DEMO_MODE=true DEMO_RESET_SCHEDULE="0 */6 * * *"
// config/demo.php 'environments' => ['demo'], 'allowed_hosts' => ['demo.example.com'],
Write the demonstration data into database/seeders/DemoSeeder.php, then check
your work:
php artisan demo:doctor
It exits non-zero on anything that would destroy data or publish a secret, so it belongs in your deploy pipeline ahead of the first reset.
Using it
use LauroGuedes\DemoMode\Facades\Demo; Demo::enabled(); // the single point of truth Demo::nextResetAt(); // derived from the cron, so a countdown cannot lie Demo::credentials(); // what the login form should prefill Demo::toArray(); // one payload for Blade, Livewire and Inertia
{{-- Both render nothing when this is not a demo, so no wrapper is needed --}} <x-demo-banner /> {{-- a floating bar the package styles itself --}} <x-demo-credentials /> @notdemo <a href="{{ route('oauth.google') }}">Sign in with Google</a> @endnotdemo
For Inertia, return Demo::toArray() from your own
HandleInertiaRequests::share(). For Blade and Livewire, the optional
ShareDemoState middleware puts the same payload in every view. See
docs/frontend.md.
Letting visitors reset it
'on_demand' => ['enabled' => true],
Off by default — it puts a migrate:fresh behind an HTTP request. Throttled,
cooled down, queued, CSRF-protected and host-checked; see
docs/on-demand-reset.md.
Isolating visitors from each other
'sandbox' => ['driver' => 'scoped'],
Each visitor gets the seeded baseline plus what they created. Not multi-tenancy and not a security boundary; see docs/sandbox.md.
Commands
| Command | What it does |
|---|---|
demo:install |
Publish the config, and a seeder stub if you want one |
demo:reset |
Rebuild the demonstration data. --dry-run prints the plan |
demo:status |
What this installation currently is |
demo:doctor |
Audit the configuration. Non-zero exit on anything dangerous |
demo:credentials |
Show, or --rotate, the published passwords |
demo:snapshot |
Capture the baseline the snapshot strategy restores |
demo:sandbox:prune |
Remove the sandboxes nobody came back to |
Working with an AI agent
If the project uses Laravel Boost, this
package ships its own guidelines and an agent skill. php artisan boost:install
picks them up with no configuration.
The guidelines are the short, always-loaded part: that this package drops tables,
that Demo::enabled() is the single source of truth, that the view components
already decide for themselves whether to render, and that a seeder must read the
published password from Demo::passwordFor() rather than hardcoding one — which
is the mistake that fails silently after the first rotation.
The demo-mode-development skill carries the rest, loaded only when it is
relevant: reset strategies, per-visitor isolation, cleaners, restrictions, write
guards and the on-demand reset.
What it is not
- Password-protecting a work in progress. That is
php artisan down --secret. - Backup and restore. That is
spatie/laravel-backup. - A demo data generator. The seeder is yours; the package runs it.
- Multi-tenancy. Visitor isolation is deliberately ephemeral and disposable.
Documentation
Full documentation is in docs/. Start with
security.md — it is the one that is not optional.
Already have a hand-rolled demo mode? UPGRADE.md is mostly a list of things to delete.
Testing
composer test
Credits
Extracted from lauroguedes/laravel-sso and lauroguedes/mary-ui-starter-kit, which had each solved this badly in their own way first.
License
The MIT License. See LICENSE.md.
If this is useful to you, please star the repository.
It is the thing that helps other people find it.
⭐ Star this project · ☕ Buy me a coffee
Crafted by an Artisan ♥ Lauro Guedes