supabase-rb-rb
Inertia + React starter

Troubleshooting

Most likely first-run failures with the Inertia + React starter — Vite not serving, cookie not set, typed routes stale, shadcn add fails, SSR fallback loops — and how to resolve them.

A guide to the failures you're most likely to hit on the path from git clone to a signed-in React dashboard. Each section names the symptom you'll see and the fix.

"Supabase URL not configured" on first request

The gem reads SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY, and SUPABASE_JWKS_URL from the environment. If any is missing, the first request raises a configuration error from Supabase::Rails::Middleware.

  • In dev: export the variables before running bin/dev. Rails does not auto-load .env — the kit's .env.example is for direnv, dotenv, or your process manager. The simplest fix is cp .env.example .env, fill it in, and source it through your shell tool of choice (direnv allow ., dotenv -- bin/dev, etc.).
  • In production: set the same four variables through your platform's secret manager.

Reminder: SUPABASE_JWKS_URL is not printed by supabase status — derive it from SUPABASE_URL as <SUPABASE_URL>/auth/v1/.well-known/jwks.json.

You signed up, see no error, but every refresh sends you back to /session/new. Two possible causes:

  1. You're on http:// and config.force_ssl = true (or config.assume_ssl = true) is on. The sb-session cookie is marked Secure, and the browser silently drops it on a non-HTTPS origin. For local development make sure those two are commented out in config/environments/production.rb and you're not somehow booting the app under production. Local dev runs config/environments/development.rb, which doesn't set either.
  2. secret_key_base is not stable. If Rails regenerates a key between requests (e.g. you have no config/master.key and no RAILS_MASTER_KEY env var), the cookie written by request N is undecryptable by request N+1. Check bin/rails credentials:show runs cleanly; if it errors, generate a master key with bin/rails credentials:edit and commit the resulting config/credentials.yml.enc (the key itself stays in config/master.key, which is gitignored).

Vite isn't serving (404 on /vite/@vite/client)

