{} CodeLift

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.

Pub 2026-04-19 Verified 2026-04-19 Upd 2026-08-12

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 installnpm 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 HandleInertiaRequests or 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' improvements branches were executed again in Docker. React and Vue both come back at 44 passed (151 assertions), and the React csp-nonce branch reproduces 48 passed (164 assertions).

When this fits

  • You are starting a production product from laravel/react-starter-kit or laravel/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

License

Both the original samples and the forks are MIT. Findings reflect the verification date.

Featured in comparisons

Related articles