Inertia Start

v1.5.0

v1.5.0

This release brings Laravel Octane to the starter kit, moves the product and subscription catalog out of configuration files and into the database, lets configured descriptions be translated per locale, and improves frontend loading performance, accessibility, and page metadata.

Highlights

  • Laravel Octane is installed, with FrankenPHP as the default server, and the application is prepared for long-lived workers.
  • In order to support Octane, products and subscriptions config are now stored in the database instead of config/env/products.php and config/env/subscriptions.php.
  • The products and plans saved in the billing editors are now the checkout allowlist, replacing the I_S_*_ALLOWED_PRICES and I_S_*_ALLOWED_DISCOUNTS environment variables.
  • Non-critical dependencies and layouts are lazy-loaded, and the primary font is preloaded.
  • Suspicious activity notifications are sent again, and three listeners no longer run twice per event.
  • Public pages carry meta descriptions, and navigation labels are translated instead of hard-coded in English.

Added

Laravel Octane

  • Added laravel/octane and published config/octane.php, with frankenphp as the default server. Added OCTANE_SERVER and OCTANE_HTTPS to .env.example.
  • Added App\Octane\Listeners\WarmRedisConnections, registered on WorkerStarting. It pings the Redis connections used by the session driver and the default cache store when a worker boots, so the first request does not pay the connection latency.
  • Enabled Spatie Permission's register_octane_reset_listener, so the permission registrar is reset between requests instead of holding a stale cache in the worker.
  • Added DevCommands::artisan('serve', 'server') to AppServiceProvider. Octane registers octane:start --watch as the server process of php artisan dev at vendor priority, which would download a FrankenPHP binary into the project root on first run; registering from userland outranks it and keeps local development on the built-in server.
  • Added frankenphp and /public/frankenphp-worker.php to .gitignore.
  • Added feature coverage in tests/Feature/DevCommandsTest.php and unit coverage in tests/Unit/Listeners/WarmRedisConnectionsTest.php.
  • Added the billing_product_configurations and billing_subscription_configurations tables, the App\Models\BillingProductConfiguration and App\Models\BillingSubscriptionConfiguration models, and the App\Concerns\StoresBillingConfiguration trait they share.

Localized product and subscription descriptions

  • A configured product, subscription group, or plan description may now be an array keyed by locale. Only the entry matching the current locale is used.
  • A plain string keeps working. When it is a Laravel translation key it still resolves in every locale; otherwise it is literal copy that only applies to the application's fallback locale.
  • Added App\Rules\LocalizedText, used to validate a description that is either a string or an array keyed by an available locale, with a 5,000 character limit per value.

Link prefetching

  • Added the enable_link_prefetching option to config/inertia-start.php, built from the new I_S_ENABLE_LINK_PREFETCHING environment variable and enabled by default. It is shared with the frontend as the linkPrefetchingEnabled prop.

Page titles and metadata

  • Added messages.meta.home, messages.meta.login, messages.meta.register, messages.meta.password_lost, messages.meta.terms_of_service, and messages.meta.privacy_policy in English, Spanish, and French.
  • The welcome, login, registration, registration completion, forgotten password, terms of service, privacy policy, and buy pages now set a meta description. The registration and forgotten password pages also gained their missing title.
  • MetaData now gives every tag it renders a head-key, so a page that sets its own description or Open Graph values replaces the shared ones instead of adding a second tag.
  • Added browser coverage asserting that each public page exposes a meta description.

Accessibility

  • Added the messages.nav_menu, messages.nav_to, messages.nav_to_item, messages.sub_nav, messages.current_nav, and messages.nav_none_selected translations in English, Spanish, and French, replacing hard-coded English labels in AppHeader and Breadcrumbs.
  • The mobile navigation trigger gained a screen-reader label, the breadcrumb group select is now wrapped in a BreadcrumbItem so it sits in the breadcrumb list, and the language switcher trigger gained an accessible name.

Other

  • Added pooled and direct host settings to the PostgreSQL connection in config/database.php, built from the new DB_POOLED and DB_DIRECT_HOST variables, for databases reached through a connection pooler.
  • Added tests/Feature/EventListenerRegistrationTest.php, asserting that each application listener is registered exactly once for the event it handles.
  • Added tests/Unit/frontendBundle.test.mjs and tests/Unit/persistentLayout.test.mjs, covering the lazy-loaded dependencies, the font preload, the date locale handling, and the persistent page layout.
  • Added browser coverage asserting that the language switcher updates the page without reloading the document.

