Skip to main content

Technology Stack - Laravel + Inertia + Vue 3

Document Version: 2.0 - Updated Recommendation Last Updated: December 12, 2025 Status: Recommended Based on Security Analysis

Security Considerations

This recommendation reflects analysis of the React2Shell vulnerability (CVE-2025-55182) disclosed in December 2024. While React remains a valid choice with proper maintenance, Vue 3 provides a simpler, more secure foundation for PBS Planning's requirements.


Executive Summary

Updated Recommended Stack:

  • Backend: Laravel 11
  • Frontend Bridge: Inertia.js
  • UI Framework: Vue 3 (Composition API)
  • Styling: Tailwind CSS v3+
  • Components: Nuxt UI / PrimeVue
  • Forms: VeeValidate + Zod validation
  • Testing: Vitest + Vue Test Utils

Architecture: Modern monolith (no separate API)

Why This Change from React:

  1. Better security track record - No CVE 10.0 vulnerabilities
  2. Simpler for forms-heavy apps - PBS is primarily CRUD operations
  3. Easier to learn - Lower complexity than React
  4. Perfect for prototypes - Vue templates are HTML-like (matches existing prototypes)
  5. 87% developer satisfaction vs React's 43%

1. Why Laravel + Inertia + Vue 3?

The Perfect Stack for PBS Planning

Laravel + Inertia + Vue combines the best of both worlds:

  • Laravel's maturity (20+ years of PHP, 13+ years of Laravel)
  • Vue's simplicity (easy to learn, powerful enough for enterprise)
  • Inertia's elegance (no API needed, SPA experience)

Security First

React2Shell Vulnerability Context:

  • In December 2024, React Server Components had a 10.0 CVSS vulnerability (CVE-2025-55182)
  • Remote Code Execution with no authentication required
  • Actively exploited in the wild
  • While patched, it exposed fundamental architectural risks

Vue's Security Record:

  • ✅ Only one CVE in 13+ years (CVE-2024-6783, medium severity, XSS)
  • ✅ Traditional architecture - no experimental serialization
  • ✅ Simpler = smaller attack surface
  • ✅ Perfect for security-critical business apps like PBS

Why This Matters for PBS:

  • PBS handles sensitive crew data, financial information, production schedules
  • Multi-tenant architecture requires bulletproof security
  • CERT-SE (Swedish National CERT) issued warnings about React2Shell
  • Conservative, proven choices are best for business-critical systems

2. Vue 3 vs React - The Honest Comparison

Developer Experience

FeatureReactVue 3Winner
Learning CurveModerateEasy🏆 Vue
Forms HandlingVerboseSimple (v-model)🏆 Vue
State ManagementContext/ReduxPinia (built-in)🏆 Vue
Side EffectsuseEffect + deps arraywatchEffect (auto-tracks)🏆 Vue
TemplatesJSX (JavaScript)HTML-like🏆 Vue
Developer Satisfaction43%87%🏆 Vue
Ecosystem SizeHugeLargeReact
Hiring PoolLargestLargeReact

Perfect for PBS Because:

1. Forms-Heavy Application

  • PBS is 80% forms: project creation, crew profiles, assignment offers, travel booking
  • Vue's v-model makes forms 30% less code than React
  • VeeValidate is the best form library (better than React Hook Form)

2. Your Prototypes Are Almost Vue Already

Current HTML prototype:

<div class="project-card">
<h3>Idre Freestyle</h3>
<button onclick="window.location.href='/projects/1'">
View Project
</button>
</div>

Vue version (minimal changes):

<template>
<div class="project-card">
<h3>{{ project.name }}</h3>
<button @click="$router.push(`/projects/${project.id}`)">
View Project
</button>
</div>
</template>

React version (more different):

<div className="project-card">
<h3>{project.name}</h3>
<button onClick={() => navigate(`/projects/${project.id}`)}>
View Project
</button>
</div>

3. Simpler Mental Model

Vue:

<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
</script>

<template>
<button @click="increment">Count: {{ count }}</button>
</template>

React:

import { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}

4. Better for Multi-tenancy

Vue's template system makes dynamic theming cleaner:

<template>
<div :style="{ '--brand-color': company.brand_color }">
<button class="bg-[--brand-color]">Company Branded Button</button>
</div>
</template>

