Views
Reference for every gem-shipped ERB template under app/views/supabase/rails/ — form fields, params, submit targets, the shared flash and OAuth buttons partials, and how to customise them.
The gem ships eight ERB templates under app/views/supabase/rails/ — six top-level forms (sign-in, sign-up, password request, password reset, OTP request, OTP verify) and two partials (the shared flash and the OAuth provider buttons). They render out of the box and can be overridden file-by-file by dropping a matching file into the host app at the same path. The fastest way to get an editable copy of all eight is bin/rails generate supabase:views — see the generator page for the override-precedence mechanics.
This page is the per-template reference: for each view, exactly which fields it renders, which params the controller reads from those fields, where the form submits, and which partials it depends on. Use it when customising a template to make sure your edit preserves the controller contract.
Template inventory
Top-level templates, in route-table order:
| Template | Renders | Submits to | Params read | Partials |
|---|---|---|---|---|
sessions/new.html.erb | Sign-in form: email + password, with "Forgot password?" and "Create an account" links. | POST /session | :email, :password | shared/_flash, oauth/_buttons |
registrations/new.html.erb | Sign-up form: email + password, with a link back to sign-in. | POST /registration | :email, :password | shared/_flash |
passwords/new.html.erb | Forgot-password request: email only. | POST /passwords | :email | shared/_flash |
passwords/edit.html.erb | Reset-password form: password + confirmation. Rendered after the user clicks the recovery email link. | PUT /passwords/:token | :password (and :password_confirmation, client-only) | shared/_flash |
otp/new.html.erb | One-time-code request: email only. | POST /otp | :email, :phone | shared/_flash |
otp/verify.html.erb | One-time-code verify: hidden email/phone/type + visible token field. | POST /otp/verify | :token, :type, :email, :phone | shared/_flash |
Partials:
| Partial | Rendered by | Locals | Reads from |
|---|---|---|---|
shared/_flash.html.erb | All six top-level templates above. | None. | flash[:notice], flash[:alert] |
oauth/_buttons.html.erb | sessions/new.html.erb (unconditionally). | None. | Rails.application.config.supabase.oauth_providers |
Where these files live
The gem keeps the originals under app/views/supabase/rails/ inside the gem source. Rails' standard view-path resolution puts the host's app/views/supabase/rails/ first, so any file you drop at the same relative path overrides the gem's copy — no prepend_view_path, no opt-in flag, no initializer change. See Override precedence on the generator page for the full mechanics.
sessions/new.html.erb
Sign-in form rendered at GET /session/new. Submits to POST /session, which routes to SessionsController#create — see Controllers → Sessions.
<h1>Sign in</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: session_path do |form| %>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
<%= form.submit "Sign in" %>
<% end %>
<br>
<%= link_to "Forgot password?", new_password_path %><br>
<%= link_to "Create an account", new_registration_path %>
<%= render "supabase/rails/oauth/buttons" %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
email | email_field | required, autofocus, autocomplete="username", value: params[:email] | Pre-fills from params[:email] so a failed sign-in keeps the email the user typed. |
password | password_field | required, autocomplete="current-password", maxlength: 72 | maxlength: 72 matches Supabase Auth's bcrypt limit — raising it lets users type passwords Supabase will reject. |
Customisation contract (preserve across any edit):
form_with url: session_path— the URL helper that resolves toPOST /session.- Field names
:emailand:password— the controller reads these fromparamsverbatim. render "supabase/rails/shared/flash"— surfacesflash.now[:alert]set by the controller on bad credentials (otherwise failed sign-ins re-render silently).render "supabase/rails/oauth/buttons"— renders the OAuth row whenconfig.supabase.oauth_providersis non-empty; safe to drop if you never want OAuth.
registrations/new.html.erb
Sign-up form rendered at GET /registration/new. Submits to POST /registration, which routes to RegistrationsController#create — see Controllers → Registrations.
<h1>Create an account</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: registration_path do |form| %>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Choose a password", maxlength: 72 %><br>
<%= form.submit "Sign up" %>
<% end %>
<br>
<%= link_to "Already have an account? Sign in", new_session_path %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
email | email_field | required, autofocus, autocomplete="username", value: params[:email] | Pre-fills after a WEAK_PASSWORD re-render so the user doesn't retype the email. |
password | password_field | required, autocomplete="new-password", maxlength: 72 | Use autocomplete="new-password" (not current-password) so password managers offer to generate. |
Customisation contract:
form_with url: registration_pathresolves toPOST /registration.- Field names
:emailand:passwordare read verbatim byRegistrationsController#create. - The flash partial surfaces the mapped error message from
supabase_sign_up—WEAK_PASSWORD,PKCE_ERROR,SESSION_MISSINGcome through with the upstream copy; other 4xx errors are masked to a generic "Invalid credentials" line. See Controllers → Registrations for the full error policy.
passwords/new.html.erb
Forgot-password request form rendered at GET /passwords/new. Submits to POST /passwords, which routes to PasswordsController#create — see Controllers → Passwords.
<h1>Forgot your password?</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: passwords_path do |form| %>
<%= form.email_field :email, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
<%= form.submit "Email reset instructions" %>
<% end %>
<br>
<%= link_to "Back to sign in", new_session_path %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
email | email_field | required, autofocus, autocomplete="username", value: params[:email] | Single-field form — the address Supabase Auth will email a reset link to. |
Customisation contract:
form_with url: passwords_pathresolves toPOST /passwords.- Field name
:emailis read verbatim byPasswordsController#create. - On success the controller redirects to
new_session_pathwith thesupabase.rails.passwords.reset_sentflash — your customised template doesn't need to handle a "submitted" state inline.
passwords/edit.html.erb
Reset-password form rendered at GET /passwords/:token/edit after the user clicks the recovery link in their inbox. Submits to PUT /passwords/:token, which routes to PasswordsController#update.
<h1>Update your password</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: password_path(params[:token]), method: :put do |form| %>
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %><br>
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %><br>
<%= form.submit "Save" %>
<% end %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
password | password_field | required, autocomplete="new-password", maxlength: 72 | The new password the controller passes to supabase_update_user. |
password_confirmation | password_field | required, autocomplete="new-password", maxlength: 72 | Client-side parity check only — the controller does not read this. The browser's required attribute and your own JS (if any) are what catch mismatches. |
The :token URL segment is opaque to the controller
The params[:token] value embedded in the URL is not what authenticates the request. Supabase Auth uses the recovery session cookie that was set when the user clicked the email link. The :token segment exists only to give the URL a stable shape; the controller body reads params[:password] and nothing else. Don't try to validate params[:token] yourself — see Controllers → Passwords for the full flow.
Customisation contract:
form_with url: password_path(params[:token]), method: :put— preserve the:putmethod and the:tokenURL segment.- Field name
:passwordis read verbatim.:password_confirmationis client-side only; remove it if you don't want the second field.
otp/new.html.erb
One-time-code request form rendered at GET /otp/new. Submits to POST /otp, which routes to OtpController#create — see Controllers → OTP.
<h1>Sign in with a one-time code</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: otp_index_path do |form| %>
<%= form.email_field :email, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email] %><br>
<%= form.submit "Send me a code" %>
<% end %>
<br>
<%= link_to "Back to sign in", new_session_path %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
email | email_field | autofocus, autocomplete="username", value: params[:email] | Not marked required — the controller also accepts :phone, so adding a phone field means dropping the email's required. |
Customisation contract:
form_with url: otp_index_pathresolves toPOST /otp.- Field name
:emailis read by the controller; the controller also reads:phone(not in the default template) for SMS OTP flows. To add a phone option, drop a second field withname="phone"and removerequiredon the email field.
otp/verify.html.erb
One-time-code verify form rendered at GET /otp/verify and submitted at POST /otp/verify (the same route handles both). Routes to OtpController#verify.
<h1>Enter your code</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: verify_otp_index_path do |form| %>
<%= form.hidden_field :email, value: params[:email] %>
<%= form.hidden_field :phone, value: params[:phone] %>
<%= form.hidden_field :type, value: params[:type] || "email" %>
<%= form.text_field :token, required: true, autofocus: true, autocomplete: "one-time-code", inputmode: "numeric", placeholder: "Enter the code we just sent" %><br>
<%= form.submit "Verify" %>
<% end %>
<br>
<%= link_to "Back to sign in", new_session_path %>Fields:
| Field | Type | HTML attributes | Notes |
|---|---|---|---|
email | hidden_field | value: params[:email] | Round-trips the email from otp/new to otp/verify. |
phone | hidden_field | value: params[:phone] | Round-trips the phone from otp/new (when SMS OTP is in use). |
type | hidden_field | value: params[:type] || "email" | OTP type as defined by Supabase Auth ("email", "sms", "phone_change", …). Defaults to "email". |
token | text_field | required, autofocus, autocomplete="one-time-code", inputmode="numeric" | The 6-digit code from the email or SMS. inputmode="numeric" pops the numeric keypad on mobile; autocomplete="one-time-code" lets iOS/Safari auto-fill from SMS. |
Customisation contract:
form_with url: verify_otp_index_pathresolves toPOST /otp/verify. The same route renders the form on GET — see theverifyaction body for therequest.post?gating that turns GET into a render and POST into an exchange.- Field names
:email,:phone,:type,:tokenare all read byOtpController#verify. - Keep
autocomplete="one-time-code"— without it, iOS will not auto-fill the SMS code and mobile UX degrades sharply.
shared/_flash.html.erb
Shared partial rendered by every top-level template. Surfaces flash[:alert] (red) and flash[:notice] (green) so every gem-shipped form shows controller-set error and success messages consistently.
<%# Flash partial — shared by every gem-shipped view. Uses the conventional
`notice` / `alert` keys (AC-4). Hosts can override this partial by shipping
their own at `app/views/supabase/rails/shared/_flash.html.erb`. %>
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>Locals: none. The partial reads directly from the Rails flash hash.
Keys read: flash[:alert] and flash[:notice]. Other keys (e.g. flash[:warning]) are ignored — add them to your override if you set custom keys from a subclassed controller.
Why it matters: the action controllers set flash.now[:alert] on every failed branch (SessionsController#create on bad credentials, RegistrationsController#create on weak passwords, OtpController#verify on invalid codes, etc.). If you customise a top-level template and forget to render this partial, your users won't see any error feedback — sign-in just appears to silently re-render. Always keep render "supabase/rails/shared/flash" (or a host equivalent that reads flash[:alert]) in every form.
Overriding it: drop a file at app/views/supabase/rails/shared/_flash.html.erb in your host app. The partial accepts no locals, so the only thing to change is the markup. Because every form renders this same partial, a single edit propagates to sign-in, sign-up, password reset, password edit, OTP request, and OTP verify in one shot — making it the highest-leverage single override in the section.
A typical Tailwind replacement:
<%# app/views/supabase/rails/shared/_flash.html.erb %>
<% if flash[:alert] %>
<div class="mb-4 rounded-md bg-red-50 p-4 text-sm text-red-800">
<%= flash[:alert] %>
</div>
<% end %>
<% if flash[:notice] %>
<div class="mb-4 rounded-md bg-green-50 p-4 text-sm text-green-800">
<%= flash[:notice] %>
</div>
<% end %>oauth/_buttons.html.erb
Shared partial rendered by sessions/new.html.erb (unconditionally). Iterates Rails.application.config.supabase.oauth_providers and renders one "Sign in with <Provider>" link per entry. Each link kicks off the PKCE flow via OauthController#authorize.
<%# OAuth provider buttons. Renders one link per provider listed in
`config.supabase.oauth_providers` (defaults to none — host sets this in
the supabase initializer). Each link initiates the PKCE flow via
`OauthController#authorize`. %>
<% providers = (Rails.application.config.supabase.oauth_providers if defined?(::Rails)) %>
<% Array(providers).each do |provider| %>
<%= link_to "Sign in with #{provider.to_s.capitalize}", oauth_authorize_path(provider: provider), data: { turbo_method: :get } %><br>
<% end %>Locals: none. The partial reads Rails.application.config.supabase.oauth_providers directly. Wrap or replace with a partial that takes a local providers: argument if you want to render different button sets in different contexts.
Default state: config.supabase.oauth_providers defaults to [], so out of the box this partial renders nothing. Your sign-in page will show no OAuth row until you populate the list.
Enabling providers
Add the provider keys you want in config/initializers/supabase.rb:
Rails.application.config.supabase.oauth_providers = %i[google github]Each entry becomes one link, in the order listed. The string must match a provider id Supabase Auth recognises (google, github, azure, apple, discord, gitlab, linkedin, …) — the gem does not validate the list, so a typo silently sends users to a Supabase URL that returns an upstream error. See Configuration → oauth_providers for the full option reference.
You also need to enable the provider in the Supabase dashboard (Authentication → Providers) and configure the OAuth client there. The Rails-side list only controls which buttons render.
Disabling all OAuth
Leave config.supabase.oauth_providers as [] (the default) — the partial's Array(providers).each no-ops and the section vanishes. If you want to hide the section header / divider as well, override the partial and skip the wrapping markup when the list is empty:
<%# app/views/supabase/rails/oauth/_buttons.html.erb %>
<% providers = Array(Rails.application.config.supabase.oauth_providers) %>
<% if providers.any? %>
<div class="oauth-row">
<p class="oauth-row__divider">or continue with</p>
<% providers.each do |provider| %>
<%= link_to "Sign in with #{provider.to_s.capitalize}",
oauth_authorize_path(provider: provider),
class: "btn btn--oauth btn--#{provider}",
data: { turbo_method: :get } %>
<% end %>
</div>
<% end %>Disabling a single provider temporarily
Remove the entry from oauth_providers and restart the Rails server. No template change needed.
Customising the button copy or markup
Override the partial at app/views/supabase/rails/oauth/_buttons.html.erb. The contract worth preserving:
oauth_authorize_path(provider: provider)resolves toGET /oauth/:provider/authorize— the route theOauthController#authorizeaction handles. Change the helper and you break the OAuth start leg.data: { turbo_method: :get }— the default link uses Turbo. If your host disables Turbo, drop thedatahash.- The loop variable
provideris a Symbol or String exactly as it appears inoauth_providers. Useto_sbefore string interpolation.
Customising per-provider styling
Use the loop variable to vary classes / icons per provider:
<% Array(Rails.application.config.supabase.oauth_providers).each do |provider| %>
<%= link_to oauth_authorize_path(provider: provider),
class: "btn btn--oauth btn--#{provider}",
data: { turbo_method: :get } do %>
<%= image_tag "oauth/#{provider}.svg", alt: "", class: "icon" %>
Sign in with <%= provider.to_s.capitalize %>
<% end %>
<% end %>The implementation behind each button — PKCE state generation, signed-cookie verifier storage, callback exchange — lives in OauthController and RequestScopedStorage. The view layer's only job is the link itself.
Worked example: adding a logo and restyling with Tailwind
End-to-end: generate the templates, drop a logo at the top, restyle the sign-in form with Tailwind utilities, and update the shared partials once so the rest of the views inherit the look.
Step 1 — Copy the templates into your app
bin/rails generate supabase:viewsAll eight files land under app/views/supabase/rails/. See supabase:views for re-run semantics, the --skip upgrade workflow, and the manual rollback steps.
Step 2 — Add a logo to sessions/new.html.erb
Replace the gem default with a layout-wrapping version that puts a logo at the top, restyles the form with Tailwind classes, and keeps the four-piece controller contract intact:
<%# app/views/supabase/rails/sessions/new.html.erb %>
<div class="mx-auto flex min-h-screen max-w-sm flex-col justify-center px-6 py-12">
<%= image_tag "logo.svg", alt: "Acme", class: "mx-auto mb-8 h-10 w-auto" %>
<h1 class="text-center text-2xl font-semibold tracking-tight text-gray-900">
Sign in to your account
</h1>
<%= render "supabase/rails/shared/flash" %>
<%= form_with url: session_path, class: "mt-8 space-y-4" do |form| %>
<div>
<%= form.label :email, class: "block text-sm font-medium text-gray-700" %>
<%= form.email_field :email,
required: true,
autofocus: true,
autocomplete: "username",
value: params[:email],
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<div>
<%= form.label :password, class: "block text-sm font-medium text-gray-700" %>
<%= form.password_field :password,
required: true,
autocomplete: "current-password",
maxlength: 72,
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" %>
</div>
<%= form.submit "Sign in",
class: "w-full rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" %>
<% end %>
<div class="mt-6 flex justify-between text-sm">
<%= link_to "Forgot password?", new_password_path, class: "text-indigo-600 hover:text-indigo-500" %>
<%= link_to "Create an account", new_registration_path, class: "text-indigo-600 hover:text-indigo-500" %>
</div>
<%= render "supabase/rails/oauth/buttons" %>
</div>The contract preserved (verify against the sessions/new contract):
form_with url: session_path— still posts toPOST /session.:emailand:passwordfield names — unchanged, controller reads them as-is.maxlength: 72— preserved on the password field.render "supabase/rails/shared/flash"— flash partial still rendered, so bad-credential errors still surface.
Step 3 — Restyle shared/_flash.html.erb once for every form
This is the highest-leverage edit in the section — every form renders this partial, so a single Tailwind rewrite styles flash messages everywhere:
<%# app/views/supabase/rails/shared/_flash.html.erb %>
<% if flash[:alert] %>
<div class="mt-4 rounded-md bg-red-50 p-3 text-sm text-red-800" role="alert">
<%= flash[:alert] %>
</div>
<% end %>
<% if flash[:notice] %>
<div class="mt-4 rounded-md bg-green-50 p-3 text-sm text-green-800" role="status">
<%= flash[:notice] %>
</div>
<% end %>After this edit, sign-up errors, password-reset success messages, OTP "code sent" notices, and bad-credential warnings all pick up the new styling automatically — no per-template change required.
Step 4 — Restyle oauth/_buttons.html.erb with provider icons
Wrap the loop in a divider row and add per-provider classes (assumes oauth_providers is populated):
<%# app/views/supabase/rails/oauth/_buttons.html.erb %>
<% providers = Array(Rails.application.config.supabase.oauth_providers) %>
<% if providers.any? %>
<div class="mt-8">
<div class="relative">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white px-2 text-gray-500">or continue with</span>
</div>
</div>
<div class="mt-6 grid grid-cols-1 gap-3">
<% providers.each do |provider| %>
<%= link_to oauth_authorize_path(provider: provider),
class: "inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50",
data: { turbo_method: :get } do %>
<%= image_tag "oauth/#{provider}.svg", alt: "", class: "mr-2 h-5 w-5" %>
Sign in with <%= provider.to_s.capitalize %>
<% end %>
<% end %>
</div>
</div>
<% end %>The contract preserved (verify against the oauth/_buttons contract):
oauth_authorize_path(provider: provider)— same URL helper, OAuth start leg still hits the gem's controller.data: { turbo_method: :get }— Turbo behaviour preserved.Array(...)guard — still no-ops whenoauth_providersis empty.
Step 5 — Apply the same shape to the remaining forms
registrations/new, passwords/new, passwords/edit, otp/new, and otp/verify all follow the same skeleton as sessions/new: a heading, the flash partial, a form_with with its specific URL helper and field whitelist, and a back-link. Copy the layout wrapper + field grouping from Step 2 across each, preserving each template's per-form contract from the per-template sections above.
Step 6 — Verify in the browser
bin/rails serverHit http://localhost:3000/session/new, then /registration/new, /passwords/new, /otp/new. The logo and Tailwind styles should render across all of them. To prove the override is active (not the gem still serving its own copy), temporarily rename app/views/supabase/rails/sessions/new.html.erb to .bak — the page should fall back to the gem's bare default on the next reload.
See also
supabase:views— generator that drops these templates into your app, plus the full override-precedence mechanics, re-run semantics, and rollback steps.- Configuration →
oauth_providers— populate this to make the OAuth buttons partial render anything. - Controllers — the five base controllers that render these templates, including each action's params whitelist and outcome dispatch table.
- Controllers → Sessions · Registrations · Passwords · OTP · OAuth — per-controller pages with the full action bodies and override patterns.
- Authentication — the
Authenticationconcern and the helpers (authenticated?,current_user) available inside customised templates.
OauthController
Supabase::Rails::OauthController — OAuth 2.0 + PKCE provider sign-in.
Migrating from Rails 8 auth
Step-by-step migration from the Rails 8 bin/rails g authentication generator to supabase-rails — artifact mapping, migration steps for the User model, session controller, password reset, routes, tests, plus rollback points and a verification checklist.