DOCS LLMs

RailsFast Native Overview

RailsFast Native is the Hotwire Native layer that sits on top of the normal RailsFast Base template. If you want the shortest setup path, start with RailsFast Native Quickstart. This page gives you the mental model so the rest of the native docs make sense fast.

INFO

RailsFast Native is an optional addon layer. RailsFast Base remains fully useful as a web-only SaaS template on its own, and you can ship the web app to production long before you ever touch the mobile shells. The Android and iOS starters share the same Rails contract and differ only where the platform containers genuinely need to.

Building a web-only product? There is nothing to remove or disable. The native surface in RailsFast Base (the native/* routes, path-configuration endpoints, and bridge markup) is inert for regular browsers — every native behavior is gated on hotwire_native_app?, browsers never see native markup (this is covered by the template's test suite), and no native-only gems are involved. Web-only apps like LicenseSeat run the exact same template in production.

The most important thing to understand from the beginning is that this is not three equal apps.

The actual shape is:

  • railsfast-base: the product brain
  • railsfast-android: a thin Android shell around the Rails app
  • railsfast-ios: a thin iOS shell around the Rails app

Rails stays the source of truth for your product:

  • authentication and session
  • routes
  • HTML screens
  • onboarding rules
  • tab ownership rules
  • billing policy
  • path configuration

The native repos mostly own:

  • the app container
  • native tabs
  • native sheets and modals
  • bridge components
  • platform-specific polish

That split is the whole point of Hotwire Native. Rails keeps owning the product. The native repos provide the mobile app shell.

The Main Mental Model

If you bought RailsFast Base plus the native addons, hold this one idea in your head:

RailsFast Base decides what your app means; Android and iOS decide how that meaning is presented natively.

A few concrete examples of what that buys you:

  • Rails decides that /dashboard belongs to the home tab.
  • Rails decides that /settings, /organizations/**, /memberships/**, /invitations/**, and /billing/** all belong to the settings tab — the whole account/workspace route family stays in one navigator.
  • Rails decides that the web pricing flow stays out of the native app for now (more on that in the companion-app note below).
  • Android decides how the bottom tab bar works.
  • Android decides which fragment or sheet class presents a given route.

So when you ask yourself "where should I change this?", start with one question:

Is this a product rule or a native container detail?

If it is a product rule, it belongs in RailsFast Base. If it is a native container detail, it belongs in the Android or iOS repo.

The Three Shell States

The spine of the whole model is a tiny three-value vocabulary. Rails publishes exactly one shell state for the current page, and both native apps decode it identically:

  • auth — signed out, no tabs
  • onboarding — signed in but setup must still run, no tabs yet
  • tabs — signed in and inside the app, with native tabs

Rails decides which state the current page belongs to (via native_shell_state_for), and native reacts. The default predicate is User#native_onboarding? — a user with no organization yet is still in setup. That predicate is intentionally small: it is the RailsFast default, not a universal truth, and it is meant to be evolved per app.

IMPORTANT

The auth/onboarding-vs-tabs split is a RailsFast addition, not stock Hotwire Native. Vanilla Hotwire Native gives you a single tab-bar root with no separate auth shell — the official Hotwire Native iOS demo uses one HotwireTabBarController and nothing else. RailsFast layers the auth/onboarding shell on top so signing in and setting up don't feel trapped inside the main app tabs. If you already know Hotwire Native, this is the piece you didn't have before.

These three states map onto two native shells:

  • a signed-out / onboarding shell (auth and onboarding)
  • a signed-in tab shell (tabs)

That split is deliberate — it's the Evernote / Linear-style pattern where authentication and setup live outside the main tab bar.

The Default Flow

Here is the default RailsFast Native journey, end to end:

  1. The app cold-launches into /native/entry.
  2. Rails decides the page's shell state and the next canonical web URL.
  3. Signed-out users land on the web welcome screen (/native/auth/welcome), still inside the auth shell.
  4. Sign up and sign in open as sheets on top of that screen.
  5. On auth success, the form posts and redirects to /native/handoff, which hands the user into the right shell.
  6. Signed-in users who still need setup stay in onboarding (no tabs).
  7. Signed-in users who are "inside the app" move into the native tabs shell.
NOTE

/native/entry and /native/handoff are two different routes on purpose. Cold launch and sign-out always go through /native/entry; auth success goes to /native/handoff. Both render a deliberately tiny handoff document whose only job is to publish the shell bridge so the native side can swap to the correct root — they don't render real content, because real content would flash inside the wrong container.

What Lives Where

RailsFast Base (the product brain)

Rails owns everything that should be remotely changeable without shipping a new mobile binary:

  • native bootstrap routes (/native/entry, /native/handoff, /native/auth/welcome)
  • the path configuration endpoints under /native/configurations/...
  • the native welcome screen (web HTML)
  • the three shell states (auth, onboarding, tabs)
  • auth, session persistence, and post-auth redirect behavior
  • which tab owns which route family
  • which routes should be modal, an auth sheet, clear-all, or stay in onboarding
  • the bridge markup for shell transitions, toasts, and menus
  • optional native-push bridge markup and token registration endpoints
  • the verified app-link files (assetlinks.json, apple-app-site-association)
TIP

Path configuration is the single source of truth for routing, served per-platform. Tab policy lives in its settings.shell block — the default tab, which tabs are visible, each tab's canonical start path, and the route-family regexes. Because this is served by Rails, you can change route and tab ownership without an App Store / Play review, as long as the installed binary already knows how to render the supported tab set. That's a big reason this architecture is worth it.

RailsFast Android (a thin shell)

Android owns container details only:

  • the Hotwire Application bootstrap
  • the launcher activity and the signed-out activity
  • the signed-in activity with native bottom tabs
  • fragment and bridge-component registration
  • Android-only polish like sheet sizing and transition behavior

RailsFast iOS (a thin shell)

iOS consumes the same Rails contract and the same shell semantics — the same /native/entry, the same path configuration, the same three shell states, the same canonical URLs, the same tab route-family ownership, the same bridge contracts. Only the iOS container details differ.

Three Facts That Orient Everything

A few non-obvious facts make the rest of the docs click. Read these once now.

Auth is the same Devise cookie, not a mobile API. Native uses the same Devise session as the web app — no JWT, no separate mobile auth API. For native requests RailsFast auto-enables Devise rememberable (no visible checkbox) so app users stay signed in across relaunches; web keeps remember-me as an explicit opt-in. The native apps are Rails-driven, not API-clients-first.

WARNING

Cookie sync solves authentication continuity, not CSRF. The shared session cookie lets native screens read authenticated pages, but Rails still enforces CSRF on every unsafe method. Native write endpoints need an explicit CSRF policy — the blessed pattern is skip_forgery_protection scoped to JSON for authenticated, native-only endpoints. See RailsFast Base for Native before you wire up any native write.

Bridge output is gated by component support, not by platform. This is the most important bridge rule:

Gate bridge output by component support, not by platform.

Hotwire Native appends a bridge-components: [...] segment to the user agent once the native app registers its components. Rails reads that list and only emits a bridge tag if the component is advertised. If both iOS and Android register toast, Rails sends both the same payload — you never branch on iOS vs Android to decide whether to render a bridge.

Prefer the same HTML tree plus CSS hiding over native-only templates. RailsFast reuses the web HTML for native instead of forking it:

Prefer the same HTML tree plus CSS hiding over separate native-only templates.

Layouts publish native-aware markers (html[data-hotwire-native], the hotwire-native: / not-hotwire-native: Tailwind variants, the .hide@native utility, and the railsfast-native-shell / railsfast-native-authenticated meta tags) so one HTML tree serves both web and native. This keeps the two surfaces aligned and minimizes divergent UI.

What To Be Careful With

The native layer is opinionated for a reason. The RailsFast way is: let Rails own the product, keep the shells thin, and ship. With that in mind, a short do-not list:

  • Do not invent a parallel mobile route universe when a normal Rails route already works.
  • Do not move product rules into Kotlin or Swift just because you can — keep them in Base where they can change without a binary release.
  • Do not assume path configuration can build your whole app shell for you. It transports app-defined settings, but your native binary still has to know how to render the supported tab set.
  • Do not think of the native apps as API clients first. They are mostly Rails-driven.
  • Do not branch bridge output on platform — branch on advertised component support.
  • Do not expose the web pricing flow in native. By default RailsFast treats native as a companion app: /pricing, /subscribe, and /billing redirect back into the app shell for native requests (signed-out and onboarding users to /native/entry, signed-in users to /settings), and pending web checkout intent is discarded. Don't half-enable native billing by accident — turn it on only with a deliberate App Store / Play billing strategy.

Where To Go Next

You have the mental model. Now go build:

  • RailsFast Native Quickstart — the practical clone, configure, run, and verify sequence across Base, Android, and iOS.
  • RailsFast Base for Native — the server contract: native routes, path configuration, shell semantics, bridge contracts, auth, and the CSRF policy you'll need for writes. Read this next.
  • RailsFast Android — the thin Android shell: activities, tabs, fragments, and bridge components.
  • RailsFast iOS — the thin iOS shell: the two-shell UIKit root swap, tab catalog, and bridge components.
  • Customization Workflow — the practical "I want to change X, where does it go?" workflow across all three repos.
  • App Icons and Launch Screens — the reproducible iOS/Android icon, splash, cache-reset, and installed-device verification workflow.
  • Notifications — the opt-in push notification contract across Rails, iOS APNs, and Android FCM.

New to RailsFast itself? Start with the Quickstart and ship the web app to production first — the native layer is something you add once the product is real. Or head back to the docs home page.