3. Complete Stack Breakdown

Backend

  • Laravel 11
  • PostgreSQL (as per schema.dbml)
  • Multi-tenancy Package: stancl/tenancy or spatie/laravel-multitenancy
  • Laravel Breeze (authentication with Inertia + Vue)
  • Spatie Permissions (role-based access control)

Frontend

  • Vue 3 (Composition API with <script setup>)
  • Inertia.js 1.0+
  • TypeScript (recommended for type safety)

Styling

  • Tailwind CSS v3+
  • Nuxt UI or PrimeVue (component library)
  • Headless UI (accessible primitives)

Forms & Validation

  • Client-side: VeeValidate + Zod
  • Server-side: Laravel validation rules
  • Auto-sync - Inertia passes Laravel errors to Vue automatically

State Management

  • Pinia (official Vue state management - like Vuex but simpler)
  • Inertia form helpers (for form state)
  • Vue Provide/Inject (for component communication)

Testing

  • Backend: PHPUnit (Laravel default)
  • Frontend: Vitest + Vue Test Utils
  • E2E: Laravel Dusk or Playwright

Development Tools

  • Vite (bundler, included with Laravel)
  • ESLint + Prettier (code formatting)
  • Laravel Pint (PHP code formatting)
  • Vue DevTools (browser extension for debugging)

4. Why Inertia.js is Perfect for PBS

What is Inertia?

Inertia.js connects Laravel backend with Vue frontend without building an API.

Traditional SPA approach:

Laravel API ← JSON → Vue SPA
(Two separate apps, CORS, tokens, complexity)

Inertia approach:

Laravel + Inertia → Vue
(One app, session auth, simpler)

Benefits for PBS

1. No API Layer

Laravel Controller:

public function index()
{
return Inertia::render('Projects/Index', [
'projects' => Project::with('crew')
->forCurrentCompany()
->paginate(20)
]);
}

Vue Component:

<script setup>
defineProps({
projects: Object // Automatically typed from Laravel!
});
</script>

<template>
<div v-for="project in projects.data" :key="project.id">
{{ project.name }}
</div>
</template>

2. Session-Based Authentication

  • More secure than JWT for web apps
  • Laravel's built-in auth works normally
  • No CORS headaches
  • No token management

3. Server-Side Routing

  • Routes defined in routes/web.php (familiar Laravel routing)
  • No need to duplicate routes in frontend
  • SEO-friendly

4. Multi-Tenant Subdomains Work Perfectly

// routes/web.php

// Admin portal: backend.pbs.com
Route::domain('backend.{company}.pbs.com')
->middleware(['auth', 'admin'])
->group(function () {
Route::get('/dashboard', [BackendController::class, 'index']);
Route::get('/projects', [ProjectController::class, 'index']);
});

// Crew portal: crew.pbs.com
Route::domain('crew.pbs.com')
->middleware(['auth', 'crew'])
->group(function () {
Route::get('/dashboard', [CrewController::class, 'index']);
Route::get('/offers', [CrewOfferController::class, 'index']);
});

5. Simpler Deployment

  • One codebase, one server
  • No separate API deployment
  • No Vercel + Heroku complexity
  • Just deploy Laravel (Forge, Ploi, or DIY)

5. Project Structure

pbs-planning/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── Backend/
│ │ │ │ ├── DashboardController.php
│ │ │ │ ├── ProjectController.php
│ │ │ │ ├── CrewController.php
│ │ │ │ └── OfferController.php
│ │ │ └── CrewPortal/
│ │ │ ├── DashboardController.php
│ │ │ ├── OfferController.php
│ │ │ └── ProfileController.php
│ │ └── Middleware/
│ │ ├── HandleInertiaRequests.php
│ │ └── TenantMiddleware.php
│ └── Models/
│ ├── Company.php
│ ├── User.php
│ ├── CrewProfile.php
│ ├── Project.php
│ ├── Assignment.php
│ └── AssignmentOffer.php

├── database/
│ ├── migrations/
│ │ └── [27 migrations from schema.dbml]
│ └── seeders/

