Skip to content

Instantly share code, notes, and snippets.

@bhaidar
Created March 5, 2026 12:24
Show Gist options
  • Select an option

  • Save bhaidar/fab36112e28d4353875ee8d71a2c6f45 to your computer and use it in GitHub Desktop.

Select an option

Save bhaidar/fab36112e28d4353875ee8d71a2c6f45 to your computer and use it in GitHub Desktop.
Scaffold a new Laravel 12 project with Vue 3, Inertia.js, TypeScript, Sail, and common packages
description arguments
Scaffold a new Laravel 12 project with Vue 3, Inertia.js, TypeScript, Sail, and common packages
name description
project_name
Name of the new Laravel project (e.g. MyApp)
name description
parent_dir
Parent directory where the project folder will be created (e.g. ~/projects)

Create a new Laravel 12 project named $ARGUMENTS.project_name inside $ARGUMENTS.parent_dir.

Progress Feedback (IMPORTANT)

You MUST output clear progress messages throughout execution. Before each step, print a header. Before each sub-action, print what you're doing. After each sub-action, confirm completion. Use this exact format:

==> [Step X/16] <icon> Step Title
    -> Installing package-name...
    βœ… Installed.
    -> Publishing config...
    βœ… Published.
    -> Running migrations...
    βœ… Migrated.

At the very start, print a summary banner:

╔══════════════════════════════════════════════════════════════╗
β•‘  πŸš€ New Laravel Project: $ARGUMENTS.project_name
β•‘  πŸ“ Directory: $ARGUMENTS.parent_dir/$ARGUMENTS.project_name
β•‘  πŸ› οΈ  Stack: Laravel 12 + Vue 3 + Inertia.js + TypeScript + Sail
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

At the very end, print a completion banner with the access URLs.

Stack Overview

Backend:

  • Laravel 12 with PHP 8.5 (Sail container)
  • Laravel Breeze (Vue 3 + TypeScript + Inertia.js, no SSR)
  • Laravel Sail (MySQL 8.4, Redis, Mailpit)

PHP Production Packages:

  • spatie/laravel-permission - role & permission management
  • spatie/laravel-medialibrary - file/image uploads tied to models

PHP Dev Packages:

  • laravel/telescope - debug dashboard (requests, queries, jobs, etc.)
  • barryvdh/laravel-debugbar - in-page debug toolbar
  • barryvdh/laravel-ide-helper - PHPStorm/IDE autocomplete generation
  • larastan/larastan - static analysis (PHPStan for Laravel)
  • laravel/pint - code style fixer (PSR-12 / Laravel preset)
  • pestphp/pest + pest-plugin-laravel - modern testing framework
  • laravel/pail - real-time log viewer

