Configuration
Every config.supabase.* Railtie option for supabase-rails, the environment-variable fallbacks, the CORS behaviour, and the logger.
supabase-rails exposes its full configuration surface on the Rails Railtie at config.supabase.*. Defaults are set when Supabase::Rails::Railtie boots; the install generator writes a starter config/initializers/supabase.rb that overrides the two values most apps need to change (mode and a commented-out session block).
The same Railtie also reads a small set of environment variables via Supabase::Rails::Env.resolve and inserts the Rack middleware that hangs the per-request context off request.env.
Where configuration lives
# config/initializers/supabase.rb (written by `bin/rails generate supabase:install`)
Rails.application.config.supabase.mode = :web
# Rails.application.config.supabase.allowed_redirect_origins = ["https://example.com"]
# Rails.application.config.supabase.expose_current_user = nil
# Rails.application.config.supabase.session = {
# cookie_name: "sb-session",
# same_site: :lax,
# secure: nil,
# domain: nil,
# path: "/"
# }Three pieces work together:
Supabase::Rails::Railtiedeclaresconfig.supabase = ActiveSupport::OrderedOptions.new, sets defaults for every key listed below, and insertsSupabase::Rails::Middlewareinto the host's Rack stack (unlessinsert_middlewareisfalse).Supabase::Rails::Engineis internal — it is not mounted by the host (mount Supabase::Rails::Engineis not required). Its single job is to install thesupabase_authentication_routesDSL helper ontoActionDispatch::Routing::Mapperbefore Rails drawsconfig/routes.rb, and to add the gem'sapp/controllers/app/views/config/localesto the host autoload paths. The engine deliberately skipsisolate_namespaceso the gem's controllers inherit from the host's::ApplicationController.Supabase::Rails::EnvparsesSUPABASE_*environment variables at request time. Keys provided viaconfig.supabase.env(a Hash override) take precedence over the corresponding environment variables.
Config keys
Every key has a default. You only need to set the ones that diverge from the defaults — the install generator sets mode = :web because the framework default is :api.
mode
| Type | Default |
|---|---|
Symbol (:api or :web) | :api |
Selects the authentication strategy. :api extracts credentials from the Authorization: Bearer and apikey request headers — appropriate for JSON APIs and SPAs that send a JWT on every request. :web reads and refreshes a Rails-encrypted session cookie (sb-session by default), so Devise-style server-rendered apps work without the client tracking tokens manually. Any other value raises Supabase::Rails::ConfigError(INVALID_MODE) when the middleware is constructed.
Rails.application.config.supabase.mode = :webauth
| Type | Default |
|---|---|
Symbol or Array<Symbol> | :user |
In :api mode, the list of auth modes the middleware will try in order when verifying credentials. Recognised values: :user (verify a Bearer JWT against the project's JWKS), :publishable (match the apikey header against SUPABASE_PUBLISHABLE_KEY[S]), :secret (match against SUPABASE_SECRET_KEY[S]), :none (skip verification — useful for public routes). Append :name to restrict to a single key entry (e.g. [:publishable, "publishable:default"]) or :* to accept any. Ignored in :web mode, where credentials come from the encrypted cookie.
Rails.application.config.supabase.auth = %i[user publishable]session
| Type | Default |
|---|---|
Hash | { cookie_name: "sb-session", same_site: :lax, secure: nil, domain: nil, path: "/" } |
Encrypted session cookie config for :web mode. The cookie is encrypted with the host's existing secret_key_base — there is no new secret to manage. Cookies are always HttpOnly; secure: nil auto-detects from Rails.env.production?.
| Key | Default | Description |
|---|---|---|
cookie_name | "sb-session" | Cookie name. |
same_site | :lax | :lax, :strict, or :none. :none requires secure: true. |
secure | nil | nil → auto-detect (Rails.env.production? → true). Set true/false to force. |
domain | nil | Cookie Domain attribute. nil → host-only cookie. Set for subdomain sharing. |
path | "/" | Cookie Path attribute. |
Rails.application.config.supabase.session = {
cookie_name: "myapp-session",
same_site: :strict,
secure: true,
domain: ".myapp.com",
path: "/"
}allowed_redirect_origins
| Type | Default |
|---|---|
Array<String> | [] |
Origin allowlist consulted by the OAuth and password-reset helpers when validating a ?redirect_to= query param. Path-only targets (/dashboard) are always allowed; absolute URLs must match an entry exactly on scheme://host[:port]. When empty, the helpers fall back to [request.host] (same-origin only) at runtime, deriving the scheme from the request — so dev apps on http://localhost:3000 still work. Off-allowlist redirects raise Supabase::Rails::AuthError(INVALID_REDIRECT) with HTTP 400.
Rails.application.config.supabase.allowed_redirect_origins = [
"https://app.example.com",
"https://staging.example.com"
]oauth_providers
| Type | Default |
|---|---|
Array<Symbol> | [] |
Provider list rendered by the supabase/rails/oauth/_buttons partial. The default sign-in / sign-up views render this partial unconditionally, so an empty array means no OAuth buttons appear. Each entry becomes one "Sign in with <provider>" link wired to oauth_path(provider). The string must match a provider id Supabase recognises (google, github, azure, …) — the gem does not validate the list.
Rails.application.config.supabase.oauth_providers = %i[google github]user_model
| Type | Default |
|---|---|
String (class name) or nil | nil |
Opt into a shadow ActiveRecord User model that mirrors the Supabase user row (FR-W14). When set, Current.user becomes the AR record returned by <Model>.from_supabase(claims) (a by-PK lookup) instead of the immutable Supabase::Rails::User value object. The supabase:user_model generator emits this class + migration and appends this line to the initializer for you.
Rails.application.config.supabase.user_model = "User"expose_current_user
| Type | Default |
|---|---|
Boolean or nil | nil (derives from mode) |
When true, the Authentication concern exposes current_user as a Rails helper_method so views can call it directly. When nil (the default), it derives from mode — true in :web, false in :api (avoids clashing with API hosts that define their own current_user). Set explicitly when you need to override the inferred behaviour.
Rails.application.config.supabase.expose_current_user = trueinsert_middleware
| Type | Default |
|---|---|
Boolean | true |
Whether the Railtie should insert Supabase::Rails::Middleware into the host's Rack stack on boot. Set to false only if you want to mount the middleware yourself in a non-standard position (for example, before a custom authentication middleware). Without the middleware, request.env[Supabase::Rails::CONTEXT_KEY] is never populated and every authenticated request returns 401.
Rails.application.config.supabase.insert_middleware = false
# Then mount it manually:
Rails.application.config.middleware.insert_before MyAuthMiddleware,
Supabase::Rails::Middleware,
mode: :apienv
| Type | Default |
|---|---|
Hash or nil | nil |
Per-host overrides for the environment variables Supabase::Rails::Env.resolve reads. Recognised keys: :url, :publishable_keys, :secret_keys, :jwks. Each key, when present, replaces the corresponding SUPABASE_* env-var lookup entirely. Useful in tests (config.supabase.env = { url: "http://localhost:54321", publishable_keys: { "default" => "sb_publishable_test" } }) or in multi-tenant apps that source credentials from a database.
Rails.application.config.supabase.env = {
url: "https://abcd1234.supabase.co",
publishable_keys: { "default" => "sb_publishable_..." },
secret_keys: { "default" => "sb_secret_..." }
}supabase_options
| Type | Default |
|---|---|
Hash or nil | nil |
Raw options forwarded to Supabase::Client.new for every context client the middleware builds. Accepts the same shape as supabase-rb's Supabase::ClientOptions (or the legacy nested-hash form). Lets you set custom global headers, swap the HTTP adapter, or tune the PostgREST / Storage timeouts. The gem rewrites auth: to always include auto_refresh_token: false, persist_session: false, and detect_session_in_url: false (hard invariants — auto_refresh_token: true would leak a background timer per request).
Rails.application.config.supabase.supabase_options = {
global: { headers: { "X-App-Version" => MyApp::VERSION } },
postgrest: { timeout: 30 }
}cors
| Type | Default |
|---|---|
Hash, nil, or false | nil |
Controls CORS behaviour on the middleware. nil (default) sends a permissive set of headers tuned for the Supabase clients calling your API. false disables CORS entirely (use this when rack-cors is mounted upstream and should own the response). A Hash is used verbatim as the Access-Control-* headers added to every response and to the 204 preflight reply. See CORS behaviour below for the default header set.
Rails.application.config.supabase.cors = {
"Access-Control-Allow-Origin" => "https://app.example.com",
"Access-Control-Allow-Headers" => "authorization, apikey, content-type",
"Access-Control-Allow-Methods" => "GET, POST, OPTIONS"
}Environment variables
Supabase::Rails::Env.resolve is invoked once per request by the middleware (and lazily by the auth helpers). It reads:
| Variable | Purpose | Required? |
|---|---|---|
SUPABASE_URL | Project URL, e.g. https://abcd1234.supabase.co. | Yes — EnvError.missing_supabase_url raises 500 when absent. |
SUPABASE_PUBLISHABLE_KEY | Default publishable (anon) key (sb_publishable_…). | At least one of the two. |
SUPABASE_PUBLISHABLE_KEYS | JSON map {"default":"sb_publishable_…","tenant_a":"sb_publishable_…"} for multi-key apps. | At least one of the two. |
SUPABASE_SECRET_KEY | Default secret (service-role) key (sb_secret_…). | Required for :secret auth mode or admin client access. |
SUPABASE_SECRET_KEYS | JSON map for multiple secret keys. | Required for :secret auth mode or admin client access. |
SUPABASE_JWKS | Inline JWKS JSON ({"keys":[…]} or a raw […] array). | One of the two for JWT verification. |
SUPABASE_JWKS_URL | Remote JWKS endpoint. Must be https:// — http:// is only accepted for loopback hosts (localhost, 127.0.0.1, [::1]). | One of the two for JWT verification. |
The gem reads `SUPABASE_PUBLISHABLE_KEY`, not `SUPABASE_ANON_KEY`
The Supabase dashboard labels the same value as "anon public" on legacy
projects and "publishable" on new projects. The gem only looks at
SUPABASE_PUBLISHABLE_KEY / SUPABASE_PUBLISHABLE_KEYS — setting
SUPABASE_ANON_KEY (or SUPABASE_SERVICE_ROLE_KEY for the secret) does
nothing. Rename your env vars when copying from a Node project.
Multiple keys (plural form)
SUPABASE_PUBLISHABLE_KEYS (and SUPABASE_SECRET_KEYS) take precedence over the singular form when set. The value must be a JSON object mapping a key name to a key value:
SUPABASE_PUBLISHABLE_KEYS='{"default":"sb_publishable_aaa","tenant_a":"sb_publishable_bbb"}'The singular form is shorthand for {"default":"<value>"}. Auth modes can target a specific key via auth: [:publishable, "publishable:tenant_a"]. Unparseable JSON silently falls back to {} — no key is registered, and request authentication will fail rather than crash at boot.
Interaction with config.supabase.env
When config.supabase.env is set (a Hash, not nil), each key present in the hash replaces the corresponding environment variable lookup wholesale. Keys not present in the hash still fall back to the environment:
# Reads SUPABASE_URL from ENV; uses the hash for keys.
Rails.application.config.supabase.env = {
publishable_keys: { "default" => "sb_publishable_test" }
}This is the recommended hook for tests (drive credentials from Rails.application.credentials.dig(:supabase, :test)) or for multi-tenant routing (build the hash per-request via a custom middleware that pre-populates request.env[Supabase::Rails::CONTEXT_KEY]).
CORS behaviour
Supabase::Rails::Middleware consults config.supabase.cors:
nil(default) — addsSupabase::Rails::CORS::DEFAULT_HEADERSto every response and replies toOPTIONSrequests with204+ the same headers.false— skips CORS entirely. Preflights pass through to the app; noAccess-Control-*headers are added.Hash— used verbatim as the header set.
The default headers are intentionally permissive so the Supabase JS / Swift / Kotlin clients can talk to your API from any origin:
| Header | Default |
|---|---|
Access-Control-Allow-Origin | * |
Access-Control-Allow-Headers | authorization, x-client-info, apikey, content-type, x-retry-count |
Access-Control-Allow-Methods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
# Lock origin to your dashboard SPA while keeping the gem's allowed headers/methods.
Rails.application.config.supabase.cors = Supabase::Rails::CORS::DEFAULT_HEADERS
.merge("Access-Control-Allow-Origin" => "https://app.example.com")Browser-only concern
CORS only governs browser fetches. Server-to-server calls and native mobile
clients ignore these headers entirely — set cors: false if your API is
consumed exclusively by trusted backends.
Logging
Supabase::Rails::Logging is a thread-safe accessor for the gem's logger. The gem writes structured one-line messages ([supabase.rails.sign_in_failure] code=… email=a***@example.com, [AUTH_…] warnings on credential failures) — set a logger to capture them.
| API | Description |
|---|---|
Supabase::Rails.logger= | Assign any object that responds to :debug, :info, :warn, :error. Mutex-guarded; safe to set at boot or swap at runtime. |
Supabase::Rails.logger | Reads the current logger. Returns nil when unset. |
Supabase::Rails::Logging.log(level, message) | Internal helper. No-op when no logger is set; swallows exceptions so a misbehaving logger never breaks an auth flow. |
# config/initializers/supabase.rb
Supabase::Rails.logger = Rails.loggerEmail addresses in log lines are automatically redacted via Supabase::Rails::Authentication.redact_email (alice@example.com → a***@example.com), so you can ship sign-in failures to your log aggregator without leaking PII. Access tokens, refresh tokens, and JWT claims are never logged.
The Railtie initializer
The Railtie runs one initializer at boot:
initializer "supabase.middleware" do |app|
cfg = app.config.supabase
next unless cfg.insert_middleware
app.middleware.use Supabase::Rails::Middleware,
mode: cfg.mode,
auth: cfg.auth,
env: cfg.env,
supabase_options: cfg.supabase_options,
cors: cfg.cors,
session: cfg.session,
user_model: cfg.user_model
endTwo things to note:
- The middleware is appended at the end of the stack with
app.middleware.use. If you need it earlier (for example, before aRack::Attackthrottler that should see the authenticated user), setinsert_middleware = falseand callRails.application.config.middleware.insert_beforeyourself. - The middleware constructor reads its config from arguments — it does not re-read
Rails.application.config.supabaseper request. Changing a key after boot has no effect; restart the Rails process.
See also
- Getting started — installing the gem and setting
SUPABASE_URL/SUPABASE_PUBLISHABLE_KEY. - Generators — what
supabase:installwrites into the initializer. - Web mode — how the encrypted session cookie is read, refreshed, and rewritten.
- Authentication — the concern that consumes
expose_current_user,user_model, andallowed_redirect_origins. - supabase-rb: Initializing —
Supabase::Client.newandSupabase::ClientOptions, the underlying surfacesupabase_optionsis forwarded to.
Getting started with Rails Supabase auth
Install supabase-rails and authenticate your first user in a fresh Rails app in under 15 minutes — supabase-rb on Rails, in :web mode, with cookie-backed sessions and a Current.user populated from the verified Supabase JWT.
Generators
Rails generators that install the gem, scaffold a User model, and copy view templates into the host app.