├── resources/
│ ├── js/
│ │ ├── Components/
│ │ │ ├── Backend/
│ │ │ │ ├── Navigation.vue
│ │ │ │ ├── ProjectCard.vue
│ │ │ │ └── OfferDialog.vue
│ │ │ └── CrewPortal/
│ │ │ ├── OfferCard.vue
│ │ │ └── AssignmentTimeline.vue
│ │ ├── Layouts/
│ │ │ ├── BackendLayout.vue
│ │ │ └── CrewLayout.vue
│ │ ├── Pages/
│ │ │ ├── Backend/
│ │ │ │ ├── Dashboard.vue
│ │ │ │ ├── Projects/
│ │ │ │ │ ├── Index.vue
│ │ │ │ │ ├── Show.vue
│ │ │ │ │ └── Create.vue
│ │ │ │ ├── Crew/
│ │ │ │ │ ├── Index.vue
│ │ │ │ │ └── Profile.vue
│ │ │ │ └── Offers/
│ │ │ │ ├── Index.vue
│ │ │ │ └── Create.vue
│ │ │ └── CrewPortal/
│ │ │ ├── Dashboard.vue
│ │ │ ├── Offers/
│ │ │ │ ├── Index.vue
│ │ │ │ └── Show.vue
│ │ │ ├── Assignments/
│ │ │ │ └── Index.vue
│ │ │ └── Profile/
│ │ │ └── Edit.vue
│ │ ├── composables/ # Reusable logic (like React hooks)
│ │ │ ├── useProjects.js
│ │ │ ├── useOffers.js
│ │ │ └── useTenant.js
│ │ └── app.js # Inertia app setup
│ └── css/
│ └── app.css # Tailwind imports

├── routes/
│ └── web.php # All routes (no api.php needed!)

├── tests/
│ ├── Feature/
│ └── Unit/

├── tailwind.config.js
├── vite.config.js
├── package.json
└── composer.json

6. Getting Started

Initial Setup

1. Create Laravel Project:

composer create-project laravel/laravel pbs-planning
cd pbs-planning

2. Install Laravel Breeze with Inertia + Vue:

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

# This installs:
# - Inertia.js
# - Vue 3
# - Tailwind CSS
# - Basic authentication (login, register, password reset)
# - Vite configuration

3. Install Additional Packages:

# Component library (choose one)
npm install @nuxt/ui # Option A: Nuxt UI
npm install primevue # Option B: PrimeVue

# Form validation
npm install vee-validate @vee-validate/zod zod

# State management
npm install pinia

# Icons
npm install @heroicons/vue

4. Configure Multi-tenancy:

# Install tenancy package
composer require stancl/tenancy

# Publish configuration
php artisan tenancy:install

5. Set up Database:

# Create migrations from schema.dbml
php artisan make:migration create_companies_table
php artisan make:migration create_users_table
php artisan make:migration create_crew_profiles_table
# ... (27 tables total)

php artisan migrate

6. Run Development Server:

# Terminal 1: Laravel backend
php artisan serve

# Terminal 2: Vite (watches Vue/CSS changes)
npm run dev

7. Open Browser:

http://localhost:8000

7. Code Examples

See the separate document: Laravel + Inertia + Vue Code Examples


8. Why NOT React?

React is Still Valid, But...

React Concerns:

  1. Security - React2Shell showed architectural risks with RSC
  2. Complexity - More boilerplate for forms and state
  3. Lower Satisfaction - 43% vs Vue's 87%
  4. JSX Learning Curve - Different from HTML

When React Makes Sense:

  • You need maximum ecosystem size
  • Your team already knows React deeply
  • You're building a highly interactive, real-time app (social media, collaborative tools)

For PBS:

  • Forms-heavy CRUD app = Vue is better fit
  • Security-critical data = Choose proven, simple tech
  • HTML prototypes already exist = Vue templates match better

9. Migration from React Recommendation

If you've already started with React:

Option 1: Continue with React

  • ✅ Stick with proven ecosystem
  • ✅ Avoid Server Components entirely (use traditional SPA)
  • ⚠️ Commit to rapid security patching
  • ⚠️ More complexity for forms

Option 2: Switch to Vue

  • ✅ Simpler long-term
  • ✅ Better for PBS's specific needs
  • ✅ Easier to hire for in Sweden/Europe
  • ⚠️ Small upfront cost to learn Vue (2-3 days for React developers)