Frontend (NPM dev):

  • pinia - Vue state management
  • @vueuse/core - Vue composition utilities
  • @headlessui/vue - accessible unstyled UI components (Dialog, Menu, Listbox, etc.)
  • lucide-vue-next - icon library
  • vue-sonner - toast notifications
  • dayjs - lightweight date manipulation
  • vee-validate + @vee-validate/zod + zod - client-side schema validation (complements Inertia's server-side validation)
  • clsx + tailwind-merge - Tailwind class merging utilities
  • concurrently - parallel dev process runner

Steps

Step 1/16: πŸ“¦ Create Laravel project

cd "$ARGUMENTS.parent_dir"
composer create-project laravel/laravel "$ARGUMENTS.project_name"
cd "$ARGUMENTS.project_name"

Step 2/16: πŸ” Install & configure Breeze

composer require laravel/breeze --dev
php artisan breeze:install vue --typescript

Breeze installs: Inertia.js v2, Vue 3, Ziggy, Tailwind CSS, TypeScript, axios, and scaffolds auth pages (login, register, password reset, email verification, profile).

Do NOT install these separately - Breeze already provides them:

  • @inertiajs/vue3, vue, tailwindcss, axios, typescript, vue-tsc, ziggy

Step 3/16: 🐳 Install & configure Sail

composer require laravel/sail --dev
php artisan sail:install --with=mysql,redis,mailpit
php artisan sail:publish

This creates compose.yaml with MySQL 8.4, Redis, and Mailpit services, and publishes the Docker files to docker/.

Step 4/16: πŸ“š Install PHP production packages

Install each, then configure:

spatie/laravel-permission:

composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

This publishes config/permission.php and a migration for roles/permissions tables.

Then add HasRoles trait to app/Models/User.php:

  • Add import: use Spatie\Permission\Traits\HasRoles;
  • Update trait list: use HasFactory, HasRoles, Notifiable;

spatie/laravel-medialibrary:

composer require spatie/laravel-medialibrary
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"

This publishes config/media-library.php and a migration for the media table.

Step 5/16: πŸ” Install PHP dev packages (debugging)

laravel/telescope:

composer require laravel/telescope --dev
php artisan telescope:install

This publishes config/telescope.php, a migration, and the Telescope service provider.

Ensure Telescope only loads in local environment. In app/Providers/TelescopeServiceProvider.php, verify the gate method restricts access. In config/telescope.php, confirm 'enabled' => env('TELESCOPE_ENABLED', true).

barryvdh/laravel-debugbar:

composer require barryvdh/laravel-debugbar --dev

Auto-discovered, no publish needed. Only active when APP_DEBUG=true.

Step 6/16: ✨ Install PHP dev packages (code quality)

barryvdh/laravel-ide-helper:

composer require barryvdh/laravel-ide-helper --dev

Add IDE helper commands to composer.json scripts.post-update-cmd array (append after the existing laravel-assets line):

"@php artisan ide-helper:generate",
"@php artisan ide-helper:meta"

larastan/larastan:

composer require larastan/larastan --dev

Create phpstan.neon.dist at project root:

includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    paths:
        - app/

    level: 5

    ignoreErrors:

    excludePaths:

Add a scripts.analyse entry to composer.json:

"analyse": [
    "./vendor/bin/phpstan analyse --memory-limit=2G"
]

laravel/pint is already installed by default. Add a scripts.format entry to composer.json:

"format": [
    "./vendor/bin/pint"
]

Step 7/16: πŸ§ͺ Install PHP dev packages (testing)

pestphp/pest:

composer require pestphp/pest pestphp/pest-plugin-laravel --dev
./vendor/bin/pest --init

This creates tests/Pest.php with the base TestCase binding. Pest runs existing PHPUnit tests too, so Breeze's auth tests still work.

Update composer.json scripts.test to use Pest:

"test": [
    "@php artisan config:clear --ansi",
    "./vendor/bin/pest"
]

laravel/pail:

composer require laravel/pail --dev

No configuration needed. Used via php artisan pail.

Step 8/16: πŸ“¦ Install NPM packages

Run a single install command for all frontend packages:

npm install --save-dev pinia @vueuse/core @headlessui/vue lucide-vue-next vue-sonner dayjs vee-validate @vee-validate/zod zod clsx tailwind-merge concurrently

All packages are devDependencies since they're bundled by Vite.

Step 9/16: βš™οΈ Configure frontend packages

Pinia setup:

Create resources/js/plugins/pinia.ts:

import { createPinia } from 'pinia';

const pinia = createPinia();

export default pinia;

Update resources/js/app.ts to register Pinia. In the setup function, add .use(pinia) before .mount(el):

import pinia from './plugins/pinia';

// inside setup():
createApp({ render: () => h(App, props) })
    .use(plugin)
    .use(ZiggyVue)
    .use(pinia)
    .mount(el);

Tailwind class merge utility:

Create resources/js/utils/cn.ts:

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]): string {
    return twMerge(clsx(inputs));
}

Day.js setup:

Create resources/js/plugins/dayjs.ts:

import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';

dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);

export default dayjs;

Vue Sonner setup:

Add the <Toaster /> component to the main layout. In resources/js/Layouts/AuthenticatedLayout.vue (or your root layout), add:

<script setup>
import { Toaster } from 'vue-sonner';
// ... existing imports
</script>

<template>
    <!-- Add at the top of the template, before other content -->
    <Toaster position="top-right" :duration="3000" />
    <!-- ... rest of template -->
</template>

Also add <Toaster /> to resources/js/Layouts/GuestLayout.vue the same way.

Step 10/16: πŸ”§ Configure .env

Update .env with the following values. The Laravel app runs on port 8080 (APP_PORT).

APP_URL=http://localhost:8080

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=<snake_case of $ARGUMENTS.project_name>
DB_USERNAME=sail
DB_PASSWORD=password

SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
CACHE_STORE=redis

REDIS_CLIENT=phpredis
REDIS_HOST=redis

MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025

APP_PORT=8080
VITE_PORT=15173
FORWARD_DB_PORT=13306
FORWARD_REDIS_PORT=16379
FORWARD_MAILPIT_PORT=11025
FORWARD_MAILPIT_DASHBOARD_PORT=18025

TELESCOPE_ENABLED=true

VITE_APP_NAME="${APP_NAME}"

IMPORTANT: APP_PORT=8080 means the Laravel app is accessible at http://localhost:8080 (NOT port 80). APP_URL must match.

Copy .env to .env.example but remove the APP_KEY value (leave it empty).

Step 11/16: ⚑ Fix vite.config.js for Sail

