supabase-rb-rb
Hotwire starter

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.example is for direnv, dotenv, or your process manager to read. 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 three variables through your platform's secret manager.

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).

"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-session for the origin. Refresh.
  • Your Supabase project changed. If SUPABASE_URL now 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 date on 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_id doesn't match Current.user.id. When you inserted from Supabase Studio, auth.uid() was null (Studio runs as postgres, not as the user), so the default user_id ended up null or the row was rejected. Insert from the notes page (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 list Users 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 — run supabase db push (against a linked project) or paste supabase/migrations/*.sql into the dashboard SQL editor.
  • You're not actually signed in. The NotesController requires authentication. If Current.user is nil you'd get redirected to /session/new, not the empty state — but a misconfigured before_action (you edited Authentication and broke require_authentication) might let the request through unauthenticated. The PostgREST call without an Authorization header 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/dev isn't running the Tailwind watcher. Procfile.dev runs both bin/rails server and bin/rails tailwindcss:watch. If you started Rails with bin/rails s directly, the watcher isn't rebuilding app/assets/builds/application.css. Use bin/dev instead.
  • The class isn't in any scanned source. Tailwind v4 scans only files declared in app/assets/tailwind/application.css's @source directives. If you added a view in a non-default location (e.g. lib/), add a matching @source line. Same for new ViewComponents under app/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.erb is 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/ci will 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 .env for 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 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 .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 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 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 install didn't run. Re-run bin/setup --skip-server to pick up the latest gems.
  • Your local Ruby is older than .ruby-version. Some omakase cops disable themselves on old Rubies and warn on nil. rbenv install / mise install whatever .ruby-version says and re-run.

For real style failures (after you've edited code), bin/rubocop -a auto-corrects what it can.

On this page