Changed

Billing

  • BillingService now reads products and subscription groups from the database instead of config('inertia-start.products') and config('inertia-start.subscriptions').
  • The checkout allowlist is now derived from the saved configuration: the price identifiers and discounts of the configured products and plans are the only ones accepted at checkout. There is no longer a * allowlist, so a price or discount that is not configured is rejected with a 404.
  • The billing editors no longer write PHP files. Saving now stores rows and takes effect immediately, so the "configuration is cached" warning and the configurationIsCached page prop were removed, and billing.config_editor.saved no longer takes a :file placeholder.
  • The product editor now fails validation when a selected price cannot be resolved from the provider catalog, when the selected prices do not share one active currency, or when a selected discount cannot be found. It previously saved the product without its price snapshot.
  • Discount snapshots now respect the provider's own enabled_for_checkout flag, and the editors only offer discounts that are enabled for checkout.

Frontend performance

  • Every layout except AppLayout is now loaded with defineAsyncComponent, so a visit only downloads the shell it renders.
  • useContext() now imports @thumbmarkjs/thumbmarkjs dynamically after the component mounts, and stops before registering its listeners when the component was unmounted meanwhile.
  • configureEcho() was removed from the browser entry point, keeping the unused realtime client out of the initial bundle.
  • The Inter variable font file is now preloaded from app.blade.php.
  • The manual vue-vendor chunk group was removed from vite.config.ts, leaving dependency chunking to Rolldown.
  • English dates now use date-fns' built-in locale instead of downloading the en-US chunk, and switching back to English clears a previously selected locale.

Localization

  • The switcher reads its languages from the live page props, so it keeps pointing at the current page's URLs when it sits in a persistent layout.
  • resolveCurrentLocale() is now backed by a reactive reference, so computed route helpers re-resolve after a locale change. Its window.history.state fallback was removed, since the synchronized locale and the locale meta tag already cover client and server rendering.

Events

  • Application listeners now rely on Laravel's automatic listener discovery. The manual Event::subscribe() and Event::listen() calls were removed from AppServiceProvider, along with the subscribe() methods of the event subscribers.
  • MediaEventSubscriber::conversionCompleted() was renamed to handleConversionCompleted(), and SecurityEventSubscriber::notifyThrottle() to handleTooManyAttempts(), so discovery picks them up.

Other

  • The "or" separators on the login, registration, registration completion, and password confirmation pages now use the page background on small screens and the card background from the lg breakpoint, instead of always using the card background.
  • --muted-foreground is now #5d5d5d in the gray, neutral, slate, stone, and zinc secondary themes, improving contrast against light backgrounds.
  • The base test case now calls withoutVite(), so backend tests no longer depend on a stale or missing frontend build manifest.
  • Upgraded the bundled Inertia Table to 1.0.1, which adds accessible names to the per-page select and the page number input in the pagination.
  • Synced with laravel/vue-starter-kit up to commit d282e81: auth inputs and the passkey name field use the v-focus directive instead of the autofocus attribute, PasswordInput gained an autofocus prop, and the directive is now typed through Vue's GlobalDirectives interface.

Fixed

  • Fixed suspicious activity notifications never being sent. Its handler is now discovered automatically and covered by a test that dispatches the event.
  • Fixed three listeners running twice for every event. SessionEventSubscriber::handleSuccessfulLogin(), RecordBillingCheckout, and ProvisionGuestCheckoutCustomer were registered manually in AppServiceProvider on top of being discovered automatically, so each login, Stripe webhook, Paddle webhook, and completed Paddle transaction invoked them twice.
  • Fixed the focus directive being missing during server-side rendering.
  • Fixed the language switcher using stale page properties when rendered inside a persistent layout, which could send the visitor to the previous page's localized URL.
  • Fixed shared meta tags being duplicated when a page set its own description or Open Graph values.

