The Boring Stack 1.5.0
The Boring Stack 1.5.0 is the release that moves the Sails + Inertia foundation to stable Inertia v3 and rounds out the developer experience around rendering, validation, testing, and errors.
This release includes inertia-sails@1.5.0, rsbuild-plugin-inertia@0.0.1, updated templates, selective SSR, Precognition, richer v3 protocol metadata, and Youch-powered development errors.
Highlights
- Stable Inertia v3 support across
inertia-sailsand the Vue, React, and Svelte templates. - New
rsbuild-plugin-inertiapackage for Rsbuild page resolution, lazy page splitting, optional Axios adapter stubbing, and SSR build wiring. - Selective Inertia SSR that runs inside Sails without a separate SSR server process.
- Inertia Precognition support for live server-side validation using the same Action2 inputs and
badRequestflow. - Rescuable deferred props with
rescuedProps, plus the.rescue()and{ rescue: true }APIs. sails.inertia.preserveFragment()for redirects that should keep the current hash.- Production Inertia error pages for
403,404,500, and503. - Youch-powered development error pages that Inertia can show inside its development modal.
- Template test structure moved toward Sounding-first functional testing.
- Templates updated to Shipwright
^1.4.0, Inertia adapters^3.1.1, andinertia-sails@^1.5.0.
Inertia v3 support
The root page payload now follows Inertia v3's dedicated JSON script shape:
<div id="app"></div>
<script type="application/json" data-page="app">
<%- JSON.stringify(page).replace(/</g, '\\u003c') %>
</script>The adapter now emits the protocol metadata the v3 client adapters need:
sharedPropsclearHistoryencryptHistorydeferredPropsmergePropsprependPropsdeepMergePropsmatchPropsOnscrollPropsrescuedPropspreserveFragment
This keeps Sails actions simple while giving the Inertia client the full v3 page object shape.
Rescuable deferred props
Deferred props can now fail gracefully when the data is useful but not critical:
analytics: sails.inertia
.defer(async () => {
return await Analytics.getExpensiveReport()
})
.rescue()You can also pass the intent inline:
analytics: sails.inertia.defer(
async () => {
return await Analytics.getExpensiveReport()
},
{ rescue: true }
)When a rescuable deferred prop fails, inertia-sails omits it from props and reports the key in rescuedProps. Use this for secondary panels such as analytics, recommendations, and activity feeds. Do not use it for critical page data.
Preserved fragments
Use preserveFragment() when a redirect should carry the current hash:
sails.inertia.preserveFragment()
return '/articles/new-slug'This is opt-in because fragment preservation should be a redirect-level product decision, not hidden global behavior.
rsbuild-plugin-inertia
rsbuild-plugin-inertia centralizes Inertia-specific Rsbuild behavior:
- default page resolver injection for
./pages - an Inertia/Vite-style
pagesshorthand for non-standard directories - lazy page loading by default for page-level code splitting
lazy: falsefor single-bundle apps- Vue, React, and Svelte adapter detection
- automatic SSR build environment setup when
assets/js/ssr.jsexists - optional Axios adapter stubbing when Axios is not installed
Template config/shipwright.js files now use this shape:
const { pluginVue } = require('@rsbuild/plugin-vue')
const { pluginInertia } = require('rsbuild-plugin-inertia')
module.exports.shipwright = {
build: {
plugins: [pluginVue(), pluginInertia()]
}
}Use pluginReact() or pluginSvelte() for React and Svelte apps.
Because the plugin stubs Inertia v3's optional Axios adapter import when Axios is not installed, templates no longer need Axios unless the app code imports Axios directly.
Selective SSR
The Boring Stack now supports Inertia SSR without requiring a separate SSR server process. Shipwright builds a private SSR bundle, and inertia-sails imports it in-process from Sails.
Enable SSR for every Inertia page:
module.exports.inertia = {
ssr: true
}Or enable it selectively:
module.exports.inertia = {
ssr: {
enabled: true,
pages: ['index', 'pricing', 'blog/show']
}
}Opt out per response:
return {
page: 'dashboard/index',
ssr: false,
props: { user }
}Use SSR for public pages where first paint, SEO, and social sharing matter. Skip it for most authenticated dashboards and heavily browser-dependent screens.
Precognition
Precognition comes from the Laravel/Inertia ecosystem. It lets the client ask the same server action to validate data before the real submit happens.
That means you do not duplicate server validation in the browser.
Client:
const form = useForm({
email: ''
}).withPrecognition('post', '/forgot-password')
form.validate('email')Server response:
// api/responses/precognitionSuccess.js
module.exports = function precognitionSuccess() {
return this.req._sails.inertia.handlePrecognitionSuccess(this.req, this.res)
}Server action:
if (sails.inertia.isPrecognitive(this.req)) {
return exits.precognitionSuccess()
}For availability checks such as "username is taken", use shouldValidate() so a blur on one field does not run every expensive validation rule:
if (sails.inertia.shouldValidate('username', this.req)) {
const exists = await User.count({ username })
if (exists > 0) {
throw {
badSignupRequest: {
problems: [{ username: 'Username is already taken.' }]
}
}
}
}Precognition is best for signup, forgot password, invite, username, profile, onboarding, and billing forms where early server-backed feedback improves the product.
Humanized validation errors
inertia-sails now humanizes common Action2, Anchor, and RTTC validation messages before they reach Inertia forms. Precognition and normal submit-time errors share the same path, so the user sees consistent field messages whether validation happened on blur or on submit.
Rich development errors with Youch
Development server errors now render with Youch.
For Inertia requests, Inertia shows the Youch HTML in its development error modal. For normal browser visits, the browser gets the full Youch page.
The Youch output includes readable stack frames, source snippets, request context, and sanitized metadata. Cookies, authorization headers, CSRF tokens, passwords, secrets, and session-looking values are redacted before rendering.
We also opened an upstream Inertia issue for the tiny empty modal flash that can appear before iframe content is written: inertiajs/inertia#3130.
Production Inertia error pages
Templates now ship an error page:
assets/js/pages/error.vueassets/js/pages/error.jsxassets/js/pages/error.svelte
inertia-sails renders that page by default for:
403404500503
The page receives:
{
status: 404,
title: 'Page not found',
message: 'The page you are looking for could not be found.'
}Templates also include notFound and forbidden responses that delegate to sails.inertia.handleErrorPage().
Hybrid apps can keep EJS error pages by setting:
module.exports.inertia = {
errorPage: false
}Sounding and template tests
The templates now lean into Sounding as the default testing story:
- unit tests for small pure logic
- functional tests for Sails actions, responses, auth, mail, and Inertia responses
- browser tests only when browser behavior itself matters
This gives Boring Stack apps a calmer test split and avoids making every workflow a full browser automation problem.
Upgrade guide
1. Update packages
Vue:
npm install inertia-sails@^1.5.0 @inertiajs/vue3@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosReact:
npm install inertia-sails@^1.5.0 @inertiajs/react@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosSvelte:
npm install inertia-sails@^1.5.0 @inertiajs/svelte@^3.1.1 rsbuild-plugin-inertia@latest sails-hook-shipwright@^1.4.0
npm uninstall axiosKeep Axios only if your app imports Axios directly.
2. Update views/app.ejs
Move the page payload to the dedicated JSON script:
<div id="app"></div>
<script type="application/json" data-page="app">
<%- JSON.stringify(page).replace(/</g, '\\u003c') %>
</script>If you enable SSR, render ssr.body when present and include ssr.head in the document head.
3. Update config/shipwright.js
Add pluginInertia() after the framework plugin:
const { pluginVue } = require('@rsbuild/plugin-vue')
const { pluginInertia } = require('rsbuild-plugin-inertia')
module.exports.shipwright = {
build: {
plugins: [pluginVue(), pluginInertia()]
}
}4. Simplify assets/js/app.js
If your pages live in assets/js/pages, remove custom page resolution and let rsbuild-plugin-inertia inject the default resolver:
createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) }).use(plugin).mount(el)
}
})Use pages: './screens' or the object form only when your app has a non-standard page directory.
5. Add production error pages
Create an error page under your page directory and add notFound and forbidden responses that call sails.inertia.handleErrorPage().
6. Add Precognition where it helps
Add api/responses/precognitionSuccess.js, return that exit before side effects, and call .withPrecognition() on the client form.
7. Add SSR only where it pays for itself
Create assets/js/ssr.js, enable ssr: true or selective ssr.pages, and verify view-source: contains rendered HTML for SSR-enabled pages.
8. Use the new protocol APIs intentionally
- Use
sails.inertia.preserveFragment()only for redirects where the hash should survive. - Use
{ rescue: true }or.rescue()only for secondary deferred props. - Use
shouldValidate()for expensive Precognition checks.
9. Verify each app
Run:
npm install
npm run lint
npm test
npm run devThen manually check:
- the root view includes
data-page="app" - Inertia navigation still works
- normal submit validation still appears
- Precognition validation appears on blur where enabled
- 404 and 403 render the production error page
- development server errors show Youch
- SSR pages show full HTML in view-source
- EJS pages still work in hybrid apps
PRs included
- #199 Support Inertia v3 protocol metadata
- #201 Complete Sounding integration across templates
- #202 Update templates to Shipwright 1.3.0
- #203 Add selective Inertia SSR support
- #204 Add Inertia Precognition support
- #205 Add rich Boring Stack error handling
Compare: v1.2.3...v1.5.0