Hardening the Inertia Starter Kits
We ran laravel/react-starter-kit and laravel/vue-starter-kit in Docker and published two forks rewritten to survive production. Tests went from 40 passed to 44 passed / 151 assertions in both.
Verification environment
- PHP 8.5.5
- Laravel 13.x
- Node 22.22.2
- Frontend React 19 / Vue 3 + Inertia
- Tests (both forks) 40 → 44 passed / 151 assertions
- OS Docker Desktop (php:8.5-cli-bookworm)
We ran laravel/react-starter-kit and laravel/vue-starter-kit in Docker and published two forks rewritten to survive production. Tests went from 40 passed to 44 passed / 151 assertions in both.
This article covers the findings specific to the Inertia layer. React and Vue are treated together in one article because verification showed their findings to be identical — more on that at the end.
The six backend fixes are shared by React, Vue and Livewire, so they live in the pillar:
→ Six backend fixes that make a Laravel starter kit production-ready
The official samples
| React | Vue | |
|---|---|---|
| Upstream | laravel/react-starter-kit | laravel/vue-starter-kit |
| Our fork | codelift-dev/react-starter-kit | codelift-dev/vue-starter-kit |
| Frontend | React 19 + TypeScript + shadcn/ui | Vue 3 Composition API + TypeScript + shadcn-vue |
| Shared | Laravel 13 + Inertia + Fortify, MIT / MIT | |
| Upstream commit | at time of verification | laravel/vue-starter-kit@1233a92 |
| Verified | 2026-04-19 | 2026-04-19 |
| Tests | 40 → 44 passed / 151 | 40 → 44 passed / 151 |
Environment
Docker Desktop, PHP 8.5.5 / Laravel 13.x / Node 22.22.2 — same as the pillar.
git clone https://github.com/codelift-dev/react-starter-kit.git # or vue-starter-kit
cd react-starter-kit
git checkout improvements
docker compose -f codelift/docker-compose.yml build
docker compose -f codelift/docker-compose.yml run --rm app
A. Running npm run build before composer install fails inexplicably
Specific to the Inertia kits. It does not happen on Livewire.
Symptom: run npm run build first on a fresh clone and the Vite build dies:
require(.../vendor/autoload.php): Failed to open stream: No such file or directory
in artisan on line 10
Cause: the @laravel/vite-plugin-wayfinder plugin in vite.config.ts shells out to php artisan wayfinder:generate during the build. Wayfinder generates typed TypeScript route helpers from Laravel's route definitions, which means starting artisan at build time. With Composer dependencies missing, artisan cannot read vendor/autoload.php.
Why it costs time: the error never mentions composer install. A newcomer suspects Vite or Wayfinder and digs in the wrong direction. Recognising vendor/autoload.php as "Composer hasn't been run" is instant only if you already know Laravel.
Fix: a Setup section in the README stating the composer install → npm run build order, and noting that Wayfinder invokes artisan during the build so the reason for that order is clear.
Both forks receive the identical change.
Inertia's initial state and CSP
Inertia connects server-side routing to frontend components. Each navigation returns a JSON page object the client renders. On first load that page object has to reach the HTML.
The output is:
<script data-page="app" type="application/json">{...}</script>
<div id="app"></div>
Because of type="application/json" this is a data block: the browser never executes it, so it falls outside script-src. Inertia is not an obstacle to CSP.
The improvements branch introduces CSP (pillar finding D) but conservatively keeps 'unsafe-inline' in script-src. The csp-nonce branch that removes it has its own article.
→ Inertia's CSP can use nonces, with no patching required
Correction (2026-07-23): this section originally claimed Inertia embeds initial props in an inline script, so nonce adoption required patching
HandleInertiaRequestsor similar. That was inferred without reading the implementation, and is corrected above.
Does React differ from Vue here?
No. The frontends feel very different — React 19's JSX and hooks versus Vue 3's SFCs with ref and computed, shadcn/ui versus shadcn-vue. But Inertia emits identical HTML and mounts the page object the same way.
For production hardening, the frontend framework makes no difference. That is also why this is one article rather than two.
J. Settings endpoints are throttled on password only
routes/settings.php attaches explicit throttle middleware to the password route and nothing else.
Route::patch('settings/profile', ...)->name('profile.update'); // no throttle
Route::delete('settings/profile', ...)->name('profile.destroy'); // no throttle
Route::put('settings/password', ...)->middleware('throttle:6,1')...; // throttled
With a hijacked session, repeated profile rewrites and repeated account-deletion attempts run within the global default only.
Fix: on React and Vue the settings actions map cleanly onto PATCH and DELETE verbs, so throttles go straight on the route definitions.
- Route::patch('settings/profile', ...)->name('profile.update');
+ Route::patch('settings/profile', ...)->middleware('throttle:10,1')->name('profile.update');
- Route::delete('settings/profile', ...)->name('profile.destroy');
+ Route::delete('settings/profile', ...)->middleware('throttle:3,1')->name('profile.destroy');
Account deletion is terminal, hence the tighter 3 per minute.
This fix does not transfer to Livewire. Its settings go through component AJAX via Route::livewire, so a route-level throttle does not bound the right thing. See the Livewire article.
Commit layout
Both forks carry 8 commits on improvements (README setup [A], .env [B], timezone [C], forceScheme [E], SetSecurityHeaders [D], rate limiter [G], auth log [I], settings throttle [J]). B, C, D, E, G and I are covered by the pillar; A and J originate here.
The React fork additionally has a csp-nonce branch at 48 passed / 164 assertions.
Before / after (Inertia-specific)
| Aspect | Official | Improved |
|---|---|---|
php artisan test |
40 passed / 136 assertions | 44 passed / 151 assertions |
| Build order documented | no | stated in README |
| Settings endpoint throttles | password only | profile update / destroy too |
The pillar covers before/after for the six shared backend fixes.
Tests re-run 2026-07-23: the numbers above date from the April verification, so both forks'
improvementsbranches were executed again in Docker. React and Vue both come back at 44 passed (151 assertions), and the Reactcsp-noncebranch reproduces 48 passed (164 assertions).
When this fits
- You are starting a production product from
laravel/react-starter-kitorlaravel/vue-starter-kit - You are deciding between React and Vue — the hardening cost is identical, and so are the findings here. Decide on frontend preference, hiring, and shadcn/ui versus shadcn-vue instead. See the three-way comparison
Reproducing
git clone https://github.com/codelift-dev/react-starter-kit.git # or vue-starter-kit
cd react-starter-kit
git diff origin/main improvements -- . ':!codelift'
About this article (merged 2026-07-23)
This was originally two articles, one for React and one for Vue. Here is why they became one.
Once the verification was done, the Inertia-specific findings came to exactly two — A and J — and both were identical across React and Vue. The test results matched too, at 44 passed / 151 assertions, leaving the Vue article's only unique content as a repository URL and a commit hash. At the time the reasoning was "the forks are separate repositories, so the articles should be separate," and the Vue article even carried a section justifying its own existence.
Needing to justify the split was the signal not to split. One page covering both serves a reader better than two pages saying the same thing. The forks stay separate; the article does not.
The old URLs 301 here.
Related
- Pillar: Six backend fixes that make a Laravel starter kit production-ready
- The Livewire kit — no Inertia, so the structure differs
- Inertia's CSP can use nonces
- React / Vue / Livewire compared
License
Both the original samples and the forks are MIT. Findings reflect the verification date.
Featured in comparisons
Related articles
- Hardening the Livewire Starter Kit We took the official laravel/livewire-starter-kit (Livewire v4 + Flux + Alpine), ran it inside Docker, and published a production-hardened fork. Tests go from the upstream 33 passed to 37 passed / 92 assertions — re-run in Docker on 2026-0…
- Hardening the shared Laravel starter-kit backend Laravel's official starter kits (React / Vue / Livewire) differ in their frontend layer, but they share the same Laravel + Fortify backend code. So most of the production-hardening work is identical across all three. This is the pillar art…
- Inertia Needs No Patch for a Nonce-Based CSP Try to drop 'unsafe-inline' from the Content-Security-Policy of a Laravel + Inertia app and you'll usually land on some version of: "Inertia embeds the initial props in an inline script, so you can't — you'd have to patch Inertia or Handle…