supabase-rb-rb
Rails API starter

Troubleshooting

Most likely first-run failures with the Rails API starter — 401 loops, JWKS errors, CORS preflight, Postgres connection refused, missing env, swagger regeneration — and how to resolve them.

A guide to the failures you're most likely to hit on the path from git clone to a working request. Each section names the symptom you'll see and the fix.

"Missing required Supabase environment variable(s)" on boot

The initializer in config/initializers/supabase.rb fails fast if SUPABASE_URL, SUPABASE_ANON_KEY, or SUPABASE_JWT_SECRET is missing in production. In development it warns and continues.

  • In dev: copy .env.example to .env and fill in the four Supabase values (see Getting started). Restart bin/rails sdotenv-rails reads .env at boot, not at request time.
  • In production: set the same three vars through your deploy platform's secret manager. .env is not loaded in production. The error message names the missing variable(s) explicitly.

Spec suite won't start

LoadError: cannot load such file -- jwt

bundle install didn't finish or you're using the wrong Ruby. Re-run bundle install after confirming ruby --version matches .ruby-version (4.0.x for Rails 8.1).

ActiveRecord::NoDatabaseError

db:test:prepare hasn't run. bundle exec rspec will trigger it automatically; if it fails, run bin/rails db:prepare RAILS_ENV=test and check Postgres is up.

If the suite hangs or makes network calls, you've broken the in-memory JWKS — most likely by editing spec/rails_helper.rb and removing the ENV['SUPABASE_JWKS'] setup. Restore it; the in-memory JWKS is what makes the suite hermetic.

GET /api/v1/me returns 401 with a valid-looking token

The middleware accepts a JWT only if signature, audience, issuer, and expiry all check out. The body shape ({"error":"unauthorized"}) doesn't tell you which one failed, so add RAILS_LOG_LEVEL=debug and look at the log line from Supabase::Rails::Middleware. Common causes:

  • Wrong SUPABASE_JWT_SECRET. Local CLI prints a long static secret; Cloud projects have a per-project one in Project Settings → API → JWT Settings. Production env must match production JWTs.
  • Wrong project. If SUPABASE_URL points at one project and the token was minted by another, the iss claim won't match and verification fails.
  • Expired token. supabase-js refreshes automatically; a hand-copied token from a debug script expires in an hour.
  • Old anon-key format. A JWT-shaped eyJ... anon key works as both SUPABASE_ANON_KEY and (effectively) SUPABASE_PUBLISHABLE_KEY. New sb_publishable_... keys are not JWTs — the kit handles either, but if you mixed an old project's secret with a new project's anon key, the validation can be inconsistent.

GET /api/v1/me returns 401 with no token at all (expected) — but the body is wrong

If you see {"message":"...","code":"..."} instead of {"error":"unauthorized"}, the JsonUnauthorizedResponder middleware isn't in the stack. Two likely causes:

  • You edited config/application.rb and broke the starter_kit.json_unauthorized_responder initializer. Confirm it's still there and references Supabase::Rails::Middleware (which only exists after the gem's own initializer, hence after: "supabase.middleware").
  • You removed the JsonUnauthorizedResponder Rack class from app/middleware/. The class needs to exist on disk and be autoload-resolvable — config.autoload_paths << Rails.root.join("app/middleware") in config/application.rb is what makes that work.

Run bin/rails middleware to see the live stack. JsonUnauthorizedResponder should appear immediately above Supabase::Rails::Middleware.

"CORS error" in the browser, but curl works

Rack::Cors only mounts if CORS_ORIGINS is set (or you're in dev/test). In production with CORS_ORIGINS empty, the middleware doesn't mount at all and every browser request gets blocked by the browser before it reaches Rails. Symptoms:

  • Browser dev tools show "Access to fetch at … from origin … has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header".
  • curl works fine because curl doesn't enforce CORS.

Fix: set CORS_ORIGINS=https://your-frontend.example.com in the deploy environment and redeploy. Multiple origins go comma-separated. Don't use * in production unless you've thought about it.

Postgres connection refused

PG::ConnectionBad: connection to server ... failed: Connection refused
  • Local CLI: the Supabase CLI's Postgres isn't running. supabase start it. The CLI prints the right DATABASE_URL — copy it into .env exactly (port 54322, not the default 5432).
  • Local non-CLI: your host Postgres isn't listening. Start it (brew services start postgresql, pg_ctl, etc.) and confirm the user/password match config/database.yml or DATABASE_URL.
  • Production / Supabase Cloud: make sure DATABASE_URL points at the pooler URL (port 6543), not the direct connection (5432). The direct connection is rate-limited and not meant for app traffic.

/api-docs returns 404 in production

By default, rswag-ui is only mounted in production if SWAGGER_UI_ENABLED=true. The route table in config/routes.rb short-circuits the mount when both conditions are false. Set the env var to true and redeploy.

If you've set it and still get 404, the YAML file at swagger/v1/swagger.yaml may be missing from the production build. Confirm swagger/ is checked in (it should be) and not in .dockerignore.

swagger/v1/swagger.yaml is stale after a change

The OpenAPI doc is regenerated from spec/integration/ specs only on demand:

RAILS_ENV=test bundle exec rake rswag:specs:swaggerize

The RAILS_ENV=test is required — rswag-specs is in the :test Bundler group, so the task isn't registered in development. Commit the regenerated YAML.

If the task says Task 'rswag:specs:swaggerize' not found, you forgot the RAILS_ENV=test. If it generates but the doc doesn't show your changes, the spec under spec/integration/ doesn't actually describe them — rswag only serializes what the DSL declares.

Rack::Attack is throttling me in development

It shouldn't be — Rack::Attack.enabled = false is set in the test env (config/initializers/rack_attack.rb). Development has the throttles on, with limits that are generous for a single user (300 req per 5 min per IP).

If you're getting throttled while iterating, either bump the limits in that initializer or temporarily set Rack::Attack.enabled = false in development.rb.

Rate limit isn't enforced in production

You set Rack::Attack to count per-IP, but the limit doesn't kick in. Almost always: Rails.cache is process-local in production, so each Puma worker has its own counter.

Confirm config.cache_store in production.rb is :solid_cache_store, :redis_cache_store, or :mem_cache_store — anything shared. The kit defaults to :solid_cache_store and it works out of the box, but a deploy that swapped it for :memory_store will silently fail-open.

Boot warning: apikey middleware not configured

Supabase::Rails::Middleware looks for SUPABASE_PUBLISHABLE_KEY. The kit's config/initializers/supabase.rb maps SUPABASE_ANON_KEY onto it at boot:

ENV["SUPABASE_PUBLISHABLE_KEY"] ||= ENV["SUPABASE_ANON_KEY"]

If you removed that line or both vars are empty, the apikey middleware won't recognise the inbound apikey header. Set SUPABASE_ANON_KEY (the canonical kit env var) and the mapping does the rest. Newer Supabase projects label the same value sb_publishable_...; either format works.

Health check is failing the LB

The Kamal proxy (and most LBs) hit GET /healthz. The controller returns {"status":"ok"} with 200 and doesn't touch the database. If it's failing:

  • 404: check config/routes.rb still has get "healthz", to: "healthz#show".
  • 500: read the Rails log — HealthzController#show is two lines, so a 500 usually means a Rack-level boot error. bin/rails middleware will tell you if the stack is broken.
  • 401: you accidentally removed allow_unauthenticated_access only: :show from HealthzController. Add it back; without it, the Authentication concern will demand a token.

On this page