Skip to content

Rails + Inertia.js + Svelte

Read this first

This page is a companion to the Rails 8 guide. All server-side logic — controller, PKCE, token exchange, JWT verification — is the same as in that guide. This page covers only the three differences for an Inertia.js + Svelte frontend.

📋 Register a Confidential client

The Rails server sends client_secret to /oauth/token, so register the app as Client type: Confidential. Never expose the secret to the Svelte frontend.

In an Inertia app, page transitions are intercepted by a client-side router (@inertiajs/svelte's router, use:inertia, <Link>). 1pass login starts with a cross-origin 302 redirect — and a client-side router tries to follow that 302 over XHR and fails silently (the same failure mode as Turbo Drive). So three things change.

① Starting login — bypass the client-side router (most important)

The "Log in with 1pass" button must be a full-page navigation. Do not use use:inertia / <Link> / router.visit.

svelte
<script lang="ts">
  import { inertia } from "@inertiajs/svelte"
</script>

<!-- ✅ Start login: plain <a>. No use:inertia.
     logi's authorize is a cross-origin 302 that a client router would eat. -->
<a href="/auth/logi/start" data-no-inertia>Log in with 1pass</a>

<!-- ❌ Never: nothing happens — no error in the console or the network tab -->
<!-- <a href="/auth/logi/start" use:inertia>Log in with 1pass</a> -->

Choose the /auth/logi/start route in your app (see Login Button). The controller's redirect_to(..., allow_other_host: true) is the same as in the Rails guide.

The callback does not need special handling

/auth/logi/callback is handled by the server controller and finishes with redirect_to root_path, so Inertia renders the next page as usual. Only the start (①) needs the bypass, not the callback.

② Login state — share current_user via inertia_share

To expose the logged-in user on every Inertia page without an extra round-trip, use ApplicationController#inertia_share.

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  inertia_share do
    {
      auth: {
        user: current_user && {
          id: current_user.id,
          email: current_user.email,
          displayName: current_user.display_name
        }
      },
      flash: { notice: flash[:notice], alert: flash[:alert] }
    }
  end

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

Don't create users from front-channel data

current_user must come only from the server session (session[:user_id]). Create users only from the server-side userinfo response — see Rails guide: first login.

③ In Svelte — page is a rune, not a store (page.props)

@inertiajs/svelte v3 trap

The v2 $page store pattern breaks in v3. In v3, page is a $state rune object, so subscribing with $page fails hydration with store_invalid_shape. Read page.props directly.

svelte
<script lang="ts">
  import { inertia, page, router } from "@inertiajs/svelte"
  let { children } = $props()

  // ✅ v3: page is a rune. Not $page (store subscription).
  let user = $derived(
    (page.props as any)?.auth?.user as
      | { displayName: string; email?: string }
      | null
      | undefined
  )

  function logout() {
    router.delete("/auth/logi/logout") // end local session → full reload to root
  }
</script>

<header>
  {#if user}
    <span title={user.email}>👤 {user.displayName}</span>
    <button type="button" onclick={logout}>Log out</button>
  {:else}
    <!-- ① again: start login with a plain <a> -->
    <a href="/auth/logi/start" data-no-inertia>Log in with 1pass</a>
  {/if}
</header>

Logout is safe via router.delete (a same-origin DELETE — not a cross-origin 302, so the client router can handle it).

Checklist

  • [ ] Login start button is a plain <a href> (not use:inertia / <Link>)
  • [ ] inertia_share exposes auth.user; current_user is session-based
  • [ ] Svelte reads page.props directly, not $page
  • [ ] RP registered as Confidential; secret lives only in server env

See also

  • Rails 8 — controller, PKCE, token exchange, JWT verification (all server-side)
  • SPA + Serverless — pure SPA with no backend (Public client)
  • Login Button — button design and start-route conventions

Identity가 제품의 신뢰를 만듭니다.