supabase-rails is the Supabase integration for Ruby on Rails. It plugs into the Rails middleware stack and gives every controller action a per-request Supabase context — RLS-scoped client, admin client, and JWT-derived user identity — with one mixin and one before_action.
The gem handles JWT validation, API-key verification, CORS, and Row-Level Security (RLS) scoping automatically. It is the Rails counterpart to @supabase/ssr; design notes are in PRD.md.
- Single-line authentication configuration
- Automatic CORS handling
- RLS-scoped and admin database clients
- Support for multiple auth modes (user JWT, API keys, publishable keys)
- Named-key validation for rotatable secrets
Supported Auth Modes:
:user— JWT-authenticated users:publishable— client-facing, key-validated endpoints:secret— server-to-server authenticated calls:none— open endpoints- Array syntax for multiple auth methods:
auth: [:user, :secret]
# Gemfile
gem "supabase-rails"bundle install
# or
gem install supabase-rails# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Supabase::Rails::Controller
end
# app/controllers/favorite_games_controller.rb
class FavoriteGamesController < ApplicationController
before_action :verify_supabase_auth
def index
my_games = supabase_context.supabase.from(:favorite_games).select.execute
render json: my_games
end
endThat's it. In a Rails app the gem's Railtie auto-inserts the middleware on boot with auth: :user and default CORS — no config.middleware.use needed. One mixin, one before_action: auth is validated, clients are ready, CORS is handled. Your action only runs on successful auth.
Every action with verify_supabase_auth receives a SupabaseContext via the supabase_context helper:
supabase— RLS-scoped client (respects user permissions)supabase_admin— unrestricted admin client (bypasses RLS)user_claims— extracted JWT identity (id,role,email, ...)jwt_claims— full JWT payloadauth_mode— which authentication method matched (:user,:publishable,:secret,:none)auth_key_name— named API-key identifier when applicable
supabase is always the safe client. When auth_mode is :user, it is scoped to that user; otherwise it is anonymous. supabase_admin always bypasses RLS — use it for operations that need full database access.
Per-route auth overrides flow through verify_supabase_auth:
class Admin::GamesController < ApplicationController
before_action -> { verify_supabase_auth(auth: :secret) }
def index
render json: supabase_context.supabase_admin.from(:games).select.execute
end
endSupabase::Rails::AuthError raised inside an action is automatically rendered as a JSON error response by the included rescue_from handler.
For multi-tenant routing, custom error responses, or any flow where the middleware/concern aren't a fit, the underlying primitives are public:
Supabase::Rails.create_context(request, auth:)— full context assembly from a Rack requestSupabase::Rails::Core.extract_credentials(headers)— pull token/apikey from headersSupabase::Rails::Core.verify_credentials(credentials, auth:)— low-level credential validationSupabase::Rails::Core.create_context_client(auth:)— RLS-scoped clientSupabase::Rails::Core.create_admin_client— unrestricted clientSupabase::Rails::JWT.verify(token, env:)— JWT verification with JWKS cachingSupabase::Rails::Env.resolve(overrides)— environment-variable resolution
require "supabase/rails"
result = Supabase::Rails.create_context(request, auth: :user)
return render(json: { message: result.error.message }, status: result.error.status) if result.failure?
result.value.supabase.from(:games).select.executecreate_context returns a Result exposing .value / .error and success? / failure?.
Standard configuration:
| Variable | Format | Description |
|---|---|---|
SUPABASE_URL |
https://<ref>.supabase.co |
Your project URL |
SUPABASE_PUBLISHABLE_KEYS |
{"default":"sb_publishable_...","web":"sb_publishable_..."} |
Publishable API keys (named, JSON) |
SUPABASE_SECRET_KEYS |
{"default":"sb_secret_...","web":"sb_secret_..."} |
Secret API keys (named, JSON) |
SUPABASE_JWKS |
{"keys":[...]} or [...] |
Inline JSON Web Key Set for JWT verification |
Supported alternatives (local dev, self-hosted, simpler setups):
| Variable | Format | Description |
|---|---|---|
SUPABASE_PUBLISHABLE_KEY |
sb_publishable_... |
Single publishable key |
SUPABASE_SECRET_KEY |
sb_secret_... |
Single secret key |
SUPABASE_JWKS_URL |
https://... |
Remote JWKS endpoint (used when SUPABASE_JWKS is unset) |
Plural forms take priority when both are set. For other environments, pass overrides via the middleware's env: option or Supabase::Rails::Env.resolve(overrides).
supabase-rails is thread-safe and runs on any Rack-compatible server.
| Target | Notes |
|---|---|
| Puma (multi-threaded) | Primary target. No per-thread setup required. |
| Puma (clustered) | Each worker gets its own JWKS cache; threads inside the worker share it. |
| Falcon | Sync I/O only at v0.x — async fibres work, but no async-http integration. |
| Passenger / Unicorn | Works; multi-process isolation means each worker re-fetches JWKS on cold path. |
| WEBrick / Thin | Works for development. |
In a Rails app, configure via config.supabase.* in config/application.rb or an environment file:
config.supabase.auth = :user # default auth mode for the whole app
config.supabase.cors = false # disable built-in CORS (e.g. when using rack-cors)
config.supabase.env = { url: "..." } # env overrides (optional)
config.supabase.supabase_options = {} # forwarded to Supabase::Client.newDefaults: auth: :user, CORS enabled with supabase-js-compatible headers, env resolved from SUPABASE_* variables.
For custom CORS headers, pass a Hash:
config.supabase.cors = {
"Access-Control-Allow-Origin" => "https://myapp.com",
"Access-Control-Allow-Headers" => "authorization, content-type"
}Named-key validation: auth: "publishable:web_app" or auth: "secret:cron" validates against a specific named key in SUPABASE_PUBLISHABLE_KEYS / SUPABASE_SECRET_KEYS.
Array syntax (auth: [:user, :secret]) accepts multiple methods — first match wins. An absent credential falls through to the next mode; a present-but-invalid JWT rejects the request (no silent downgrade).
The Railtie auto-inserts the middleware at the end of the stack. To position it precisely, opt out of auto-insertion and insert it yourself:
config.supabase.insert_middleware = false
config.middleware.insert_before Rack::Runtime, Supabase::Rails::Middleware, auth: :userOr use the gem in a plain Rack app (no Rails, no Railtie):
require "supabase/rails"
use Supabase::Rails::Middleware, auth: :userThe gem is in public beta (v0.x). Breaking changes only ship as a major bump. The gem is still early — expect ergonomic improvements and features to land frequently in minor releases. Found a rough edge? Open an issue or send a PR.
Releases are published to RubyGems via Trusted Publishing — no API tokens stored anywhere. To cut a release:
- Update
CHANGELOG.md, moving items from[Unreleased]to a new version section. - Bump
Supabase::Rails::VERSIONinlib/supabase/rails/version.rb. - Commit:
git commit -am "release: v0.1.0" - Tag and push:
git tag v0.1.0 && git push origin main --tags
The release workflow builds the gem, runs the test suite, publishes to RubyGems via OIDC, and creates a GitHub Release with auto-generated notes.
One-time setup (on rubygems.org): under the supabase-rails gem settings → Trusted Publishers, add a GitHub Actions publisher with repo supabase-ruby/supabase-rails, workflow release.yml. Until the gem is first published, use Pending Trusted Publishers to reserve the name.
For added safety, gate the workflow on a GitHub Environment (environment: name: rubygems in release.yml) so publishing requires manual approval per release.
MIT