Recommendation: If starting fresh, choose Vue. If deep into React development, continue but avoid RSC.


10. Developer Experience Comparison

State Management

React (Context + useState):

// Verbose setup required
const UserContext = createContext();

export function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}

// Usage
function Dashboard() {
const { user, setUser } = useContext(UserContext);
return <div>{user.name}</div>;
}

Vue (Pinia):

// stores/user.js - Simple, built-in
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
state: () => ({ user: null }),
actions: {
setUser(newUser) {
this.user = newUser;
}
}
});

// Usage - no providers needed!
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
</script>

<template>
<div>{{ userStore.user.name }}</div>
</template>

Forms

React:

const [formData, setFormData] = useState({
name: '',
budget: 0
});

const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};

return (
<form>
<input
name="name"
value={formData.name}
onChange={handleChange}
/>
<input
name="budget"
value={formData.budget}
onChange={handleChange}
/>
</form>
);

Vue:

<script setup>
import { reactive } from 'vue';

const formData = reactive({
name: '',
budget: 0
});
</script>

<template>
<form>
<input v-model="formData.name" />
<input v-model.number="formData.budget" />
</form>
</template>

Result: Vue is ~40% less code for forms.


11. Security Checklist

Laravel Security

  • ✅ CSRF protection (built-in)
  • ✅ SQL injection prevention (Eloquent ORM)
  • ✅ XSS prevention (Blade escaping)
  • ✅ Session security (HTTPOnly cookies)
  • ✅ Password hashing (bcrypt)
  • ✅ Rate limiting (middleware)

Vue Security

  • ✅ Template escaping (automatic)
  • ✅ No eval() usage
  • ✅ Content Security Policy compatible
  • ✅ Traditional architecture (no serialization risks)

Additional Measures

  • ✅ Multi-tenancy row-level security
  • ✅ Automated dependency updates (Dependabot)
  • ✅ Vulnerability scanning (Snyk)
  • ✅ Regular security audits
  • ✅ HTTPS enforcement
  • ✅ Secure headers middleware

12. Cost Analysis

Open Source (Free)

  • Laravel ✅
  • Vue 3 ✅
  • Inertia.js ✅
  • Tailwind CSS ✅
  • Pinia ✅
  • VeeValidate ✅

Optional Paid

  • PrimeVue (free) or Nuxt UI (free)
  • Laravel Forge: $12-19/month (deployment)
  • Hosting: $20-100/month (DigitalOcean, AWS, etc.)

Total MVP Cost: ~$0 (except hosting)


13. Next Steps

  1. Team Review - Discuss this recommendation
  2. Proof of Concept - Build one view (Projects List)
  3. Developer Training - 2-3 day Vue learning for React developers
  4. Setup Development Environment
  5. Create Database Migrations from schema.dbml
  6. Build MVP Features following user cases

14. Questions & Answers

Is Vue harder to hire for than React?

No - Vue is the 3rd most popular framework (15.4% market share). Large talent pool in Europe, especially Sweden.

Can we add a mobile app later?

Yes - You can:

  1. Build a REST API alongside Inertia (for mobile)
  2. Use Capacitor/Ionic to wrap the Vue web app
  3. Use Vue Native (community solution)

What about real-time features?

Laravel Broadcasting + Pusher (or Laravel Reverb) works perfectly with Vue.

How mature is Vue 3?

Very mature - Released in 2020, stable since 2021. Used by Alibaba, GitLab, Adobe, Nintendo.


15. Alternative: Keep React Option Open

React + Inertia is still valid if:

  • Your team has deep React expertise
  • You want the largest ecosystem
  • You avoid Server Components entirely
  • You commit to rapid security patching

Setup would be:

php artisan breeze:install react  # Instead of vue

Everything else stays the same. You get:

  • Laravel backend
  • Inertia.js bridge
  • React frontend (traditional, no RSC)
  • Tailwind CSS
  • shadcn/ui components

Recommendation: Vue is safer and simpler for PBS, but React is viable with proper maintenance.


References


Document Status: Ready for team review Recommended Decision: Laravel + Inertia + Vue 3 Alternative Option: Laravel + Inertia + React (without Server Components) Next Review: After team discussion and technology selection