# Laravel + React Starter Kit
This is a modern full-stack web application starter kit that combines Laravel's powerful backend framework with React's component-based frontend architecture. Built on Inertia.js, it provides seamless client-side routing with server-side controllers, eliminating the need for a separate API layer. The starter kit delivers a complete authentication system with two-factor authentication, user profile management, email verification, and password reset functionality out of the box.
The application leverages React 19 with TypeScript for type safety, Tailwind CSS 4 for utility-first styling, and the shadcn/ui component library built on Radix UI primitives. Laravel Fortify handles authentication concerns on the backend, while Laravel Wayfinder generates type-safe route helpers for the frontend. The project uses Vite 7 for lightning-fast hot module replacement during development and optimized production builds, along with the React Compiler for automatic performance optimization. This architecture enables developers to build modern single-page applications while maintaining the simplicity and productivity of traditional server-side rendering.
## Authentication APIs
### User Registration Endpoint
Register a new user account with name, email, and password. Laravel Fortify handles the registration logic using a custom action class that validates input, creates the user record with hashed password, triggers the Registered event, and automatically logs in the new user.
```php
// Backend: app/Actions/Fortify/CreateNewUser.php
['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => $this->passwordRules(),
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
]);
}
}
// Fortify configuration: app/Providers/FortifyServiceProvider.php
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::registerView(fn () => Inertia::render('auth/register'));
// Frontend: resources/js/pages/auth/register.tsx
import { register as registerAction } from '@/routes/register';
import { Form } from '@inertiajs/react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
export default function Register() {
return (
);
}
```
### User Login Endpoint
Authenticate a user with email and password, with support for remember-me functionality and two-factor authentication. Laravel Fortify handles the login logic automatically, including 2FA checks. If 2FA is enabled for the user, Fortify redirects to the two-factor challenge page instead of logging in directly.
```php
// Backend: Laravel Fortify handles login automatically
// Route: POST /login
// Configured in: app/Providers/FortifyServiceProvider.php
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
'canResetPassword' => Features::enabled(Features::resetPasswords()),
'canRegister' => Features::enabled(Features::registration()),
'status' => $request->session()->get('status'),
]));
// Logout route: POST /logout (handled by Fortify)
// Frontend: resources/js/pages/auth/login.tsx
import { store } from '@/routes/login';
import { request } from '@/routes/password';
import { Form } from '@inertiajs/react';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
export default function Login({
canResetPassword,
canRegister
}: {
canResetPassword: boolean;
canRegister: boolean;
}) {
return (
);
}
```
### Password Reset Request
Send a password reset link to the user's email address. Laravel Fortify validates the email exists in the system and uses Laravel's password broker to send the reset link with a signed URL token.
```php
// Backend: Laravel Fortify handles password reset requests
// Routes automatically registered:
// GET /forgot-password - Show password reset request form
// POST /forgot-password - Send reset link email
// Configured in: app/Providers/FortifyServiceProvider.php
Fortify::requestPasswordResetLinkView(fn (Request $request) =>
Inertia::render('auth/forgot-password', [
'status' => $request->session()->get('status'),
])
);
// Frontend: resources/js/pages/auth/forgot-password.tsx
import { email } from '@/routes/password';
import { Form } from '@inertiajs/react';
export default function ForgotPassword({ status }: { status?: string }) {
return (
);
}
// Usage: POST to /forgot-password
curl -X POST http://localhost/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
```
### Email Verification
Verify user email addresses via signed URL links. Laravel Fortify handles email verification automatically. Users receive an email with a verification link after registration, and certain routes require verified emails using the 'verified' middleware.
```php
// Backend: Laravel Fortify registers these routes automatically
// GET /verify-email - Show verification notice page
// GET /verify-email/{id}/{hash} - Verify email via signed URL
// POST /email/verification-notification - Resend verification email
// Configured in: app/Providers/FortifyServiceProvider.php
Fortify::verifyEmailView(fn (Request $request) =>
Inertia::render('auth/verify-email', [
'status' => $request->session()->get('status'),
])
);
// config/fortify.php - Enable feature
'features' => [
Features::emailVerification(),
],
// Protected route requiring verification: routes/web.php
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', fn() => Inertia::render('dashboard'))->name('dashboard');
});
// Frontend: resources/js/pages/auth/verify-email.tsx
import { send } from '@/routes/verification';
import { Link } from '@inertiajs/react';
export default function VerifyEmail({ status }: { status?: string }) {
return (
<>
Thanks for signing up! Before getting started, please verify
your email address by clicking the link we emailed to you.
)}
>
);
}
```
## Two-Factor Authentication
### Enable Two-Factor Authentication
Generate QR code and secret key for setting up two-factor authentication using authenticator apps like Google Authenticator or Authy. Laravel Fortify provides built-in endpoints for 2FA management.
```typescript
// Frontend: resources/js/hooks/use-two-factor-auth.ts
import { qrCode, recoveryCodes, secretKey } from '@/routes/two-factor';
import { useCallback, useMemo, useState } from 'react';
export const useTwoFactorAuth = () => {
const [qrCodeSvg, setQrCodeSvg] = useState(null);
const [manualSetupKey, setManualSetupKey] = useState(null);
const [recoveryCodesList, setRecoveryCodesList] = useState([]);
const [errors, setErrors] = useState([]);
const hasSetupData = useMemo(
() => qrCodeSvg !== null && manualSetupKey !== null,
[qrCodeSvg, manualSetupKey]
);
const fetchQrCode = useCallback(async (): Promise => {
try {
const { svg } = await fetch(qrCode.url(), {
headers: { Accept: 'application/json' }
}).then(r => r.json());
setQrCodeSvg(svg);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch QR code']);
}
}, []);
const fetchSetupKey = useCallback(async (): Promise => {
try {
const { secretKey: key } = await fetch(secretKey.url(), {
headers: { Accept: 'application/json' }
}).then(r => r.json());
setManualSetupKey(key);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch setup key']);
}
}, []);
const fetchSetupData = useCallback(async (): Promise => {
await Promise.all([fetchQrCode(), fetchSetupKey()]);
}, [fetchQrCode, fetchSetupKey]);
const fetchRecoveryCodes = useCallback(async (): Promise => {
try {
const codes = await fetch(recoveryCodes.url(), {
headers: { Accept: 'application/json' }
}).then(r => r.json());
setRecoveryCodesList(codes);
} catch {
setErrors((prev) => [...prev, 'Failed to fetch recovery codes']);
}
}, []);
return {
qrCodeSvg, manualSetupKey, recoveryCodesList, hasSetupData, errors,
fetchSetupData, fetchRecoveryCodes
};
};
// Usage in component
const { qrCodeSvg, manualSetupKey, fetchSetupData, fetchRecoveryCodes } = useTwoFactorAuth();
// Enable 2FA: POST to /user/two-factor-authentication
await fetch('/user/two-factor-authentication', { method: 'POST' });
await fetchSetupData(); // Get QR code and setup key
// Confirm with OTP code: POST to /user/confirmed-two-factor-authentication
await fetch('/user/confirmed-two-factor-authentication', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: '123456' })
});
await fetchRecoveryCodes(); // Get recovery codes after confirmation
```
### Two-Factor Challenge Login
Verify two-factor authentication code during login process when 2FA is enabled for the user account.
```php
// Backend: Laravel Fortify handles 2FA routes automatically
// Route: POST /two-factor-challenge
// Body: { "code": "123456" } or { "recovery_code": "abcd-efgh" }
// Frontend usage with Inertia form
import { Form } from '@inertiajs/react';
import { InputOTP } from '@/components/ui/input-otp';
export default function TwoFactorChallenge() {
const [useRecoveryCode, setUseRecoveryCode] = useState(false);
return (
);
}
```
## User Profile Management
### Update User Profile
Update authenticated user's name and email address. If email changes, verification status is reset requiring re-verification. Uses custom controller instead of Fortify for profile updates.
```php
// Backend: app/Http/Controllers/Settings/ProfileController.php
$request->user() instanceof MustVerifyEmail,
'status' => $request->session()->get('status'),
]);
}
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return to_route('profile.edit');
}
}
// Request validation: app/Http/Requests/Settings/ProfileUpdateRequest.php
public function rules()
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255',
Rule::unique(User::class)->ignore($this->user()->id)],
];
}
// Routes: routes/settings.php
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('settings/profile', [ProfileController::class, 'update'])->name('profile.update');
// Frontend: resources/js/pages/settings/profile.tsx
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import { usePage } from '@inertiajs/react';
export default function Profile() {
const { auth } = usePage().props;
return (
);
}
```
### Update Password
Change the authenticated user's password with current password confirmation and new password validation. Uses a custom controller with rate limiting for security.
```php
// Backend: app/Http/Controllers/Settings/PasswordController.php
validate([
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back();
}
}
// Routes: routes/settings.php
Route::middleware('auth')->group(function () {
Route::get('settings/password', [PasswordController::class, 'edit'])
->name('user-password.edit');
Route::put('settings/password', [PasswordController::class, 'update'])
->middleware('throttle:6,1')
->name('user-password.update');
});
// Frontend: Use Wayfinder-generated form helpers
import PasswordController from '@/actions/App/Http/Controllers/Settings/PasswordController';
```
### Delete User Account
Permanently delete the authenticated user's account after verifying their password. The user is logged out and redirected to the home page.
```php
// Backend: app/Http/Controllers/Settings/ProfileController.php
public function destroy(Request $request): RedirectResponse
{
$request->validate([
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
// Route: routes/settings.php
Route::middleware('auth')->group(function () {
Route::delete('settings/profile', [ProfileController::class, 'destroy'])
->name('profile.destroy');
});
// Frontend: resources/js/components/delete-user.tsx
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import { Form } from '@inertiajs/react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
export default function DeleteUser() {
const [open, setOpen] = useState(false);
return (
);
}
```
## Frontend Hooks and Utilities
### Theme/Appearance Management
Manage light, dark, and system theme preferences with localStorage persistence and cookie-based SSR support. Automatically responds to system theme changes when in system mode.
```typescript
// resources/js/hooks/use-appearance.tsx
import { useCallback, useEffect, useState } from 'react';
export type Appearance = 'light' | 'dark' | 'system';
const prefersDark = () => {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const setCookie = (name: string, value: string, days = 365) => {
if (typeof document === 'undefined') return;
const maxAge = days * 24 * 60 * 60;
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
};
const applyTheme = (appearance: Appearance) => {
const isDark = appearance === 'dark' || (appearance === 'system' && prefersDark());
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
};
export function useAppearance() {
const [appearance, setAppearance] = useState('system');
const updateAppearance = useCallback((mode: Appearance) => {
setAppearance(mode);
localStorage.setItem('appearance', mode);
setCookie('appearance', mode);
applyTheme(mode);
}, []);
useEffect(() => {
const savedAppearance = localStorage.getItem('appearance') as Appearance | null;
updateAppearance(savedAppearance || 'system');
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
const current = localStorage.getItem('appearance') as Appearance;
applyTheme(current || 'system');
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [updateAppearance]);
return { appearance, updateAppearance } as const;
}
// Usage in component: resources/js/pages/settings/appearance.tsx
import { useAppearance } from '@/hooks/use-appearance';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
function AppearanceSettings() {
const { appearance, updateAppearance } = useAppearance();
return (
LightDarkSystem
);
}
```
### Inertia Shared Data Access
Access globally shared data from Laravel backend including authentication state, application name, and sidebar state.
```php
// Backend: app/Http/Middleware/HandleInertiaRequests.php
random())->explode('-');
return [
...parent::share($request),
'name' => config('app.name'),
'quote' => ['message' => trim($message), 'author' => trim($author)],
'auth' => [
'user' => $request->user(),
],
'sidebarOpen' => !$request->hasCookie('sidebar_state') ||
$request->cookie('sidebar_state') === 'true',
];
}
}
// Frontend: Access shared data in any Inertia page component
import { usePage } from '@inertiajs/react';
interface SharedProps {
name: string;
quote: { message: string; author: string };
auth: { user: User | null };
sidebarOpen: boolean;
}
function WelcomePage() {
const { name, auth, quote } = usePage().props;
return (
Welcome to {name}
{auth.user &&
Hello, {auth.user.name}!
}
{quote.message} — {quote.author}
);
}
```
## Configuration and Setup
### Environment Configuration
Configure application settings and build-time environment variables for both Laravel backend and React frontend.
```bash
# .env file - Laravel configuration
APP_NAME="My Application"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite
# Expose to frontend via Vite
VITE_APP_NAME="${APP_NAME}"
# Email configuration for password resets and verification
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
```
### Vite Build Configuration
Configure frontend build tooling with Laravel plugin, React support with the React Compiler, Tailwind CSS, and Wayfinder for type-safe routing.
```typescript
// vite.config.ts
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import laravel from 'laravel-vite-plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
ssr: 'resources/js/ssr.tsx',
refresh: true,
}),
react({
babel: {
plugins: ['babel-plugin-react-compiler'], // Automatic React optimization
},
}),
tailwindcss(),
wayfinder({
formVariants: true, // Generate type-safe form helpers
}),
],
esbuild: {
jsx: 'automatic',
},
});
// package.json scripts
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:ssr": "vite build && vite build --ssr",
"format": "prettier --write resources/",
"lint": "eslint . --fix",
"types": "tsc --noEmit"
}
}
// Development: Run Laravel, Queue, Logs, and Vite concurrently
// composer.json
{
"scripts": {
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"dev:ssr": [
"npm run build:ssr",
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others"
]
}
}
// Start development servers with colored output
composer dev
```
### Fortify Authentication Configuration
Configure Laravel Fortify features and behavior for authentication, registration, and two-factor authentication. All authentication features are enabled by default in this starter kit.
```php
// config/fortify.php
'web',
'passwords' => 'users',
'username' => 'email',
'email' => 'email',
'lowercase_usernames' => true,
'home' => '/dashboard',
'limiters' => [
'login' => 'login', // 5 requests per minute per email/IP
'two-factor' => 'two-factor', // 5 requests per minute per session
],
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::twoFactorAuthentication([
'confirm' => true, // Require OTP confirmation to enable 2FA
'confirmPassword' => true, // Require password before viewing 2FA settings
// 'window' => 0 // Allow codes from previous/next time window
]),
],
];
// Rate limiting configuration: app/Providers/FortifyServiceProvider.php
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
// Note: Profile and password updates use custom controllers in this starter kit
// instead of Fortify's built-in features for more control
```
## Summary
This Laravel + React starter kit provides a complete foundation for building modern web applications with robust authentication, user management, and security features. The authentication system handles registration, login, password resets, email verification, and two-factor authentication through Laravel Fortify, while the frontend delivers a polished user experience with React 19, TypeScript, and shadcn/ui components. All user-facing authentication flows include proper validation, error handling, rate limiting, and security measures like CSRF protection and session regeneration. The React Compiler automatically optimizes component performance without manual memoization.
The integration between Laravel and React via Inertia.js creates a seamless development experience where developers write standard Laravel controllers and React components without building REST APIs or managing complex client-side state. Shared data flows automatically from Laravel middleware to React components, type-safe routing is generated via Laravel Wayfinder, and form submissions work intuitively with server-side validation and Wayfinder's form helpers. Development is streamlined with Vite 7 for instant hot module replacement, Laravel Pail for beautiful colored logs, and concurrent processes for server, queue, logs, and frontend build. This architecture scales from simple CRUD applications to complex SPAs while maintaining developer productivity, code organization, and the best features of both ecosystems. The starter kit serves as an ideal foundation for SaaS applications, internal tools, customer portals, and any web application requiring user authentication and modern UX.