The Vite dev server must bind to 0.0.0.0 inside the Docker container. Update vite.config.js:

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    server: {
        host: '0.0.0.0',
        port: 15173,
        hmr: {
            host: 'localhost',
        },
    },
    plugins: [
        laravel({
            input: 'resources/js/app.ts',
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});

CRITICAL: server.port must match VITE_PORT in .env and the port mapping in compose.yaml.

Step 12/16: πŸ“ Add composer scripts

In composer.json, add/replace these entries in the scripts section:

"scripts": {
    "setup": [
        "composer install",
        "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
        "@php artisan key:generate",
        "@php artisan migrate --force",
        "@php artisan db:seed --force",
        "npm install",
        "npm run build"
    ],
    "dev": [
        "Composer\\Config::disableProcessTimeout",
        "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
    ],
    "test": [
        "@php artisan config:clear --ansi",
        "./vendor/bin/pest"
    ],
    "format": [
        "./vendor/bin/pint"
    ],
    "analyse": [
        "./vendor/bin/phpstan analyse --memory-limit=2G"
    ],
    "post-update-cmd": [
        "@php artisan vendor:publish --tag=laravel-assets --ansi --force",
        "@php artisan ide-helper:generate",
        "@php artisan ide-helper:meta"
    ]
}

Keep all other existing script entries (post-autoload-dump, post-root-package-install, post-create-project-cmd, pre-package-uninstall) unchanged.

Step 13/16: πŸ™ˆ Add IDE helper files to .gitignore

Append to .gitignore:

_ide_helper.php
_ide_helper_models.php
.phpstorm.meta.php
.phpunit.cache

Step 14/16: πŸ‘€ Create admin user migration & seeder

Create migration to add is_admin column to users table:

php artisan make:migration add_is_admin_to_users_table --table=users

Edit the generated migration file:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->boolean('is_admin')->default(false)->after('email');
        });
    }
};

Create AdminUserSeeder:

php artisan make:seeder AdminUserSeeder

Edit database/seeders/AdminUserSeeder.php:

<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;

class AdminUserSeeder extends Seeder
{
    public function run(): void
    {
        $adminRole = Role::firstOrCreate(['name' => 'admin']);

        $admin = User::firstOrCreate(
            ['email' => 'admin@example.com'],
            [
                'name' => 'Admin',
                'password' => Hash::make('password'),
                'is_admin' => true,
                'email_verified_at' => now(),
            ]
        );

        $admin->assignRole($adminRole);
    }
}

Register in DatabaseSeeder:

Edit database/seeders/DatabaseSeeder.php to call the admin seeder:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            AdminUserSeeder::class,
        ]);
    }
}

Step 15/16: πŸ—οΈ Build and verify

sail up -d
sail npm install
sail artisan migrate
sail artisan db:seed
sail composer ide-helper:generate
sail composer ide-helper:meta
sail npm run build

Verify the app loads at http://localhost:8080. Verify Telescope at http://localhost:8080/telescope. Verify admin login works at http://localhost:8080/login with admin@example.com / password.

Step 16/16: πŸŽ‰ Initialize git

git init
git add -A
git commit -m "initial setup: Laravel 12, Vue 3, Inertia, Sail, Spatie, Telescope, Pest, Larastan"

Print completion banner:

╔══════════════════════════════════════════════════════════════╗
β•‘  βœ… Project "$ARGUMENTS.project_name" created successfully!
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

  🌐 Access URLs (after `sail up -d` and `sail npm run dev`):

  🏠 App:              http://localhost:8080
  ⚑ Vite HMR:         http://localhost:15173
  πŸ”­ Telescope:        http://localhost:8080/telescope
  πŸ“¬ Mailpit:          http://localhost:18025
  πŸ—„οΈ  MySQL:            localhost:13306
  πŸ”΄ Redis:            localhost:16379

  πŸ‘€ Admin credentials:
  πŸ“§ Email:            admin@example.com
  πŸ”‘ Password:         password

  πŸ› οΈ  Useful commands:
  sail up -d              Start all containers
  sail npm run dev        Start Vite dev server with HMR
  sail artisan migrate    Run migrations
  sail artisan db:seed    Seed the database
  sail composer test      Run Pest tests
  sail composer format    Fix code style with Pint
  sail composer analyse   Run Larastan static analysis
  sail artisan telescope  Manage Telescope
  sail artisan pail       Tail logs in real-time

  composer dev            Local dev (no Sail): server + queue + logs + vite

Package Reference

What Breeze already provides (do NOT install separately)

  • @inertiajs/vue3, inertiajs/inertia-laravel
  • vue, @vitejs/plugin-vue, vue-tsc, typescript
  • tailwindcss, @tailwindcss/forms, @tailwindcss/vite, postcss, autoprefixer
  • axios
  • tightenco/ziggy
  • laravel/sanctum
  • laravel/pint
  • Auth scaffolding (login, register, password reset, email verify, profile)

When to use what for forms

  • Inertia useForm: standard form submissions with server-side Laravel validation (FormRequest). This is the default for most forms.
  • vee-validate + zod: client-side validation for instant feedback before submission, complex multi-step forms, or forms that need schema-based validation rules on the frontend.

Notes

  • Always use sail prefix for artisan/npm/composer when working inside containers
  • The composer dev script is for NON-Sail local dev (runs php artisan serve directly). Inside Sail, use sail npm run dev for Vite only since Sail already runs the PHP server via supervisord
  • Telescope is dev-only. It loads conditionally and is gated by TelescopeServiceProvider
  • Debugbar only appears when APP_DEBUG=true
  • IDE helper files are gitignored - regenerate with sail artisan ide-helper:generate
  • If Vite HMR doesn't connect, verify VITE_PORT in .env, server.port in vite.config.js, and port mapping in compose.yaml all match
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment