Every fresh Laravel install needs an admin account to log into and basic site settings so it isn't blank on first load — doing this manually every deploy is exactly what seeders exist to eliminate. This guide covers seeders vs factories properly, then builds a real example: a default admin seeded from .env credentials using firstOrCreate so it's safe to re-run, plus a flexible settings table for site name, logo, timezone, favicon, and meta tags.
Every Laravel project needs some data to exist the moment it's installed — an admin account to log in with, and basic site settings so the app isn't blank on first load. Doing this manually through phpMyAdmin every time you deploy a fresh copy is exactly the kind of repetitive task seeders exist to eliminate. This post covers seeders and factories properly, then applies both to a real setup: a default admin user with known login credentials, and a site settings table pre-filled with sensible defaults.
Seeders vs Factories — What Each One Is For
They're often confused because they're used together, but they solve different problems:
- Factories define how to generate fake/realistic data for a model — useful for testing and for filling a database with dummy records (100 fake blog posts, 50 fake users)
- Seeders define what data should exist and when to run it — used for both real, fixed data (a default admin account, site settings, categories) and for calling factories to generate bulk fake data
Rule of thumb: if the data is something specific your app needs to function (an admin account, default settings, fixed categories), that's seeder territory — often without a factory at all. If it's bulk realistic-looking data for testing/demo purposes, that's factory territory, called from a seeder.
Part 1: Default Admin User Seeder
The Migration (if not already covered by Laravel's default users table)
Laravel's default users table already has what's needed (name, email, password). If you want a role column to distinguish admin from regular users:
Schema::table('users', function (Blueprint $table) {
$table->string('role')->default('user')->after('email');
});
The Seeder
php artisan make:seeder AdminUserSeeder
// database/seeders/AdminUserSeeder.php
use Illuminate\Support\Facades\Hash;
use App\Models\User;
class AdminUserSeeder extends Seeder
{
public function run(): void
{
User::firstOrCreate(
['email' => env('ADMIN_EMAIL', 'admin@example.com')],
[
'name' => 'Administrator',
'password' => Hash::make(env('ADMIN_PASSWORD', 'ChangeMe123!')),
'role' => 'admin',
'email_verified_at' => now(),
]
);
}
}
Why firstOrCreate instead of create: running seeders more than once (which happens constantly during development, or on redeploys) with plain create() would throw a duplicate-email error or create multiple admin accounts. firstOrCreate checks for an existing record by email first, and only creates it if missing — safe to run repeatedly.
Why credentials come from .env instead of being hardcoded: hardcoding admin@example.com / password123 directly in a seeder means that exact password ships in your Git history forever, on every environment. Pulling from .env means each environment (local, staging, production) can have different credentials, and the real password never touches version control.
Add to .env (and .env.example with placeholder values, so teammates know these exist):
ADMIN_EMAIL=admin@yourdomain.com
ADMIN_PASSWORD=Set-A-Strong-Password-Here
Part 2: Site Settings Seeder
The Migration
A flexible key-value settings table handles most "basic site settings" needs without a rigid single-row table that needs a new column every time you add a setting:
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->timestamps();
});
The Seeder
php artisan make:seeder SiteSettingsSeeder
// database/seeders/SiteSettingsSeeder.php
use App\Models\Setting;
class SiteSettingsSeeder extends Seeder
{
public function run(): void
{
$defaults = [
'site_name' => 'Your Site Name',
'site_title' => 'Your Site Name — Tagline Here',
'site_logo' => 'images/logo.png',
'site_favicon' => 'images/favicon.ico',
'timezone' => 'Asia/Dhaka',
'meta_title' => 'Your Site Name | Home',
'meta_description' => 'A short, accurate description of what the site offers.',
'contact_email' => 'contact@yourdomain.com',
];
foreach ($defaults as $key => $value) {
Setting::firstOrCreate(['key' => $key], ['value' => $value]);
}
}
}
Using firstOrCreate here matters just as much — if you re-run seeders after a client has already changed the site name through an admin panel, this won't overwrite their change back to the default.
A Simple Helper to Read Settings Anywhere
// app/Helpers/helpers.php
if (! function_exists('setting')) {
function setting(string $key, $default = null)
{
static $settings;
if (! $settings) {
$settings = \App\Models\Setting::pluck('value', 'key');
}
return $settings[$key] ?? $default;
}
}
Usage in a Blade layout:
<title>{{ setting('meta_title', config('app.name')) }}</title>
<meta name="description" content="{{ setting('meta_description') }}">
<link rel="icon" href="{{ asset(setting('site_favicon', 'favicon.ico')) }}">
<img src="{{ asset(setting('site_logo')) }}" alt="{{ setting('site_name') }}">
Part 3: Registering Seeders in the Right Order
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
SiteSettingsSeeder::class,
AdminUserSeeder::class,
CategorySeeder::class,
// Factories for demo/testing data go last
SongFactorySeeder::class,
]);
}
}
Order matters when data depends on other data — for example, if songs need categories to exist first via a foreign key, CategorySeeder must run before anything that creates songs.
Part 4: Factories for Demo/Test Data
Factories come in once you need realistic bulk data — useful for local development and automated tests, not for things like the one fixed admin account above.
php artisan make:factory SongFactory --model=Song
// database/factories/SongFactory.php
class SongFactory extends Factory
{
public function definition(): array
{
return [
'singer_id' => Singer::factory(),
'category_id' => Category::inRandomOrder()->first()?->id ?? Category::factory(),
'title' => $this->faker->sentence(3),
'duration_seconds' => $this->faker->numberBetween(120, 300),
'file_path' => 'songs/' . $this->faker->uuid() . '.mp3',
'release_date' => $this->faker->dateTimeBetween('-5 years', 'now'),
'play_count' => $this->faker->numberBetween(0, 500000),
];
}
}
A seeder that uses it, for local/staging environments only:
// database/seeders/SongFactorySeeder.php
class SongFactorySeeder extends Seeder
{
public function run(): void
{
if (app()->environment('production')) {
return; // never generate fake data in production
}
Song::factory()->count(50)->create();
}
}
That environment check matters — it's easy to forget a factory seeder is still registered in DatabaseSeeder and accidentally run php artisan db:seed in production, filling real customer-facing tables with fake songs.
Part 5: Running It All
Fresh install, migrate and seed together:
php artisan migrate --seed
Or separately, if migrations already ran:
php artisan db:seed
Run just one seeder (useful when you only need to refresh settings, for example):
php artisan db:seed --class=SiteSettingsSeeder
What You Get After Running This
- A working admin login immediately after deployment — credentials pulled from
.env, never hardcoded or committed - Site settings (name, logo, timezone, favicon, meta title/description) populated with sensible defaults instead of a blank/broken-looking first load
- Safe to re-run at any point without duplicating the admin account or overwriting settings someone already customized
- A clear separation between fixed app data (seeders) and bulk fake data for development (factories), with factories guarded against ever running in production
Quick Checklist
- ✓ Admin credentials come from
.env, never hardcoded in the seeder file - ✓
firstOrCreateused for anything that shouldn't be duplicated on re-run - ✓ Settings stored in a flexible key-value table, not a rigid fixed-column table
- ✓ Seeder call order respects foreign key dependencies (categories before songs, etc.)
- ✓ Factory-based seeders explicitly blocked from running in production
- ✓
.env.exampleupdated with placeholder keys so teammates know which env variables are expected
This setup means any teammate — or you, six months from now, spinning up a new environment — runs one command and gets a fully functional, logged-in-ready application instead of a blank database and a guessing game about default credentials.