Ruby Native reuses your existing sign in and registration screens in the native app. There's no need to build separate native auth flows. Your web forms, validation, error messages, and forgot password pages all work as-is.
There are two things to get right: keeping sessions alive so native users aren't logged out unexpectedly, and telling the app who is signed in so signing out clears their screens.
If your app uses OAuth (Sign in with Google, GitHub, etc.), see the OAuth guide.
If you're using the built-in Rails authentication generator, set the session cookie to permanent.
cookies.signed.permanent[:session_id] = session.id
Always remember native users by adding a hidden field to your sign-in form.
<%= form_with url: session_path do |form| %>
<% if native_app? %>
<%= form.hidden_field :remember_me, value: true %>
<% else %>
<%= form.check_box :remember_me %>
<%= form.label :remember_me %>
<% end %>
<% end %>
The native app keeps screens alive between page loads. When signing out, other tabs need their context cleared. Tell the app who is signed in with native_identity_tag, and it will clear everything when they sign out or switch accounts.
Render the tag on every page, even when the user is signed out:
<%= native_identity_tag current_user&.id %>
The value rendered with this tag is hashed using your app's secret_key_base, so it won't expose any information.
Pass an array to widen the identity boundary:
<%= native_identity_tag [current_user&.id, current_team&.id] %>
Use this when switching teams should clear the screens, not just re-render the current one.
The app resets when the identity is removed or changes: signing out or switching accounts. Signing in never resets, so a login reached deep in a checkout flow keeps its place.
A page that renders no tag at all changes nothing in either direction, so a template that misses the layout can never sign anyone out by accident.
The token is computed server-side so secret_key_base never reaches the client. Share it as a prop alongside your other shared data:
# app/controllers/concerns/inertia_shared_data.rb
inertia_share do
{
native_identity: helpers.native_identity_token(current_user&.id),
# ...
}
end
Don't reach for the ERB helper here: Inertia visits never re-render the Rails layout, so an element placed there would keep the old token across a sign-out.
Attach the element in createInertiaApp's resolve wrapper, never only in your main layout component. Auth and landing pages opt out of layouts, and a signed-out page missing the element makes sign-out invisible to the app. The React version wraps whatever layout a page chose; the Vue version leans on Inertia's nested layout arrays to do the same.
// app/frontend/entrypoints/inertia.jsx
import { createInertiaApp } from "@inertiajs/react"
import { createRoot } from "react-dom/client"
import Layout from "~/layouts/Layout"
const pages = import.meta.glob("../pages/**/*.jsx", { eager: true })
createInertiaApp({
resolve: (name) => {
const page = pages[`../pages/${name}.jsx`]
if (!page) throw new Error(`Page not found: ${name}`)
if (!page.default.layout?.__withIdentity) {
const layout = page.default.layout || ((page) => <Layout>{page}</Layout>)
const wrapped = (page) => (
<>
<div data-native-identity={page.props.native_identity ?? ""} hidden />
{layout(page)}
</>
)
wrapped.__withIdentity = true
page.default.layout = wrapped
}
return page
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})
// app/frontend/entrypoints/inertia.js
import { createApp, h, Fragment } from "vue"
import { createInertiaApp, usePage } from "@inertiajs/vue3"
import Layout from "~/layouts/Layout.vue"
const pages = import.meta.glob("../pages/**/*.vue", { eager: true })
const IdentityLayout = {
setup(_, { slots }) {
const page = usePage()
return () => h(Fragment, [
h("div", { "data-native-identity": page.props.native_identity ?? "", hidden: true }),
slots.default ? slots.default() : null,
])
},
}
createInertiaApp({
resolve: (name) => {
const page = pages[`../pages/${name}.vue`]
if (!page) throw new Error(`Page not found: ${name}`)
const existing = page.default.layout || Layout
const layouts = Array.isArray(existing) ? existing : [existing]
if (layouts[0] !== IdentityLayout) {
page.default.layout = [IdentityLayout, ...layouts]
}
return page
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
})
NativeIdentity from @ruby-native/react and @ruby-native/vue renders the same element when a component is more convenient, but the wrapper is what guarantees every page carries one.