DEV Community

Kamran Khan
Kamran Khan

Posted on

Seeders that make your starter kit demoable in 30 seconds

The difference between a starter kit that sells and one that gets refunded is often the first five minutes. If a buyer runs migrate --seed, logs in, and sees a populated dashboard immediately, they trust the code. If they land on an empty screen and have to hand-create users before they can evaluate anything, they start wondering what else is unfinished. The seeder is the demo. Treat it like one.

I keep a small, opinionated seeder in every starter project. Here's the full thing:

public function run(): void
{
    User::firstOrCreate(
        ['email' => 'admin@demo.io'],
        [
            'name'     => 'Admin',
            'password' => 'password',
            'role'     => 'admin',
        ]
    );

    // Top the demo users up to 9 total (1 admin + 8 users).
    $missing = 9 - User::count();
    if ($missing > 0) {
        User::factory($missing)->create();
    }
}
Enter fullscreen mode Exit fullscreen mode

There are four ideas packed into those fifteen lines. Each one exists because I got burned by the alternative.

1. firstOrCreate for the demo admin

The demo login — admin@demo.io / password — is created with firstOrCreate, keyed on the email. Running the seeder twice does not create two admins. This sounds obvious, but the number of projects I've seen where re-running the seeders throws a duplicate-email exception (or worse, silently doubles the demo data) is embarrassing.

And yes, the password is passed as plain text: 'password' => 'password'. That works because the User model casts the password attribute to hashed:

protected function casts(): array
{
    return [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Laravel hashes it on assignment. If your model doesn't have the hashed cast, this seeder stores a plain-text password and your demo login breaks in a confusing way. It's worth knowing which of your conventions a seeder silently depends on — I once spent an hour debugging a "login doesn't work" report that was just a missing cast in a refactored model.

2. Top-up logic, not fixed counts

$missing = 9 - User::count() means the seeder tops the database up to 9 users instead of blindly creating 9 more. The seeder is idempotent: safe to run on every deploy, every restart, every CI run. Existing records are left untouched; only missing demo data is created.

Idempotency is the single most important property of a seeder. A seeder you can only run once is a landmine — the first time someone runs it in staging to debug something, the data doubles. A seeder that's safe to run always becomes part of the deployment routine, which means the demo environment is always one command away from healthy.

3. The factory caches the password hash

protected static ?string $password;

public function definition(): array
{
    return [
        'name'              => fake()->name(),
        'email'             => fake()->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password'          => static::$password ??= Hash::make('password'),
        'remember_token'    => Str::random(10),
        'role'              => 'user',
    ];
}
Enter fullscreen mode Exit fullscreen mode

static::$password ??= Hash::make('password') hashes 'password' once and reuses the hash for every factory-created user. Bcrypt is deliberately slow — hashing per user makes seeding 100 users take noticeably longer than it should. This is the standard Laravel factory pattern, but I've seen custom seeders that call Hash::make in a loop and then complain that seeding is slow. Cache the hash.

Also note the factory defaults every user to role => 'user'. The only admin in the system is the explicit demo admin from the seeder. Roles should be assigned deliberately, never by random factory output — a fake()->randomElement(['admin', 'user']) in a factory is how you end up with mystery admins in a demo database.

4. Demo credentials are documented, not hidden

The login is admin@demo.io / password, and it says so in the README. Some developers treat demo credentials like secrets. They're not — they're the front door of the demo. If an evaluator can't find them in thirty seconds, they'll evaluate a competitor's product instead. (For production, this seeder gets replaced or gated behind an environment check. Demo convenience and production security are different jobs.)

The 30-second demo

The full flow for a new buyer: clone, composer install, copy .env.example, php artisan migrate --seed, log in as admin@demo.io. What they see: a dashboard with real stat cards (total users: 9, admins: 1, fresh "new this week" numbers), a populated users table with pagination working, and an admin panel they can immediately click through. No hand-created data, no empty states, no imagination required.

That's what sells a starter kit — not the feature list, but the feeling of "this already works". The seeder is fifteen lines. Write it like it matters, because for the first five minutes, it's the whole product.

This seeder, the factory, and the admin@demo.io demo flow ship in my Laravel + React SaaS Starter Kit: https://kamranofficial.gumroad.com/l/mhekoig ($19, one-time). The pattern is stack-agnostic, though — idempotent seeders with a documented demo login will improve any boilerplate you maintain.

Top comments (0)