You opened http://localhost:3000 and the page is unstyled, the console is full of Failed to load resource: 404 on /vite/... paths.

  • bin/dev isn't running both processes. Procfile.dev runs bin/rails s -p ${PORT:-3000} and npx vite. If you started Rails alone with bin/rails s, the Vite dev server isn't up and rails-vite-plugin has nothing to proxy to. Use bin/dev instead.
  • Port 5173 is taken. Vite defaults to 5173. If another Vite app is already there, the second instance picks a random free port — but Rails still looks at 5173. Stop the other dev server (or override the Vite port via VITE_RUBY_PORT / vite.config.ts's server.port).
  • npm install didn't run. If node_modules/ is empty, npx vite fails silently. Run npm install (or bin/setup --skip-server) and try again.

Inertia page rendering throws "Page not found: <name>"

Inertia tried to resolve a page that doesn't exist under app/javascript/pages/. Two causes:

  • You forgot to create the React file. DashboardController#index with default_render: true resolves to pages/dashboard/index.tsx. If only the controller exists, Inertia raises this error at render time. Create the matching file.
  • Casing mismatch. Inertia resolves with the literal page name. On macOS the filesystem is case-insensitive but the bundler is case-sensitive — so a page name "Dashboard/Index" won't resolve pages/dashboard/index.tsx even though the file is there. Use the kebab/snake casing the controller emits ("dashboard/index", "settings/passwords/show").

Typed routes are stale after editing routes.rb

You added a new route, restarted bin/dev, but import { projects } from "@/routes" still errors with "no exported member 'projects'".

  • Typelizer didn't regenerate. Typelizer hooks into the Rails reloader; in dev it should regenerate app/javascript/routes/*.ts on every relevant routes.rb change. If it didn't, force it: bin/rails typelizer:generate. Restart npx vite so the TS file is re-read.
  • The route is in the exclude list. config/initializers/typelizer.rb excludes /^\/rails/ and /^\/up/ by default. If your route happens to match one of those, the helper is intentionally not emitted. Edit the exclude: list if needed.

npm run check fails with "Cannot find module '@/...'"

tsc can't resolve the @/ path alias. The kit ships two tsconfigs (tsconfig.app.json for app/javascript, tsconfig.node.json for the Vite config + build scripts). The path alias lives in tsconfig.app.json under compilerOptions.paths.

  • Make sure you're using the right one. npm run check runs both tsconfigs in sequence; if one fails, the message shows which. The Vite config (vite.config.ts) is type-checked by tsconfig.node.json and shouldn't import @/.
  • Restart npm run check after editing a tsconfig. TypeScript caches resolution; re-run from a fresh process.

npx shadcn@latest add <component> fails

You ran the CLI and it errored. Common causes:

  • components.json lost. Some npx shadcn@latest init invocations rewrite it. The kit's components.json is checked in for a reason — git checkout components.json if you accidentally clobbered it.
  • Path alias mismatch. The CLI uses aliases.utils = "@/lib/utils". If you re-aliased @/ in tsconfig.json without updating components.json, the import the CLI writes won't resolve at build time.
  • CDN unreachable. The CLI fetches from ui.shadcn.com. A corporate proxy or air-gapped network blocks it. Vendor the source: clone the shadcn-ui/ui repo, copy the primitive you want directly into app/javascript/components/ui/, and adjust imports by hand.

SSR fallback loop (every request is client-rendered)

You set ssr_enabled = true and --build-arg SSR_ENABLED=true, deployed, and every page is being client-rendered (no pre-rendered HTML in the initial response).

config.on_ssr_error in Inertia falls back silently to client rendering whenever the SSR process raises. Causes you'll see most often:

  • A window / document access in a component that runs on the server. Server-side, window is undefined. Guard browser-only code:

    useEffect(() => {
      // Anything that touches window goes here — useEffect doesn't run on the server.
    }, [])

    Avoid top-level access like const isDark = window.matchMedia(...).matches. The kit's lib/browser.ts exports isBrowser for that purpose.

  • SSR bundle missing. If SSR_ENABLED=true wasn't passed to docker build, public/vite-ssr/ssr.js doesn't exist; Puma's inertia_ssr plugin reports "SSR bundle not found" and falls back. Re-build the image with the flag.

  • Node 22 missing in the final image. The Dockerfile's branch-ssr-true stage copies /usr/local/node; branch-ssr-false does not. If SSR is on but the wrong stage was selected (build arg passed in the wrong spot), the runtime image has no node. Confirm docker run … which node returns a path.

Watch your APM / logs for InertiaRails::SSR::Error; that's the only signal you'll get without manually checking.

React <Form> posts but the server response doesn't update the page

You're seeing the server's flash toast, but the page's props aren't updating to match.

  • You're returning a redirect, not a re-render. That's the right pattern for success (the kit's SessionsController#create does it). For errors, you want render inertia: …, status: :unprocessable_content so Inertia merges the new props (and the errors hash) onto the current page.
  • The action returned a 302 but Inertia treated it as 200. Inertia uses the response status to decide whether to follow. Make sure your error path returns :unprocessable_content (422) and the success path returns :see_other (303) for non-GET. redirect_to in Rails 8 defaults to 303 for POST/PUT/PATCH/DELETE, which is what Inertia wants.

bin/rspec fails with "Failed to authenticate / Supabase URL not configured"

Request specs that hit an Inertia controller chain need the test helper's stubbed resume_session. If you wrote a new spec and didn't sign_in_as, the gem's middleware will try the real Supabase call and fail (the fake SUPABASE_URL set by rails_helper.rb points at the local CLI's port, which isn't running in CI).

  • Use sign_in_as at the top of any spec that asserts on an authenticated page:
    before { sign_in_as(email: "user@example.com", name: "Test User") }
  • Or stub sign_out for explicit unauthenticated paths.

Both helpers come from spec/support/supabase_auth_helpers.rb and are auto-included for type: :request specs.

"Could not start Supabase: port 54321 already in use"

Another Supabase stack — almost always one from a different project on the same machine — is bound to the local ports. Either stop that stack first:

cd ../other-project
supabase stop

…or change this project's ports in supabase/config.toml (look for [api] port = 54321, [db] port = 54322, [studio] port = 54323) and re-run supabase start.

"Lock timeout" or "database is locked" on SQLite

SQLite serialises writes — under concurrent writes (a job process + a web process, or two Puma workers writing at once), one will block until the other commits. Symptoms:

SQLite3::BusyException: database is locked

Mitigations, in order:

  • Confirm WAL is on. Rails 8.1 enables journal_mode = WAL by default, but check bin/rails db -pPRAGMA journal_mode; returns wal.
  • Lower RAILS_MAX_THREADS. Default is 3 in the kit's puma config. If you're seeing locks, you're probably running too many concurrent writes; a smaller pool serialises at the Ruby level instead of inside SQLite.
  • Move off SQLite for hot tables. The most common offender is Solid Queue (a job-heavy app writes constantly to solid_queue_*). Pointing the queue role at Postgres while keeping primary on SQLite is a valid compromise.
  • One server max. SQLite is local-disk; the file isn't shared across hosts. For multi-server deploys, you have to swap to Postgres regardless.

bin/ci fails at npm run format on a fresh checkout

Prettier disagreed with a file. On a fresh checkout this is almost always:

  • Different Prettier version. The kit pins Prettier in package.json. If npm install didn't run (or node_modules is stale), npx prettier falls back to a globally installed copy. Run npm install and re-try.
  • CRLF line endings on Windows. Prettier expects LF. Configure git with git config core.autocrlf input or run npm run format:fix to rewrite the files.

For real format failures (after you've edited code), npm run format:fix writes the corrections in place.

On this page