diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 700c612..e4242af 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -10,6 +10,7 @@ export default defineConfig({
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/introduction' },
+ { text: 'Quick start', link: '/guide/quick-start' },
{ text: 'API', link: '/reference/api' },
{
text: 'Links',
@@ -27,8 +28,11 @@ export default defineConfig({
text: 'Guide',
items: [
{ text: 'Introduction', link: '/guide/introduction' },
- { text: 'Installation', link: '/guide/installation' },
+ { text: 'Quick start', link: '/guide/quick-start' },
+ { text: 'How it works', link: '/guide/how-it-works' },
+ { text: 'Feel native', link: '/guide/feel-native' },
{ text: 'Navigation', link: '/guide/navigation' },
+ { text: 'Manual installation', link: '/guide/installation' },
],
},
{
@@ -50,6 +54,10 @@ export default defineConfig({
text: 'Reference',
items: [{ text: 'API', link: '/reference/api' }],
},
+ {
+ text: 'Help',
+ items: [{ text: 'FAQ', link: '/guide/faq' }],
+ },
],
socialLinks: [
diff --git a/docs/guide/faq.md b/docs/guide/faq.md
new file mode 100644
index 0000000..08a21a9
--- /dev/null
+++ b/docs/guide/faq.md
@@ -0,0 +1,112 @@
+# FAQ
+
+Answers to the questions people ask most before they try Inertia Native. Facts
+about other projects come from their own sites and were checked in October
+2026.
+
+## How does it compare to other options?
+
+Each of these tools gets an app into the App Store and Google Play. They differ
+in where your screens come from and how much of your current app you reuse.
+Inertia Native keeps your server and your Inertia pages as they are, loads them
+in native screens, and works with any backend.
+
+### Capacitor
+
+[Capacitor](https://capacitorjs.com/docs) runs a web app inside a native
+project, and plugins give your JavaScript access to native APIs. It bundles
+your built web assets, starting from an `index.html`, into the app. Its
+`server.url` option loads a remote site instead, but the
+[config docs](https://capacitorjs.com/docs/config) say it's meant for live
+reload and "not intended for use in production."
+
+An Inertia app needs your server to answer every page visit, so it doesn't fit
+the bundled model without changes. Inertia Native loads pages from your server,
+and each visit opens a native screen.
+
+### NativePHP Mobile
+
+[NativePHP Mobile](https://nativephp.com/) runs your Laravel app on the phone
+itself, with a PHP runtime embedded in the app, so the app needs no server.
+Since version 4 it can turn Blade views into SwiftUI and Jetpack Compose views,
+and it still supports showing HTML in a web view, which is where Inertia apps
+run. Its core is free and MIT-licensed, and some plugins are paid.
+
+Choose it when you want the app to work on the device without your server.
+Inertia Native does the opposite: your Laravel app keeps running on your
+server, and the phone shows its pages. Releasing a change means deploying your
+server, not shipping a new app build.
+
+### Ruby Native
+
+[Ruby Native](https://rubynative.com/) is a paid service for Rails apps, built
+on Hotwire Native. You add its gem and a configuration file, and its cloud
+builds compile, sign, and submit the apps, so you don't need Xcode or Android
+Studio. Its site says it works with any Rails frontend, Inertia included.
+Plans start at $299 per app per year.
+
+Inertia Native is free and open source, works with any backend, and gives you
+the native projects as plain Xcode and Gradle code that you build yourself.
+
+### React Native and Expo
+
+[React Native](https://reactnative.dev/) renders React components to native
+platform UI, and [Expo](https://docs.expo.dev/get-started/introduction/) is a
+framework and set of tools around it. With either one you write a second app
+with its own screens, which usually calls your backend through an API.
+
+Pick React Native when most of your screens need to be fully native. Inertia
+Native reuses the pages you already have, and you can still add native screens
+and [bridge components](/components/overview) where they matter.
+
+## Will Apple accept my app?
+
+Apple reviews every app on its own, so nobody can promise approval. The
+approach has a track record: 37signals builds its
+[Basecamp](https://apps.apple.com/us/app/basecamp-project-management/id1015603248)
+and [HEY](https://apps.apple.com/us/app/hey-email/id1506603805) apps on
+[Turbo Native](https://dev.37signals.com/speeding-up-mobile-development-with-turbo/)
+and [Strada](https://dev.37signals.com/announcing-strada/), the two libraries
+that [became Hotwire Native](https://dev.37signals.com/announcing-hotwire-native/).
+Both apps are in the App Store and Google Play.
+
+The guideline to read is
+[4.2 Minimum Functionality](https://developer.apple.com/app-store/review/guidelines/#minimum-functionality):
+"Your app should include features, content, and UI that elevate it beyond a
+repackaged website." Native navigation helps. So do hiding your site's own
+navigation in the app (see [Feel native](/guide/feel-native)) and adding
+native features where your users benefit from them.
+
+## Does server-side rendering work?
+
+Yes, with a guard. `initInertiaNative()` uses `window`, so if your entrypoint
+also runs on the server, call it only in the browser:
+
+```js
+if (typeof window !== 'undefined') initInertiaNative()
+```
+
+Bridge components don't render on the server yet. `useBridgeComponent` reads
+`window` while a component renders, in all three framework bindings, so a page
+that uses a bridge component fails to render on the server.
+
+## Which Inertia versions does it support?
+
+Inertia Native needs Inertia 3 (`@inertiajs/core` 3.0 or later) with React,
+Vue, or Svelte. Version 1.0 dropped support for Inertia 2. See
+[Manual installation](/guide/installation#requirements) for the full list.
+
+## Does it work offline? Does it support push notifications?
+
+Neither is built in yet. The app loads every page from your server, so it
+needs a network connection. The shells from `init` don't include push
+notifications. Both are native features that you can add to the projects in
+`ios/` and `android/` yourself.
+
+## Do I need to know Swift or Kotlin?
+
+Not to get started. `init` creates both native projects, and
+`npm run ios` and `npm run android` build and run them. You open the native
+code to change the app's name, icon, or URL, or to add a native component. The
+[iOS](/native/ios) and [Android](/native/android) pages show where each of
+those lives.
diff --git a/docs/guide/feel-native.md b/docs/guide/feel-native.md
new file mode 100644
index 0000000..a6d53c2
--- /dev/null
+++ b/docs/guide/feel-native.md
@@ -0,0 +1,424 @@
+# Feel native
+
+After the [quick start](/guide/quick-start), your app runs in a native shell
+but still looks like a website in places. This page covers the changes that
+make the biggest difference: hiding web-only parts, closing the sidebar,
+starting fresh after logging in, closing a modal after a form, and adding a
+native button.
+
+The examples use the Laravel and Rails React starter kits, and the tabs show
+where the two differ.
+
+## Tell the app apart from a browser
+
+Hotwire Native adds its name to the web view's user agent on both platforms.
+iOS requests contain `Hotwire Native iOS;` and Android requests contain
+`Hotwire Native Android;`. Check for `Hotwire Native` and you cover both.
+
+Your server sees the user agent on every request, including Inertia's own
+requests. The simplest setup checks it there and shares the result with every
+page as an Inertia prop:
+
+::: code-group
+
+```php [Laravel]
+// app/Http/Middleware/HandleInertiaRequests.php
+public function share(Request $request): array
+{
+ return [
+ ...parent::share($request),
+ 'nativeApp' => str_contains($request->userAgent() ?? '', 'Hotwire Native'),
+ ];
+}
+```
+
+```ruby [Rails]
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+ inertia_share do
+ { nativeApp: native_app? }
+ end
+
+ private
+
+ def native_app?
+ request.user_agent.to_s.include?("Hotwire Native")
+ end
+end
+```
+
+:::
+
+In JavaScript that runs outside a component, read the user agent directly.
+Put the check in a module, so you can import it wherever you need it:
+
+::: code-group
+
+```ts [Laravel]
+// resources/js/lib/native.ts
+export const isNativeApp =
+ typeof navigator !== 'undefined' &&
+ navigator.userAgent.includes('Hotwire Native');
+```
+
+```ts [Rails]
+// app/javascript/lib/native.ts
+export const isNativeApp =
+ typeof navigator !== "undefined" &&
+ navigator.userAgent.includes("Hotwire Native")
+```
+
+:::
+
+The `typeof` check keeps the line safe during server-side rendering, where
+`navigator` may not exist.
+
+For CSS, add a class to the `` element. Do it in an inline script in
+your root template's `
`, so the class is there before the page first
+appears:
+
+::: code-group
+
+```blade [Laravel]
+{{-- resources/views/app.blade.php, inside --}}
+
+```
+
+```erb [Rails]
+<%# app/views/layouts/application.html.erb, inside %>
+
+```
+
+:::
+
+A selector that starts with `.native-app` now matches only in the app. React
+doesn't render ``, so the class is safe with server-side rendering too.
+
+::: warning Don't check `window.webkit`
+`window.webkit.messageHandlers` comes from WKWebView, the web view on iOS.
+Android's web view doesn't have it, so a check like
+`window.webkit?.messageHandlers?.turbo` is false on Android. Use the user
+agent instead.
+:::
+
+## Hide web-only parts
+
+In the app, the shell already shows a navigation bar with the page title and a
+back button. Your site's own header, back links, and footer now repeat it.
+Hide them when `nativeApp` is true:
+
+::: code-group
+
+```jsx [React]
+import { usePage } from '@inertiajs/react'
+
+export default function Layout({ children }) {
+ const { nativeApp } = usePage().props
+
+ return (
+ <>
+ {!nativeApp && }
+ {children}
+ {!nativeApp && }
+ >
+ )
+}
+```
+
+```vue [Vue]
+
+
+
+
+
+
+
+```
+
+```svelte [Svelte]
+
+
+{#if !page.props.nativeApp}
+
+{/if}
+{@render children()}
+{#if !page.props.nativeApp}
+
+{/if}
+```
+
+:::
+
+The server sends the prop with the page, so the header doesn't flash on screen
+while the page loads.
+
+### Page titles
+
+The native navigation bar shows the page's ``, the same one you set with
+Inertia's ``. The starter kits add two things around it. Their
+`createInertiaApp` adds the app's name to every title, so the bar reads
+`Dashboard - Laravel`. And the header at the top of the page shows the title
+again as breadcrumbs.
+
+Drop the app's name in the app:
+
+::: code-group
+
+```tsx [Laravel]
+// resources/js/app.tsx
+import { isNativeApp } from '@/lib/native';
+
+createInertiaApp({
+ title: (title) =>
+ title ? (isNativeApp ? title : `${title} - ${appName}`) : appName,
+ // …
+});
+```
+
+```tsx [Rails]
+// app/javascript/entrypoints/inertia.tsx
+import { isNativeApp } from "@/lib/native"
+
+createInertiaApp({
+ title: (title) =>
+ title ? (isNativeApp ? title : `${title} - ${appName}`) : appName,
+ // …
+})
+```
+
+:::
+
+Then hide the breadcrumbs with the `native-app` class. The kits build them
+with shadcn/ui's `Breadcrumb`, which marks itself with
+`data-slot="breadcrumb"`:
+
+::: code-group
+
+```css [Laravel]
+/* resources/css/app.css */
+.native-app [data-slot='breadcrumb'] {
+ display: none;
+}
+```
+
+```css [Rails]
+/* app/javascript/entrypoints/application.css */
+.native-app [data-slot="breadcrumb"] {
+ display: none;
+}
+```
+
+:::
+
+The header keeps its sidebar button, which is how you reach the menu in the
+app.
+
+## Close the sidebar when a link opens
+
+On a phone, the starter kits show the sidebar as a sheet over the page. In the
+Laravel kit, the sheet stays open when you tap a link in it: the next screen
+opens with the sheet still on top, and it's still open when you go back.
+
+The shell shows each screen in the same web view, and the Laravel kit keeps
+its sidebar layout between pages, so the open sheet comes along. Close it as
+soon as a visit starts:
+
+```tsx
+// resources/js/components/app-sidebar.tsx
+import { router } from '@inertiajs/react';
+import { useEffect } from 'react';
+import { useSidebar } from '@/components/ui/sidebar';
+import { isNativeApp } from '@/lib/native';
+
+export function AppSidebar() {
+ const { setOpenMobile } = useSidebar();
+
+ useEffect(() => {
+ if (!isNativeApp) {
+ return;
+ }
+
+ return router.on('before', (event) => {
+ if (!event.detail.visit.async) {
+ setOpenMobile(false);
+ }
+ });
+ }, [setOpenMobile]);
+
+ // …
+}
+```
+
+Inertia fires `before` when you tap a link, before the shell opens the next
+screen. Polls and partial reloads are `async` visits that update the current
+page, so they leave the sheet alone. In a browser the sheet behaves as before.
+
+The Rails kit renders its layout inside each page, so its sheet already closes
+on the next screen.
+
+## Start fresh after logging in
+
+After you log in, the dashboard shows up on the login screen, and Back takes
+you to the screens from before you logged in. Two things cause this. By
+default, a form's result stays on the form's screen, so the shell doesn't
+learn that you're on the dashboard now. And when it does, it opens the
+dashboard on top of the login screen.
+
+First, have `inertia-native` hand the page a form lands on to the shell, as a
+new visit. [Form redirects](/guide/navigation#form-redirects) explains the
+option:
+
+::: code-group
+
+```tsx [Laravel]
+// resources/js/app.tsx
+initInertiaNative({ proposeFormRedirects: true });
+```
+
+```tsx [Rails]
+// app/javascript/entrypoints/inertia.tsx
+initInertiaNative({ proposeFormRedirects: true })
+```
+
+:::
+
+Then make the dashboard the first screen, with nothing to go back to. That's
+the `replace_root` presentation. In `ios/App/path-configuration.json` and
+`android/app/src/main/assets/json/path-configuration.json`, replace the
+`^/$` rule with this one:
+
+::: code-group
+
+```json [Laravel]
+{
+ "patterns": ["^/$", "^/dashboard$"],
+ "properties": {
+ "presentation": "replace_root"
+ }
+}
+```
+
+```json [Rails]
+{
+ "patterns": ["^/$", "^/dashboard$", "^/sign_in$"],
+ "properties": {
+ "presentation": "replace_root"
+ }
+}
+```
+
+:::
+
+The home page `/` moves to the new rule too. Its old presentation,
+`clear_all`, goes back to the first screen, and once you've logged in, that's
+the dashboard.
+
+The rule also handles logging out. The Laravel kit logs you out to `/`. The
+Rails kit logs you out to `/sign_in`, so its rule lists that page as well. As
+a side effect, the **Log in** link on the Rails kit's welcome page also opens
+the login screen as the first screen, without a Back button.
+
+The app reads these files when it's built, so run `npm run ios` and
+`npm run android` again. If you also
+[serve the path configuration](/guide/how-it-works#where-it-lives) from your
+app, change the rule there too, because the server's copy replaces the bundled
+file.
+
+## Close a modal after a form
+
+In the shells from `init`, a page whose URL ends in `/new` or `/edit` opens as
+a modal. After the form saves, you want the modal to close and show the screen
+underneath. A redirect can't do this: Inertia follows a redirect inside its own
+request, so by default the shell never sees it. With `proposeFormRedirects`,
+the shell sees the page the form lands on, but it opens that page as a new
+visit instead of going back to the screen underneath.
+
+Instead, answer the form with a historical location. Hotwire Native reserves
+three paths for this, and the shell handles them itself. Your server doesn't
+need routes for them. `/recede_historical_location` closes the modal, or goes
+back one screen when there's no modal.
+
+Send it as an Inertia location visit, and only to the app. Browsers get your
+usual redirect:
+
+::: code-group
+
+```php [Laravel]
+// app/Http/Controllers/PostController.php
+use App\Models\Post;
+use Illuminate\Http\Request;
+use Inertia\Inertia;
+
+public function store(Request $request)
+{
+ Post::create($request->validate([
+ 'title' => ['required', 'string', 'max:255'],
+ ]));
+
+ if (str_contains($request->userAgent() ?? '', 'Hotwire Native')) {
+ return Inertia::location('/recede_historical_location');
+ }
+
+ return redirect()->route('posts.index');
+}
+```
+
+```ruby [Rails]
+# app/controllers/posts_controller.rb
+def create
+ @post = Post.new(post_params)
+
+ if @post.save
+ return inertia_location("/recede_historical_location") if native_app?
+
+ redirect_to posts_path
+ else
+ redirect_to new_post_path, inertia: { errors: @post.errors }
+ end
+end
+```
+
+:::
+
+`inertia-native` hands the location visit to the shell, and the shell closes
+the modal. Validation errors still go back to the form, so they show up inside
+the modal as usual.
+
+To close the modal and also reload the screen under it, so a list shows the
+new post, answer with `/refresh_historical_location` instead.
+[Historical locations](/guide/navigation#historical-locations) lists all three
+paths and has a ready-made Rails helper.
+
+## Add a native button
+
+A native button sits in the navigation bar, next to the title, where native
+apps put actions like **Save** or **Edit**. The [Button](/components/button)
+component adds one from your page and calls your code when someone taps it. In
+a browser it renders your normal button instead.
+
+The shells from `init` already include the native half of this component, and
+of the [Alert](/components/alert) component. You only copy the web half into
+your app.
+
+## Next steps
+
+- [Navigation](/guide/navigation): how every kind of Inertia visit maps to
+ native screens.
+- [Bridge components](/components/overview): how to build your own native
+ components.
diff --git a/docs/guide/how-it-works.md b/docs/guide/how-it-works.md
new file mode 100644
index 0000000..35786d4
--- /dev/null
+++ b/docs/guide/how-it-works.md
@@ -0,0 +1,143 @@
+# How it works
+
+This page explains Hotwire Native for Inertia developers who haven't used it.
+It covers what the native app does, what your web app does, and the two
+Hotwire ideas you'll meet in the rest of these docs: path configuration and
+bridge components.
+
+## The mental model
+
+Think of the native app as a picture frame and your web app as the picture.
+The frame is a real iOS or Android app. It owns the parts around the page:
+the navigation bar, the back button, modals, and error screens. Your server
+still renders every page, the same way it does for a browser.
+
+Each time you visit a new page, the frame opens a new native screen and shows
+the page inside it. Going back closes that screen. That's why the app feels
+native even though every page is your own Inertia page.
+
+[Hotwire Native](https://native.hotwired.dev) is the open-source framework that
+provides the frame. 37signals makes it. It replaced Turbo Native and Strada,
+the two libraries 37signals built its own mobile apps on.
+
+## What happens when you tap a link
+
+Hotwire Native was built for Turbo, the page-navigation library that ships
+with Rails. Each time the app loads one of your pages, it adds a small script
+that expects to find Turbo there. `inertia-native` stands in for Turbo and
+translates between that script and Inertia's router.
+
+Here is a link tap, step by step:
+
+```text
+ You tap
+ │
+ ▼
+ inertia-native stops Inertia from changing the current page
+ and asks the native app to visit /posts/1
+ │
+ ▼
+ The app checks its path configuration:
+ open /posts/1 as a new screen, or as a modal?
+ │
+ ▼
+ The app opens a new native screen
+ and loads /posts/1 into it
+```
+
+In a regular browser no native app is listening, so `inertia-native` does
+nothing and Inertia navigates as usual. The same JavaScript bundle serves both.
+
+[Navigation](/guide/navigation) lists every kind of visit and how it maps to
+native screens.
+
+## What the native app does and what your web app does
+
+These docs call the native app the shell, because it holds your web app but
+has little content of its own. Here is how the work splits between the two:
+
+| The native shell | Your web app |
+| --- | --- |
+| Starts at your URL | Renders every page, as it does for a browser |
+| Shows each page in a web view, the browser engine an app can embed | Decides what's on each page |
+| Opens screens and modals, and draws the back button | Handles links, forms, validation, and sign-in |
+| Shows the page's `` in the navigation bar | Sets that title, for example with Inertia's `` |
+| Shows native error screens and pull to refresh | Returns errors as usual |
+
+Most changes you make happen on the web side and reach users when you deploy.
+You only touch the shell for things like the app name, its icon, or a new
+native component.
+
+## Path configuration
+
+Path configuration is a JSON file of rules that tells the shell how to open
+each URL. Each rule has a list of URL patterns and the properties to apply
+when a URL matches. These are the shared rules in the shells that `init`
+creates:
+
+```json
+{
+ "settings": {},
+ "rules": [
+ {
+ "patterns": ["/new$", "/edit$"],
+ "properties": {
+ "context": "modal",
+ "pull_to_refresh_enabled": false
+ }
+ },
+ {
+ "patterns": ["^/$"],
+ "properties": {
+ "presentation": "clear_all"
+ }
+ }
+ ]
+}
+```
+
+Read it like this. A URL ending in `/new` or `/edit` opens as a modal, with
+pull to refresh turned off. The home page `/` clears the navigation history,
+so going home doesn't stack one more screen on top. The patterns are regular
+expressions matched against the URL's path. When several rules match, the app
+combines their properties, and a later rule wins over an earlier one.
+
+The Android file starts with one more rule. It tells Android which kind of
+native screen shows web pages. You don't need to change it.
+
+### Where it lives
+
+The shell reads path configuration from two places:
+
+- **A file bundled in the app.** It's `ios/App/path-configuration.json` and
+ `android/app/src/main/assets/json/path-configuration.json`. The app uses
+ this file from the first launch.
+- **A copy on your server, if you serve one.** When the app starts, it
+ downloads `/inertia-native/path-configuration/ios_v1.json` or
+ `/inertia-native/path-configuration/android_v1.json` from your app's URL.
+ If the download works, its rules replace the bundled ones, and the app keeps
+ a copy for the next launch. If your server doesn't have that route, the app
+ keeps using the bundled file.
+
+Serving the file lets you change navigation by deploying your web app, without
+shipping an app update. [iOS](/native/ios#path-configuration) and
+[Android](/native/android#path-configuration) show a Laravel and a Rails route
+that serve it.
+
+## Bridge components
+
+A bridge component lets a web page ask the shell for a piece of native UI, such
+as a button in the navigation bar, and hear back when someone uses it. Each
+component has a web half in your JavaScript and a native half in the shell,
+and when the native half is missing the web half renders your normal web
+markup instead.
+
+[Bridge components](/components/overview) covers how to use them, and the
+shells from `init` already include the native halves of the ready-made ones.
+
+## Next steps
+
+- [Feel native](/guide/feel-native): hide web-only parts and close modals
+ after forms.
+- [Navigation](/guide/navigation): every kind of Inertia visit and how it maps
+ to native screens.
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index a793557..ed12f29 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -1,4 +1,9 @@
-# Installation
+# Manual installation
+
+Most apps should start with `npx inertia-native init`, which installs the
+package, sets it up, and creates the native apps. The
+[quick start](/guide/quick-start) walks through it. This page covers the
+package on its own: what it needs and how to set it up by hand.
```bash
npm add inertia-native
@@ -6,10 +11,13 @@ npm add inertia-native
## Requirements
+The package needs Inertia 3. The framework packages are optional peers, so you
+only need the one your app uses:
+
- `@inertiajs/core` >= 3.0
-- `react` >= 18 — only for the `inertia-native/react` entry
-- `vue` >= 3.0 — only for the `inertia-native/vue` entry
-- `svelte` >= 4.0 — only for the `inertia-native/svelte` entry
+- `react` >= 18, only for the `inertia-native/react` entry
+- `vue` >= 3.0, only for the `inertia-native/vue` entry
+- `svelte` >= 4.0, only for the `inertia-native/svelte` entry
## Setup
@@ -20,24 +28,49 @@ entrypoint:
import { createInertiaApp } from '@inertiajs/react'
import { initInertiaNative } from 'inertia-native'
-const isNativeApp = !!window.webkit?.messageHandlers?.turbo
-initInertiaNative({ debug: import.meta.env.DEV || isNativeApp })
+initInertiaNative()
createInertiaApp({ /* ... */ })
```
-That's all that's needed for native navigation (push/pop/replace/restore,
-modals, forms, error screens, pull-to-refresh). See the [API](/reference/api)
-for the available options.
+That's all native navigation needs: new screens, going back, modals, forms,
+error screens, and pull to refresh. In a regular browser the call does
+nothing.
+
+To log the messages between your page and the native app while you develop,
+pass `debug`:
+
+```js
+initInertiaNative({ debug: import.meta.env.DEV })
+```
+
+See the [API](/reference/api) for every option.
+
+### Server-side rendering
-To set up the native iOS and Android shells that load your app, see
-[Native apps → iOS](/native/ios) and [Android](/native/android).
+`initInertiaNative()` uses `window`. If your entrypoint also runs on the
+server, guard the call:
+
+```js
+if (typeof window !== 'undefined') initInertiaNative()
+```
+
+The [FAQ](/guide/faq#does-server-side-rendering-work) covers bridge components
+and server-side rendering.
+
+## Native apps
+
+The package is the web half. The native iOS and Android apps that load your
+pages are separate projects, and `npx inertia-native init` creates them. See
+[iOS](/native/ios) and [Android](/native/android).
## Upgrading from 0.1.0-beta
+Version 1.0 renamed the setup function and dropped Inertia 2:
+
- `initHotwireNative` is now `initInertiaNative`.
- `@inertiajs/core` 2.x is no longer supported.
- The package was published as `inertia-hotwire-native` before
`0.1.0-beta.2`; replace that dependency with `inertia-native`.
-Next: [Navigation](/guide/navigation).
+Next: [Bridge components](/components/overview).
diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md
index 1ce16fd..bacc29d 100644
--- a/docs/guide/introduction.md
+++ b/docs/guide/introduction.md
@@ -1,9 +1,19 @@
# Introduction
-[`inertia-native`](https://www.npmjs.com/package/inertia-native)
-lets an [Inertia.js](https://inertiajs.com) application run inside
-[Hotwire Native](https://native.hotwired.dev) (iOS & Android) and behave like a
-native app.
+[`inertia-native`](https://www.npmjs.com/package/inertia-native) turns an
+[Inertia.js](https://inertiajs.com) app into iOS and Android apps. Your server
+keeps rendering every page, and each page visit opens as a native screen, with
+native navigation, modals, error screens, and pull to refresh. It works with
+React, Vue, and Svelte, and with any backend, such as Laravel or Rails.
+
+It's built on [Hotwire Native](https://native.hotwired.dev), the open-source
+framework from 37signals for apps that show web pages in native screens. You
+don't need to know Hotwire Native to use it.
+
+::: tip New here?
+The [quick start](/guide/quick-start) takes you from your Inertia app to a
+native app running in a simulator.
+:::
::: info Unofficial
This is a community-built integration. It is **not** an official Inertia.js or
@@ -12,21 +22,25 @@ Hotwire project.
## How it works
-Hotwire Native injects a `turbo.js` script into every web view that expects a
-`window.Turbo` object to drive. This package installs a small shim that maps the
-native adapter protocol onto Inertia's router, so Inertia pages push, pop, and
-restore as native screens — plus a web bridge so Inertia pages can use native
-bridge components (submit buttons, menus, etc.).
+Hotwire Native was built for Turbo, the page-navigation library that ships
+with Rails, and it expects to find Turbo on every page. This package stands in
+for Turbo and translates its messages to Inertia's router. When Inertia visits a new page, the native app opens a new screen for
+it. The package also lets your pages use bridge components, which draw native
+UI such as buttons in the navigation bar.
+
+In a regular browser no native app is listening, so the package does nothing
+and Inertia navigates as usual.
-In a regular browser it stays inert: with no native adapter connected, Inertia
-navigates exactly as usual.
+[How it works](/guide/how-it-works) explains the native side step by step.
## Entry points
-- **Framework-agnostic core** (`inertia-native`) — peer-depends on
- `@inertiajs/core`. Handles all navigation.
-- **Bridge component bindings** — optional, each peer-depends on its framework
- and provides `useBridgeComponent`:
+The package has one core entry and three optional ones:
+
+- **Framework-agnostic core** (`inertia-native`) peer-depends on
+ `@inertiajs/core` and handles all navigation.
+- **Bridge component bindings** are optional. Each one peer-depends on its
+ framework and provides `useBridgeComponent`:
- `inertia-native/react`
- `inertia-native/vue`
- `inertia-native/svelte`
@@ -35,9 +49,10 @@ navigates exactly as usual.
| Repository | What it is |
| --- | --- |
-| [inertia-native](https://github.com/inertia-native/inertia-native) | The npm package |
+| [inertia-native](https://github.com/inertia-native/inertia-native) | The npm package, including the native app templates that `init` uses |
+| [bridge-components](https://github.com/inertia-native/bridge-components) | Ready-made bridge components to copy into your app |
| [demo-rails](https://github.com/inertia-native/demo-rails) | Web / Rails demo app |
| [demo-ios](https://github.com/inertia-native/demo-ios) | iOS Hotwire Native shell |
| [demo-android](https://github.com/inertia-native/demo-android) | Android Hotwire Native shell |
-Next: [Installation](/guide/installation).
+Next: [Quick start](/guide/quick-start).
diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md
new file mode 100644
index 0000000..c31b8f1
--- /dev/null
+++ b/docs/guide/quick-start.md
@@ -0,0 +1,332 @@
+# Quick start
+
+This guide takes an Inertia app you already have and runs it as a native iOS
+and Android app in a simulator. It works with any backend. The examples show
+Laravel and Rails, and the tabs show where the two differ.
+
+You don't need to know Hotwire Native, Swift, or Kotlin to follow along.
+[How it works](/guide/how-it-works) explains the moving parts once you've seen
+them run.
+
+## Before you start
+
+You need an Inertia app and the tools for the platforms you want to build:
+
+- An Inertia.js app on Inertia 3 or later, with React, Vue, or Svelte.
+- For iOS: a Mac with Xcode 16 or later.
+- For Android: Android Studio with at least one emulator. Android Studio
+ calls an emulator an Android Virtual Device (AVD), and you create one in its
+ **Device Manager**.
+
+You can skip the tools for a platform you don't build.
+
+## 1. Run `init`
+
+Run this in your app's root, the directory with `package.json`:
+
+```bash
+npx inertia-native init
+```
+
+It asks a few questions. Press Enter to accept each default:
+
+- **Platforms**: iOS, Android, or both.
+- **App name**: the name under the app icon. The default comes from your
+ directory name.
+- **Bundle ID**: the app's unique ID, written like a reversed domain name
+ (`com.acme.shop`). The App Store and Google Play use it to tell apps apart.
+ The default `com.example.…` is fine for trying things out.
+- **Dev server URL**: the address the app loads. Use the same URL you open in
+ your browser during development.
+- **Vite host**, if you build for Android: whether `init` may change one line
+ of your Vite config so that Android can load your scripts.
+ [Step 2](#_2-start-your-dev-server) explains why.
+
+Then it changes your project. The output looks like this, with the defaults
+accepted:
+
+::: code-group
+
+```text [Laravel]
+$ npx inertia-native init
+? Platforms [both/ios/android] (both) ›
+✓ Found entrypoint resources/js/app.tsx
+? App name (Acme Shop) ›
+? Bundle ID (com.example.acmeshop) ›
+? Dev server URL (http://localhost:8000) ›
+? Set server.host to 127.0.0.1 in vite.config.ts, so Android can load scripts from Vite? (Y/n) ›
+› npm install inertia-native
+✓ Added inertia-native to package.json (npm)
+✓ Patched resources/js/app.tsx
+✓ Set server.host to 127.0.0.1 in vite.config.ts
+✓ Created ios/ and android/
+✓ Added "ios" and "android" scripts to package.json
+
+Next: start your dev server (composer run dev), then run: npm run ios (or npm run android)
+```
+
+```text [Rails]
+$ npx inertia-native init
+? Platforms [both/ios/android] (both) ›
+✓ Found entrypoint app/javascript/entrypoints/inertia.tsx
+? App name (Acme Shop) ›
+? Bundle ID (com.example.acmeshop) ›
+? Dev server URL (http://localhost:3000) ›
+? Set server.host to 127.0.0.1 in vite.config.ts, so Android can load scripts from Vite? (Y/n) ›
+› npm install inertia-native
+✓ Added inertia-native to package.json (npm)
+✓ Patched app/javascript/entrypoints/inertia.tsx
+✓ Set server.host to 127.0.0.1 in vite.config.ts
+✓ Created ios/ and android/
+✓ Added "ios" and "android" scripts to package.json
+
+Next: start your dev server (bin/dev), then run: npm run ios (or npm run android)
+```
+
+:::
+
+`init` finds your Inertia entrypoint by looking for `createInertiaApp(` in the
+usual places. It picks `http://localhost:8000` when it finds `artisan` and
+`http://localhost:3000` otherwise. If your app runs somewhere else, type that
+URL at the prompt.
+
+`init` uses the package manager your lockfile belongs to, so its hints may say
+`pnpm run ios` or `yarn run ios` instead of `npm run ios`.
+
+`init` is safe to run again: it skips anything that's already done. See
+[What `init` did](#what-init-did) for the details, or
+[`init` options](#init-options) to run it without questions.
+
+## 2. Start your dev server
+
+If you build for Android, `init` asked to change one line of your Vite config.
+The Laravel and Rails starter kits serve scripts from `http://[::1]:5173`, an
+IPv6 address that Android can't reach. The change makes Vite listen on
+`127.0.0.1` instead:
+
+::: code-group
+
+```ts [Laravel]
+// vite.config.ts
+export default defineConfig({
+ // …
+ server: {
+ host: '127.0.0.1', // [!code ++]
+ // …
+ },
+})
+```
+
+```ts [Rails]
+// vite.config.ts
+export default defineConfig(({ command }) => ({
+ server: { host: "127.0.0.1" }, // [!code ++]
+ // …
+}))
+```
+
+:::
+
+If you answered no, or `init` couldn't change your config safely, make the
+change by hand. `init` printed the line to add, and `npm run android` checks
+for it and prints the line again if it's missing. iOS works either way.
+
+Rails apps on `vite_rails` (the ones with `config/vite.json`) don't need this
+change, so `init` doesn't ask. Rails serves their scripts from its own
+address.
+
+Now start your app the way you always do:
+
+::: code-group
+
+```bash [Laravel]
+composer run dev
+```
+
+```bash [Rails]
+bin/dev
+```
+
+:::
+
+Open the dev server URL in your browser to check that the app is up. The
+native app loads the same URL, so it needs the server running.
+
+## 3. Run the app
+
+In a second terminal, run the platform you want:
+
+```bash
+npm run ios
+npm run android
+```
+
+`npm run ios` builds the app, installs it on the simulator that's running, and
+opens it. `npm run android` does the same on the emulator or phone that's
+connected. On Android it also runs `adb reverse` for your dev server's ports,
+so `localhost` on the device reaches your computer. The first build downloads
+Hotwire Native and the other dependencies, so it takes longer than the ones
+after it.
+
+If nothing is running, `npm run ios` asks which iPhone simulator to start,
+with the newest one as the default. `npm run android` starts your emulator, or asks which
+one when you have several. If more than one simulator or device is running,
+it asks which one to use. To see them all, add `--list`:
+
+```bash
+npm run ios -- --list
+npm run android -- --list
+```
+
+You should see your home page on a native screen. Try these:
+
+- Tap a link. A new screen slides in, with a native back button.
+- Go back with the back button or a swipe from the left edge.
+- Open a page whose URL ends in `/new` or `/edit`. It opens as a modal: a
+ screen that slides up from the bottom.
+- Pull the page down to reload it.
+
+If Android shows **Error loading page**, see
+[the checklist](/native/android#error-loading-page-on-the-emulator).
+
+::: tip Laravel starter kit: stuck on "Email verification"?
+The starter kit asks new users to verify their email. In development it
+doesn't send the email: it writes the link to `storage/logs/laravel.log`, and
+the app has no address bar to open it. Use an account that's already verified
+instead. `php artisan db:seed` creates `test@example.com` with the password
+`password`. To verify an account you've already registered, run this and then
+pull down to reload the page:
+
+```bash
+php artisan tinker --execute="App\Models\User::where('email', 'you@example.com')->update(['email_verified_at' => now()])"
+```
+
+:::
+
+To run on your own phone, see [iOS](/native/ios#run-on-your-iphone) and
+[Android](/native/android#run-on-your-phone).
+
+## 4. Make it feel native
+
+Your app runs, but it still shows web parts that a native app doesn't need,
+such as your site's top navigation bar. [Feel native](/guide/feel-native)
+shows how to hide them, close a modal after a form, and add a native button.
+
+## What `init` did
+
+`init` made up to five changes. You can make them by hand instead if you
+prefer to see each step.
+
+### The package and two lines of JavaScript
+
+`init` added the `inertia-native` package with your package manager, then
+added two lines to your Inertia entrypoint:
+
+::: code-group
+
+```js [Laravel]
+// resources/js/app.tsx (or app.ts, app.js)
+import { createInertiaApp } from '@inertiajs/react'
+import { initInertiaNative } from 'inertia-native' // [!code ++]
+
+initInertiaNative() // [!code ++]
+
+createInertiaApp({
+ // …
+})
+```
+
+```js [Rails]
+// app/javascript/entrypoints/inertia.tsx (or app/frontend/entrypoints/…)
+import { createInertiaApp } from '@inertiajs/react'
+import { initInertiaNative } from 'inertia-native' // [!code ++]
+
+initInertiaNative() // [!code ++]
+
+createInertiaApp({
+ // …
+})
+```
+
+:::
+
+The call must come before `createInertiaApp`. In a regular browser it does
+nothing, so your website keeps working as before. The
+[installation page](/guide/installation) covers the options it takes.
+
+If `init` can't find your entrypoint, or finds a file it can't safely change,
+it prints these two lines for you to add yourself.
+
+### The Vite host
+
+If you build for Android and answered yes, `init` set `server.host` to
+`127.0.0.1` in your Vite config, as shown in
+[step 2](#_2-start-your-dev-server). It adds the line to your `server` options,
+or adds `server` if your config doesn't have it, and keeps your quotes and
+indentation.
+
+`init` leaves the file alone when it already sets `server.host`, or when it
+can't tell where the line goes, for example when `defineConfig` gets a function
+with a body. Then it prints the line for you to add.
+
+### The native projects
+
+`init` created two native projects next to your app code:
+
+- `ios/`: an Xcode project. To run it from Xcode instead of the script, run
+ `npx inertia-native open ios`, pick a simulator at the top of the window,
+ and press **Run**.
+- `android/`: a Gradle project. To run it from Android Studio instead of the
+ script, run `npx inertia-native open android`, pick an emulator, and press
+ **Run**. You also need to
+ [forward your dev server's ports](/native/android#run-from-android-studio)
+ once per emulator boot. `open android` prints the commands for it.
+
+Both are small and readable. [iOS](/native/ios) and [Android](/native/android)
+explain each file and how to change the name, bundle ID, and URL later.
+
+### The scripts
+
+`init` added two scripts to `package.json`, unless you already had scripts
+with those names:
+
+```json
+{
+ "scripts": {
+ "ios": "inertia-native run ios",
+ "android": "inertia-native run android"
+ }
+}
+```
+
+`run` warns you if your dev server doesn't answer, and tells you how to fix a
+missing Xcode, Android SDK, or JDK. The native pages list its options.
+
+## `init` options
+
+Pass flags to skip the questions, for example in a script:
+
+```bash
+npx inertia-native init ios --name "Acme Shop" --bundle-id com.acme.shop --url http://localhost:8000
+```
+
+| Option | What it does |
+| --- | --- |
+| `ios`, `android`, `both` | Which native projects to create. Default: ask, or both. |
+| `--name` | Name under the app icon. Default: your directory name, title-cased (`acme-shop` becomes `Acme Shop`). |
+| `--bundle-id` | iOS bundle ID and Android application ID. Default: `com.example.` plus the name in lowercase letters and digits. |
+| `--url` | URL the app loads. Default: `http://localhost:8000` if `artisan` exists, otherwise `http://localhost:3000`. |
+| `--entrypoint` | Path to your Inertia entrypoint, if `init` picks the wrong file. |
+| `--skip-install` | Don't add the npm package. |
+| `--force` | Replace existing `ios/` and `android/` directories. This deletes changes you made in them. |
+| `--yes` | Accept every default without asking, including the Vite host change for Android. |
+
+`init` only asks questions in an interactive terminal. Elsewhere it uses the
+flags and defaults.
+
+## Next steps
+
+- [Feel native](/guide/feel-native): hide web-only parts, close modals after
+ forms, and add native buttons.
+- [How it works](/guide/how-it-works): what the native app does and what your
+ web app does.
diff --git a/docs/index.md b/docs/index.md
index fdd3cc7..1e36bf7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -10,8 +10,8 @@ hero:
text: Get started
link: /guide/introduction
- theme: alt
- text: Installation
- link: /guide/installation
+ text: Quick start
+ link: /guide/quick-start
- theme: alt
text: GitHub
link: https://github.com/inertia-native/inertia-native
diff --git a/docs/native/android.md b/docs/native/android.md
index cf91fa0..fea2da5 100644
--- a/docs/native/android.md
+++ b/docs/native/android.md
@@ -1,25 +1,379 @@
# Android
-The native Android shell is a standard [Hotwire Native Android](https://native.hotwired.dev/android/overview)
-app that loads your Inertia application in a web view. The package handles the
-web side; the Android app provides the navigation stack, path configuration, and
-any bridge components.
-
-::: tip Reference app
-A runnable example lives in
-[demo-android](https://github.com/inertia-native/demo-android).
-Clone it to see a working setup end to end.
+`npx inertia-native init` creates an `android/` directory with a small Hotwire
+Native Android app. This page explains its files, how to run it on an emulator
+and on your phone, and how to change it later. If you haven't run `init` yet,
+start with the [quick start](/guide/quick-start).
+
+## Requirements
+
+You need Android Studio to build and run the app:
+
+- Android Studio with at least one emulator. Create one in its **Device
+ Manager**. Android Studio also installs the Android SDK and comes with a JDK.
+- The app runs on Android 9 (API level 28) and later.
+- Hotwire Native Android 1.3.1. Gradle downloads it on the first build.
+
+## What `init` creates
+
+The project is plain Kotlin with no generated code, so you can read and change
+every file:
+
+```text
+android/
+├── app/
+│ ├── build.gradle.kts app ID, the URL the app loads, SDK versions
+│ └── src/
+│ ├── main/
+│ │ ├── AndroidManifest.xml
+│ │ ├── assets/json/
+│ │ │ └── path-configuration.json bundled navigation rules
+│ │ ├── kotlin/dev/inertianative/app/
+│ │ │ ├── MainApplication.kt Hotwire Native setup: path
+│ │ │ │ configuration, bridge components
+│ │ │ ├── MainActivity.kt opens your URL on launch
+│ │ │ ├── WebFragment.kt the screen that shows a web page
+│ │ │ ├── Urls.kt the URL the app starts at
+│ │ │ └── bridge/ native halves of the alert, button,
+│ │ │ form, menu, and overflow-menu components
+│ │ └── res/values/strings.xml app name
+│ └── debug/res/xml/
+│ └── network_security_config.xml plain HTTP to your computer,
+│ debug builds only
+├── gradle/libs.versions.toml dependency versions, including Hotwire Native
+└── gradlew Gradle wrapper
+```
+
+The Kotlin package stays `dev.inertianative.app` in every app. Your app's
+identity is its application ID, which `init` sets from your bundle ID. You
+don't need to rename the package.
+
+`WebFragment.kt` adds a spinner to the toolbar while a form submits.
+
+`Urls.kt` adds a trailing `/` to a URL without a path, so the app starts at
+`http://localhost:8000/`. That's the form the web view reports, and starting
+there avoids an extra redirect on launch.
+
+## Run on the emulator
+
+Run this from your app's root, with your dev server running:
+
+```bash
+npm run android
+```
+
+It builds and installs a debug build with Gradle, forwards your dev server's
+ports to the device, and launches your app. It warns you if your dev server
+doesn't answer.
+
+It picks the device this way:
+
+- **One device or emulator is connected**: it uses that one.
+- **Several are connected**: it asks which one. Outside an interactive
+ terminal, for example in a script, it stops and lists them instead.
+- **None is connected**: it starts your emulator and waits for it to boot. If
+ you have several emulators, it asks which one to start, or stops and lists
+ them outside an interactive terminal.
+
+To pick one yourself, pass an option after `--`:
+
+```bash
+npm run android -- --avd Pixel_9
+```
+
+| Option | What it does |
+| --- | --- |
+| `--avd ` | The emulator to use, by its AVD name. It's started if needed. |
+| `--device ` | A connected device or running emulator to use, by its serial, such as `emulator-5554`. |
+| `--list` | List the connected devices and your emulators, then exit. |
+
+### How the device reaches your computer
+
+Inside the emulator, `localhost` is the emulator itself, and the app loads your
+URL as it is. `npm run android` fixes this with `adb reverse`, which forwards a
+port on the device to the same port on your computer. It forwards your app's
+port and the Vite dev server's port:
+
+```text
+✓ adb reverse tcp:8000, tcp:5173 (localhost on emulator-5554 reaches this machine)
+```
+
+After that, `http://localhost:8000` in the app reaches your dev server, and so
+do the scripts that Vite serves at `http://127.0.0.1:5173`. The forward lasts
+until the emulator shuts down.
+
+Vite must listen on `127.0.0.1` for this to work. When it listens on the IPv6
+address `[::1]`, the page loads its scripts from `http://[::1]:5173`, which
+the device can't reach. `init` offers to make the one-line Vite config change
+that [Quick start](/guide/quick-start#_2-start-your-dev-server) shows, and
+`npm run android` warns you when it's missing.
+
+Android blocks plain HTTP by default. In debug builds,
+`network_security_config.xml` allows it for `localhost`, `127.0.0.1`, and
+`10.0.2.2` only. Release builds keep Android's default and need HTTPS.
+
+### Run from Android Studio
+
+You can also run from Android Studio. Run this to open the `android/`
+directory in it:
+
+```bash
+npx inertia-native open android
+```
+
+Wait for Gradle to finish syncing, choose an emulator in the toolbar, and
+press **Run**. On Linux, `open` needs Android Studio's `studio` or `studio.sh`
+launcher on your `PATH`; otherwise it prints the directory to open.
+
+Android Studio doesn't forward ports, so `open android` prints the `adb reverse`
+commands for your app's ports. Once per emulator boot, either run
+`npm run android`, or run those commands. `adb` is in the Android SDK's
+`platform-tools` directory:
+
+::: code-group
+
+```bash [Laravel]
+adb reverse tcp:8000 tcp:8000
+adb reverse tcp:5173 tcp:5173
+```
+
+```bash [Rails]
+adb reverse tcp:3000 tcp:3000
+adb reverse tcp:5173 tcp:5173
+```
+
:::
-## Outline
+If your Rails app uses `vite_rails`, its dev server port is in
+`config/vite.json`, and it's `3036` by default.
+
+### "Error loading page" on the emulator
+
+The app shows this screen when your page doesn't load. Check these, in order:
+
+1. Your dev server is running, and the URL works in your browser.
+2. The ports are forwarded. Run `npm run android` again, or the `adb reverse`
+ commands above.
+3. Vite listens on `127.0.0.1`. If `npm run android` printed a warning about
+ `[::1]`, add the line it shows to your Vite config and restart the dev
+ server.
+
+To look closer, open `chrome://inspect` in Chrome on your computer, find the
+app's web view, and check its console and network requests. Debug builds make
+the web view inspectable.
+
+## Run on your phone
+
+A phone connected over USB works like the emulator, including the port
+forwarding. Turn on USB debugging first:
+
+1. On the phone, open **Settings** > **About phone** and tap **Build number**
+ seven times. This turns on **Developer options**.
+2. In **Developer options**, turn on **USB debugging**.
+3. Connect the phone with a cable and accept the prompt on the phone.
-1. Create a Hotwire Native Android app (Gradle).
-2. Point its navigation at your app's URL.
-3. Add a `path-configuration` JSON to control which routes present as modals,
- pushes, etc.
-4. Register the bridge components your web pages use (`form`, `menu`, …) if any.
+Then run `npm run android`. It installs the app on the phone and forwards the
+ports, so `localhost` works without changes. If an emulator is running too, it
+asks which one to use. You can also pick the phone with `--device` and the
+serial that `npm run android -- --list` prints. Unplugging the phone ends the
+forward, so run the command again after you reconnect.
+
+### Over Wi-Fi instead
+
+Without a cable, the phone needs your computer's address on your Wi-Fi network,
+for example `192.168.1.20`. On a Mac, run `ipconfig getifaddr en0` to find it.
+
+1. Set the URL in `android/app/build.gradle.kts`:
+
+ ```kotlin
+ buildConfigField("String", "BASE_URL", "\"http://192.168.1.20:8000\"")
+ ```
+
+2. Allow plain HTTP to that address in
+ `android/app/src/debug/res/xml/network_security_config.xml`. Without this
+ line, Android blocks the request:
+
+ ```xml
+ 192.168.1.20
+ ```
+
+3. Start your server so it accepts connections from the network. Vite's
+ `127.0.0.1` address isn't reachable over Wi-Fi, so run your app without
+ the Vite dev server:
+
+ ::: code-group
+
+ ```bash [Laravel]
+ npm run build
+ php artisan serve --host=0.0.0.0
+ ```
+
+ ```bash [Rails]
+ # Rails builds the assets when the Vite dev server isn't running.
+ bin/rails server -b 0.0.0.0
+ ```
+
+ :::
+
+## Change the name, bundle ID, or URL
+
+`init` wrote your answers into a few places. Change them there:
+
+| What | Where |
+| --- | --- |
+| Name under the icon | `app_name` in `android/app/src/main/res/values/strings.xml` |
+| Application ID (bundle ID) | `applicationId` in `android/app/build.gradle.kts` |
+| URL the app loads | `BASE_URL` in `android/app/build.gradle.kts` |
+
+`init` also put the name in `android/settings.gradle.kts`. That's only the
+Gradle project's name, and users don't see it.
+
+Before you ship, point release builds at your production site. Set the URL per
+build type in `android/app/build.gradle.kts`. A value in a build type wins over
+the one in `defaultConfig`:
+
+```kotlin
+buildTypes {
+ debug {
+ buildConfigField("String", "BASE_URL", "\"http://localhost:8000\"")
+ }
+ release {
+ buildConfigField("String", "BASE_URL", "\"https://acme.example\"")
+ // keep the existing release settings
+ }
+}
+```
+
+You can also run `npx inertia-native init android --force` with new values. It
+recreates `android/` from scratch, so it deletes any changes you made there.
+
+## Path configuration
+
+Path configuration is the set of rules that decides how each URL opens, for
+example as a modal. [How it works](/guide/how-it-works#path-configuration)
+explains the format.
+
+The app reads it from two places:
+
+1. `android/app/src/main/assets/json/path-configuration.json`, bundled in the
+ app.
+2. `/inertia-native/path-configuration/android_v1.json` on your server, if you
+ serve it.
+
+When the app starts, it applies the copy it saved from your server last time,
+or the bundled file if it has no saved copy. Then it downloads a fresh copy. A
+downloaded copy replaces the bundled rules completely. If the download fails,
+for example with a 404, the app keeps what it has.
+
+The Android file starts with a rule for every URL (`.*`). It tells the app to
+show web pages in `WebFragment`, with pull to refresh turned on. Keep that rule
+first.
+
+### Serve it from your app
+
+Serving the file lets you change navigation with a deploy instead of an app
+update. Add a route that returns the same JSON as the bundled file:
+
+::: code-group
+
+```php [Laravel]
+// routes/web.php
+Route::get('/inertia-native/path-configuration/android_v1.json', fn () => [
+ 'rules' => [
+ [
+ 'patterns' => ['.*'],
+ 'properties' => [
+ 'context' => 'default',
+ 'uri' => 'hotwire://fragment/web',
+ 'fallback_uri' => 'hotwire://fragment/web',
+ 'pull_to_refresh_enabled' => true,
+ ],
+ ],
+ [
+ 'patterns' => ['/new$', '/edit$'],
+ 'properties' => ['context' => 'modal', 'pull_to_refresh_enabled' => false],
+ ],
+ [
+ 'patterns' => ['^/$'],
+ 'properties' => ['presentation' => 'clear_all'],
+ ],
+ ],
+]);
+```
+
+```ruby [Rails]
+# config/routes.rb
+get "inertia-native/path-configuration/android_v1", to: "path_configurations#android"
+
+# app/controllers/path_configurations_controller.rb
+# ActionController::Base skips your app-wide filters, such as sign-in checks.
+class PathConfigurationsController < ActionController::Base
+ def android
+ render json: {
+ settings: {},
+ rules: [
+ {
+ patterns: [".*"],
+ properties: {
+ context: "default",
+ uri: "hotwire://fragment/web",
+ fallback_uri: "hotwire://fragment/web",
+ pull_to_refresh_enabled: true
+ }
+ },
+ { patterns: ["/new$", "/edit$"], properties: { context: "modal", pull_to_refresh_enabled: false } },
+ { patterns: ["^/$"], properties: { presentation: "clear_all" } }
+ ]
+ }
+ end
+end
+```
-::: info Work in progress
-A full step-by-step walkthrough is being written. For now, the demo app is the
-source of truth.
:::
+
+If you also serve the iOS file, add this action to the same controller. The
+[iOS page](/native/ios#serve-it-from-your-app) has the other half.
+
+Keep these points in mind:
+
+- **Include every rule.** The server copy replaces the bundled file, so start
+ from a copy of it.
+- **Keep the route public.** The app downloads the file on launch, before
+ anyone signs in.
+- **Leave out `settings` in PHP.** PHP turns an empty array into `[]` instead
+ of `{}`, and Android can't read `[]` there. The key is optional, so skip it
+ unless you have settings to send.
+- **Relaunch to see changes.** The app downloads the file when it starts.
+
+## Bridge components
+
+The native halves of bridge components live in
+`android/app/src/main/kotlin/dev/inertianative/app/bridge/`, and
+`MainApplication.kt` registers them at launch:
+
+```kotlin
+Hotwire.registerBridgeComponents(
+ BridgeComponentFactory("alert", ::AlertComponent),
+ BridgeComponentFactory("button", ::ButtonComponent),
+ BridgeComponentFactory("form", ::FormComponent),
+ BridgeComponentFactory("menu", ::MenuComponent),
+ BridgeComponentFactory("overflow-menu", ::OverflowMenuComponent)
+)
+```
+
+To add a component, put its Kotlin file in the `bridge/` directory and add it
+to this list. Set the file's first line to
+`package dev.inertianative.app.bridge`. If it uses resources, import
+`dev.inertianative.app.R`. Components from the
+[registry](https://github.com/inertia-native/bridge-components) mark both
+lines for you to replace. See [Bridge components](/components/overview) for
+the web side.
+
+## Debugging
+
+Debug builds make the web view inspectable. Open `chrome://inspect` in Chrome
+on your computer and pick the app's web view. Hotwire Native's own logs show up
+in Android Studio's **Logcat** in debug builds.
+
+To log the messages between your page and the app, turn on `debug` in your
+entrypoint. See [Debugging](/guide/navigation#debugging).
diff --git a/docs/native/ios.md b/docs/native/ios.md
index 00e74ad..9f75e41 100644
--- a/docs/native/ios.md
+++ b/docs/native/ios.md
@@ -1,44 +1,259 @@
# iOS
-The native iOS shell is a standard [Hotwire Native iOS](https://native.hotwired.dev/ios/overview)
-app that loads your Inertia application in a web view. The package handles the
-web side; the iOS app provides the navigation stack, path configuration, and any
-bridge components.
-
-::: tip Reference app
-A runnable example lives in
-[demo-ios](https://github.com/inertia-native/demo-ios).
-Clone it to see a working setup end to end.
+`npx inertia-native init` creates an `ios/` directory with a small Hotwire
+Native iOS app. This page explains its files, how to run it on a simulator and
+on your iPhone, and how to change it later. If you haven't run `init` yet,
+start with the [quick start](/guide/quick-start).
+
+## Requirements
+
+You need a Mac with Xcode to build the app:
+
+- Xcode 16 or later.
+- The app runs on iOS 17 and later.
+- Hotwire Native iOS 1.3.1. Xcode downloads it with Swift Package Manager on
+ the first build.
+
+## What `init` creates
+
+The project is plain Swift with no generated code, so you can read and change
+every file:
+
+```text
+ios/
+├── App.xcodeproj Xcode project: target "App", scheme "App"
+└── App/
+ ├── AppConfig.swift the URL the app loads
+ ├── AppDelegate.swift Hotwire Native setup: path configuration,
+ │ bridge components, options
+ ├── SceneDelegate.swift opens your URL on launch, handles failed pages
+ ├── path-configuration.json bundled navigation rules
+ ├── Bridge/ native halves of the alert, button, form,
+ │ menu, and overflow-menu components
+ ├── Assets.xcassets app icon and accent color
+ └── Info.plist allows plain HTTP to local network addresses
+```
+
+`AppDelegate.swift` also sets two options: the back button shows only an
+arrow, and every modal gets a **Done** button.
+
+## Run on the simulator
+
+Run this from your app's root, with your dev server running:
+
+```bash
+npm run ios
+```
+
+It builds the app into `ios/build`, opens the Simulator app, then installs and
+launches your app. It warns you if your dev server doesn't answer.
+
+It picks the simulator this way:
+
+- **One simulator is running**: it uses that one.
+- **Several are running**: it asks which one. Outside an interactive
+ terminal, for example in a script, it stops and lists them instead.
+- **None is running**: it asks which iPhone simulator to start, with the
+ newest one selected. Outside an interactive terminal, it starts the newest
+ iPhone.
+
+To pick a simulator yourself, pass its name or UDID after `--`:
+
+```bash
+npm run ios -- --device "iPhone 17 Pro"
+```
+
+| Option | What it does |
+| --- | --- |
+| `--device ` | The simulator to use, by name or UDID. It's started if needed. |
+| `--list` | List the running and available simulators, then exit. |
+
+You can also run from Xcode. Run `npx inertia-native open ios` to open
+`ios/App.xcodeproj`, choose a simulator in the run destination menu at the top
+of the window, and press **Run** (⌘R).
+
+The simulator shares your Mac's network, so `http://localhost:8000` or
+`http://localhost:3000` reaches your dev server without changes.
+
+## Run on your iPhone
+
+`npm run ios` only targets simulators, so use Xcode for a real device. Follow
+these steps:
+
+1. Connect your iPhone to your Mac with a cable.
+2. Open the project in Xcode with `npx inertia-native open ios`.
+3. Select the **App** target, open **Signing & Capabilities**, and choose your
+ **Team**. The project ships without a team, so Xcode can't sign the app
+ until you pick one. If Xcode says the bundle ID isn't available, change it
+ to one that's yours, like `com.yourname.acmeshop`.
+4. On the iPhone, turn on **Developer Mode** in **Settings** > **Privacy &
+ Security**.
+5. Point the app at your Mac and start your server for the network, as
+ described below.
+6. Choose your iPhone in the run destination menu and press **Run**.
+
+On the phone, `localhost` means the phone itself. Use your Mac's address on
+your Wi-Fi network instead, for example `http://192.168.1.20:8000`. Find it in
+**System Settings** > **Wi-Fi** > **Details**, or run
+`ipconfig getifaddr en0`. Put it in `ios/App/AppConfig.swift`:
+
+```swift
+static let baseURL = URL(string: "http://192.168.1.20:8000")!
+```
+
+The app's `Info.plist` sets `NSAllowsLocalNetworking`, which lets iOS load
+plain HTTP from local network addresses like this one.
+
+Your server must accept connections from the network, and the page's scripts
+must come from an address the phone can reach. The Laravel and Rails starter
+kits load scripts from the Vite dev server at a local address, so the page
+won't load on the phone. The reliable option is to run your app without the
+Vite dev server:
+
+::: code-group
+
+```bash [Laravel]
+npm run build
+php artisan serve --host=0.0.0.0
+```
+
+```bash [Rails]
+# Rails builds the assets when the Vite dev server isn't running.
+bin/rails server -b 0.0.0.0
+```
+
+:::
+
+## Change the name, bundle ID, or URL
+
+`init` wrote your answers into a few places. Change them there:
+
+| What | Where |
+| --- | --- |
+| Name under the icon | Xcode: **App** target > **General** > **Display Name** |
+| Bundle ID | Xcode: **App** target > **General** > **Bundle Identifier** |
+| URL the app loads | `baseURL` in `ios/App/AppConfig.swift` |
+
+Xcode saves the name and bundle ID in `ios/App.xcodeproj/project.pbxproj`. The
+project, target, and scheme stay named `App`.
+
+Before you ship, point release builds at your production site. Debug builds
+can keep your dev server:
+
+```swift
+// ios/App/AppConfig.swift
+#if DEBUG
+static let baseURL = URL(string: "http://localhost:8000")!
+#else
+static let baseURL = URL(string: "https://acme.example")!
+#endif
+```
+
+You can also run `npx inertia-native init ios --force` with new values. It
+recreates `ios/` from scratch, so it deletes any changes you made there.
+
+## Path configuration
+
+Path configuration is the set of rules that decides how each URL opens, for
+example as a modal. [How it works](/guide/how-it-works#path-configuration)
+explains the format.
+
+The app reads it from two places:
+
+1. `ios/App/path-configuration.json`, bundled in the app.
+2. `/inertia-native/path-configuration/ios_v1.json` on your server, if you
+ serve it.
+
+When the app starts, it applies the bundled file, then the copy it saved from
+your server last time, then downloads a fresh copy. A downloaded copy replaces
+the bundled rules completely. If the download fails, for example with a 404,
+the app keeps what it has.
+
+### Serve it from your app
+
+Serving the file lets you change navigation with a deploy instead of an app
+update. Add a route that returns the same JSON as the bundled file:
+
+::: code-group
+
+```php [Laravel]
+// routes/web.php
+Route::get('/inertia-native/path-configuration/ios_v1.json', fn () => [
+ 'rules' => [
+ [
+ 'patterns' => ['/new$', '/edit$'],
+ 'properties' => ['context' => 'modal', 'pull_to_refresh_enabled' => false],
+ ],
+ [
+ 'patterns' => ['^/$'],
+ 'properties' => ['presentation' => 'clear_all'],
+ ],
+ ],
+]);
+```
+
+```ruby [Rails]
+# config/routes.rb
+get "inertia-native/path-configuration/ios_v1", to: "path_configurations#ios"
+
+# app/controllers/path_configurations_controller.rb
+# ActionController::Base skips your app-wide filters, such as sign-in checks.
+class PathConfigurationsController < ActionController::Base
+ def ios
+ render json: {
+ settings: {},
+ rules: [
+ { patterns: ["/new$", "/edit$"], properties: { context: "modal", pull_to_refresh_enabled: false } },
+ { patterns: ["^/$"], properties: { presentation: "clear_all" } }
+ ]
+ }
+ end
+end
+```
+
:::
-## Outline
+Keep these points in mind:
-1. Create a Hotwire Native iOS app and add the SDK via Swift Package Manager.
-2. Point its `Navigator` at your app's URL.
-3. Add a `path-configuration` JSON to control which routes present as modals,
- pushes, etc.
-4. Register the bridge components your web pages use (`button`, `form`, `menu`,
- …) if any.
+- **Include every rule.** The server copy replaces the bundled file, so start
+ from a copy of it.
+- **Keep the route public.** The app downloads the file on launch, before
+ anyone signs in.
+- **Leave out `settings` in PHP.** PHP turns an empty array into `[]` instead
+ of `{}`. The key is optional, so skip it unless you have settings to send.
+- **Relaunch to see changes.** The app downloads the file when it starts.
-## Registering bridge components
+## Bridge components
-Each bridge component your web pages use needs a Swift counterpart registered at
-launch, otherwise the web side falls back to its ordinary markup:
+The native halves of bridge components live in `ios/App/Bridge/`, and
+`AppDelegate.swift` registers them at launch:
```swift
Hotwire.registerBridgeComponents([
+ AlertComponent.self,
ButtonComponent.self,
FormComponent.self,
MenuComponent.self,
+ OverflowMenuComponent.self,
])
```
-Ready-made components — Swift source plus the matching web half — live in the
-[bridge-components](https://github.com/inertia-native/bridge-components)
-registry. See [Button](/components/button) for a worked example, including the
-minimum SDK version they require.
+To add a component, put its Swift file in `ios/App/Bridge/` and add it to this
+list. Xcode picks up new files in the `App` folder without extra steps. See
+[Bridge components](/components/overview) for the web side.
-::: info Work in progress
-A full step-by-step walkthrough is being written. For now, the demo app is the
-source of truth.
-:::
+## Debugging
+
+Debug builds make the web view inspectable. Open Safari on your Mac, turn on
+**Show features for web developers** in **Settings** > **Advanced**, then pick
+the simulator or iPhone from the **Develop** menu. Hotwire Native's own logs
+show up in Xcode's console in debug builds.
+
+To log the messages between your page and the app, turn on `debug` in your
+entrypoint. See [Debugging](/guide/navigation#debugging).
+
+## Handle failed pages
+
+When a page fails to load, `SceneDelegate.swift` shows Hotwire Native's error
+screen with a **Retry** button. To handle a case yourself, such as sending a
+401 to your sign-in page, edit `visitableDidFailRequest` there.
diff --git a/docs/reference/api.md b/docs/reference/api.md
index 0591f7e..ff23638 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -104,4 +104,4 @@ The package installs and reads these on `window`:
| --- | --- | --- |
| `window.Turbo` | `initInertiaNative()` | Shim that `turbo.js` drives for navigation. Internal. |
| `window.HotwireNative.web` | `initInertiaNative()` | Web side of the bridge (`send`, `supportsComponent`, `removeCallback`, …). The name is Hotwire Native's bridge protocol. |
-| `window.webkit.messageHandlers.turbo` | WKWebView (iOS) | Feature-detect that the app runs inside the native shell. |
+| `window.webkit.messageHandlers.turbo` | WKWebView (iOS) | Hotwire Native's message channel on iOS. Android has no `window.webkit`, so don't use it to detect the app: check the user agent for `Hotwire Native` instead (see [Feel native](/guide/feel-native#tell-the-app-apart-from-a-browser)). |