Troubleshooting
Most likely first-run failures with the Hotwire starter — missing env, expired-session loops, empty notes list, Tailwind not building, Importmap pin missing, Letter Opener empty — 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 dashboard with a working /notes view. Each section names the symptom you'll see and the fix.
"Supabase URL not configured" on first request
The gem reads SUPABASE_URL, SUPABASE_ANON_KEY, and (for admin calls) SUPABASE_SERVICE_ROLE_KEY 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.exampleis fordirenv,dotenv, or your process manager to read. The simplest fix iscp .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 three variables through your platform's secret manager.
Cookie isn't being set
You signed up, see no error, but every refresh sends you back to /session/new. Two possible causes:
- You're on
http://andconfig.force_ssl = true(orconfig.assume_ssl = true) is on. Thesb-sessioncookie is markedSecure, and the browser silently drops it on a non-HTTPS origin. For local development make sure those two are commented out inconfig/environments/production.rband you're not somehow booting the app underproduction. Local dev runsconfig/environments/development.rb, which doesn't set either. secret_key_baseis not stable. If Rails regenerates a key between requests (e.g. you have noconfig/master.keyand noRAILS_MASTER_KEYenv var), the cookie written by request N is undecryptable by request N+1. Checkbin/rails credentials:showruns cleanly; if it errors, generate a master key withbin/rails credentials:editand commit the resultingconfig/credentials.yml.enc(the key itself stays inconfig/master.key, which is gitignored).
"Your session has expired" loop
You get redirected to /session/new with the expired-session flash, sign in, get redirected, see the flash again, and so on.
The flash is attached by ExpiredSessionFlash in app/controllers/concerns/authentication.rb, but only when the request arrived with an sb-session cookie that the middleware just had to invalidate. A loop usually means one of:
- The browser is sending an old cookie that you can't clear. Open dev tools → Application → Cookies → delete
sb-sessionfor the origin. Refresh. - Your Supabase project changed. If
SUPABASE_URLnow points at a different project than the one that issued the original session, refresh fails and the cookie keeps getting invalidated. Clear the cookie or sign out from another tab first. - Clock skew. A wildly wrong system clock can make the gem think every token is expired. Check
dateon your local machine; on a server, confirm NTP is working.
If the loop persists with a clean browser, check Current.session.access_token (in the Rails console) is what you expect — if it's nil, the cookie isn't reaching the gem at all and you're back at the "Cookie isn't being set" case.
/notes is empty when I know I inserted a row
The /notes page lists rows from public.notes scoped to the signed-in user. Empty almost always means one of:
- The row's
user_iddoesn't matchCurrent.user.id. When you inserted from Supabase Studio,auth.uid()wasnull(Studio runs aspostgres, not as the user), so the defaultuser_idended upnullor the row was rejected. Insert from thenotespage (once you've added a Recipe-1-style create form) or run an SQL statement that sets the column explicitly:insert into public.notes (user_id, content) values ('<your-uuid>', 'hello');. - RLS is blocking the read. Confirm the policies are in place:
select policyname, cmd from pg_policies where tablename = 'notes';should listUsers can read own notes,Users can insert own notes,Users can update own notes,Users can delete own notes. If they're missing, you skipped the migrations — runsupabase db push(against a linked project) or pastesupabase/migrations/*.sqlinto the dashboard SQL editor. - You're not actually signed in. The
NotesControllerrequires authentication. IfCurrent.userisnilyou'd get redirected to/session/new, not the empty state — but a misconfiguredbefore_action(you editedAuthenticationand brokerequire_authentication) might let the request through unauthenticated. The PostgREST call without anAuthorizationheader returns nothing under RLS.
Tailwind classes aren't applied
You added a class like bg-emerald-500 to a view, refreshed, and it didn't take.
bin/devisn't running the Tailwind watcher.Procfile.devruns bothbin/rails serverandbin/rails tailwindcss:watch. If you started Rails withbin/rails sdirectly, the watcher isn't rebuildingapp/assets/builds/application.css. Usebin/devinstead.- The class isn't in any scanned source. Tailwind v4 scans only files declared in
app/assets/tailwind/application.css's@sourcedirectives. If you added a view in a non-default location (e.g.lib/), add a matching@sourceline. Same for new ViewComponents underapp/components/— that path is already scanned by default; non-default paths are not. - Browser cached the old CSS. Hard refresh (Cmd-Shift-R on macOS / Ctrl-Shift-R elsewhere). Turbo Drive's
data-turbo-track="reload"on the stylesheet tag in_head.html.erbis supposed to bust this, but a hard refresh is the surefire fix.
"No matching importmap entry" in dev tools
After adding a Stimulus controller that imports a new package:
Uncaught TypeError: Failed to resolve module specifier "<name>".You forgot the Importmap pin. Run bin/importmap pin <name> to add it; that updates config/importmap.rb and (for downloadable packages) drops a vendored copy into vendor/javascript/. Restart the dev server so the new importmap.rb is read.
If the package is published only on a CDN (e.g. @floating-ui/dom is already pinned to jsdelivr), add the pin by hand:
pin "the-package", to: "https://cdn.jsdelivr.net/npm/the-package@1.2.3/+esm"/letter_opener shows no emails
In development, letter_opener_web is mounted at /letter_opener. It only captures mail sent via Rails' ActionMailer — Supabase Auth sends its confirmation, password-reset, and OTP mails from Supabase's own infrastructure, not through Rails.
For a fully local Supabase stack (supabase start), the CLI runs inbucket on port 54324 (open http://127.0.0.1:54324) and routes outbound auth mails there. That's where the confirmation email lands in dev.
letter_opener_web only matters once you start sending mail from Rails — e.g. a "Welcome" mailer the kit doesn't ship.
"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.
E2E suite says "Supabase CLI not found" or "Docker is not running"
bin/e2e needs both. Both errors are reported up-front before any test runs:
- "Supabase CLI not found" — install via
brew install supabase/tap/supabase(macOS) or follow the official install docs. Re-open your shell so the new PATH entry is picked up. - "Docker is not running" — start Docker Desktop and wait for the whale icon to stop animating. Re-run
bin/e2e.bin/ciwill silently skip the E2E step when Docker is unreachable; the rest of the suite still runs.
If the stack boots but tests fail with "Supabase reset failed", bump the boot budget with E2E_SUPABASE_TIMEOUT=240 bin/e2e. For a clean slate, supabase stop --no-backup drops the local DB volume — the next bin/e2e cold-boots a fresh state. --no-backup is destructive (your local data is gone), so reach for it only when you suspect a corrupted local DB.
GitHub OAuth redirects to a 500 page
The "Continue with GitHub" button kicks off the Supabase OAuth flow, which goes Browser → Supabase → GitHub → Supabase → Rails. If the final redirect to /oauth/callback 500s:
- Supabase isn't configured for GitHub. In the Supabase dashboard, Authentication → Providers → GitHub must be enabled and have the Client ID + Client Secret pasted in. The credentials live in your local
.envfor documentation only — they're not read by Rails. - The callback URL on the GitHub OAuth app is wrong. It must be the Supabase callback URL (
https://<project-ref>.supabase.co/auth/v1/callback), not your Rails app's URL. Supabase is the OAuth target; it then hands the session back to Rails. - Your Supabase project's "Redirect URLs" allowlist doesn't include the kit's origin. Add
http://localhost:3000(dev) and your production URL under Authentication → URL Configuration.
The end-to-end smoke flow for OAuth is in the kit's README "Sign in with GitHub" section.
"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 lockedMitigations, in order:
- Confirm WAL is on. Rails 8.1 enables
journal_mode = WALby default, but checkbin/rails db -p→PRAGMA journal_mode;returnswal. - Lower
RAILS_MAX_THREADS. Default is 3 in the kit's.env.example. 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 thequeuerole at Postgres while keepingprimaryon 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 rubocop on a fresh checkout
The kit ships rubocop-rails-omakase — the Rails team's preset. If bin/rubocop fails on a fresh checkout with no edits from you, the cause is almost always:
bundle installdidn't run. Re-runbin/setup --skip-serverto pick up the latest gems.- Your local Ruby is older than
.ruby-version. Some omakase cops disable themselves on old Rubies and warn onnil.rbenv install/mise installwhatever.ruby-versionsays and re-run.
For real style failures (after you've edited code), bin/rubocop -a auto-corrects what it can.
Deployment
Production-readiness checklist for the Hotwire starter — secrets, SQLite vs Postgres, TLS, cache, health checks, observability.
Inertia + React starter
Rails 8.1 + Inertia + React 19 + TypeScript + Vite + Tailwind v4 + shadcn/ui starter kit, with Supabase Auth via supabase-rails in :web mode and optional Inertia SSR.