Removed

  • Removed the I_S_STRIPE_ALLOWED_PRICES, I_S_STRIPE_ALLOWED_DISCOUNTS, I_S_PADDLE_ALLOWED_PRICES, and I_S_PADDLE_ALLOWED_DISCOUNTS environment variables and their config keys. The saved product and subscription selections are now the allowlist.
  • Removed the inertia-start.products and inertia-start.subscriptions config keys. config/env/products.php and config/env/subscriptions.php are no longer read.
  • Removed App\Listeners\PaddleEventListener, an unused example listener.
  • Removed the unused Helpers::displaySettings(), Helpers::dumpMemory(), Helpers::increasePhpLimits(), and Helpers::generateArrayHash() helpers.
  • Removed tests/Unit/navigation.test.mjs, whose assertions are now covered by the persistent layout and bundle tests.

Dependencies

  • Added laravel/octane 2.20.
  • Updated minor and patch Composer and npm dependencies.

Upgrade Guide

  1. Run composer install.
  2. Run npm install.
  3. Run php artisan migrate to create the billing_product_configurations and billing_subscription_configurations tables.
  4. Rebuild frontend assets with npm run build or your normal frontend workflow.
  5. Move your billing configuration into the database. config/env/products.php and config/env/subscriptions.php are no longer read, and both new tables start empty, so every /buy/{product} and /subscribe/{group}/{plan} page returns 404 until you re-create your catalog from /admin/billing/products and /admin/billing/subscriptions. Keep the old files at hand while you do it, then delete them.
  6. Remove I_S_STRIPE_ALLOWED_PRICES, I_S_STRIPE_ALLOWED_DISCOUNTS, I_S_PADDLE_ALLOWED_PRICES, and I_S_PADDLE_ALLOWED_DISCOUNTS from your .env file. Saved products and plans are now the allowlist, and a wildcard allowlist no longer exists: if your own code starts a checkout with a price or discount that is not part of a configured product or plan, configure it in the editors first, or that checkout will return 404.
  7. If your code reads config('inertia-start.products') or config('inertia-start.subscriptions'), read BillingProductConfiguration::configuration() or BillingSubscriptionConfiguration::configuration() instead. Both return the same array shape, ordered, with the row's slug merged in.
  8. If you customized resources/js/pages/admin/billing/ProductsConfig.vue or SubscriptionsConfig.vue, merge the new locales prop and the per-locale description fields, and drop the configurationIsCached prop. If you customized lang/*/billing.php, note that config_editor.saved no longer takes a :file placeholder and that config_editor.config_cached and config_editor.file_path were replaced by config_editor.database_storage.
  9. To translate a description, save one per locale from the editors, or store an array keyed by locale. A plain string that is not a translation key now only shows in the fallback locale, and other locales fall back to the generic buy page description.
  10. Installing Octane does not change how your application is served. php artisan dev deliberately stays on php artisan serve, and config/octane.php is already published. To serve with Octane, add OCTANE_SERVER and OCTANE_HTTPS to your .env file, install the server binary for your chosen driver, and start it with php artisan octane:start. Review any singleton, static property, or long-lived state in your own code before doing so, since workers keep the application booted between requests.
  11. If you registered your own listeners in a service provider, check them against the new EventListenerRegistrationTest: Laravel discovers any listener method named handle or starting with handle that type-hints its event, so a manual registration on top of that runs the listener twice.
  12. If you customized MediaEventSubscriber or SecurityEventSubscriber, rename their handlers to handleConversionCompleted() and handleTooManyAttempts(), or register them manually, otherwise they stop being called.
  13. If you used Helpers::displaySettings(), Helpers::dumpMemory(), Helpers::increasePhpLimits(), or Helpers::generateArrayHash(), copy the implementation you need into your own code.
  14. Link prefetching stays enabled. Set I_S_ENABLE_LINK_PREFETCHING=false to turn it off, and if you customized the navigation components, the breadcrumbs, or the user menu, replace their hard-coded prefetch attribute with :prefetch="$page.props.linkPrefetchingEnabled".
  15. If you customized PageLayout.vue, merge the asynchronous layout imports, and if you customized resources/js/ssr.ts, register FocusPlugin there too.
  16. If you customized resources/views/app.blade.php, add the font preload link, pointing it at your own font file if you replaced Inter.
  17. If you customized MetaData.vue, add a head-key to each tag, and set a description on your own pages so they do not inherit another page's.
  18. If you overrode --muted-foreground in a secondary theme, or relied on the previous value, review the new contrast.
  19. If you use PostgreSQL behind a connection pooler, set DB_POOLED=true and point DB_DIRECT_HOST at the direct host used for operations the pooler cannot serve.