supabase-rb-rb
Views

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:

TemplateRendersSubmits toParams readPartials
sessions/new.html.erbSign-in form: email + password, with "Forgot password?" and "Create an account" links.POST /session:email, :passwordshared/_flash, oauth/_buttons
registrations/new.html.erbSign-up form: email + password, with a link back to sign-in.POST /registration:email, :passwordshared/_flash
passwords/new.html.erbForgot-password request: email only.POST /passwords:emailshared/_flash
passwords/edit.html.erbReset-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.erbOne-time-code request: email only.POST /otp:email, :phoneshared/_flash
otp/verify.html.erbOne-time-code verify: hidden email/phone/type + visible token field.POST /otp/verify:token, :type, :email, :phoneshared/_flash

Partials:

PartialRendered byLocalsReads from
shared/_flash.html.erbAll six top-level templates above.None.flash[:notice], flash[:alert]
oauth/_buttons.html.erbsessions/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:

FieldTypeHTML attributesNotes
emailemail_fieldrequired, autofocus, autocomplete="username", value: params[:email]Pre-fills from params[:email] so a failed sign-in keeps the email the user typed.
passwordpassword_fieldrequired, autocomplete="current-password", maxlength: 72maxlength: 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 to POST /session.
  • Field names :email and :password — the controller reads these from params verbatim.
  • render "supabase/rails/shared/flash" — surfaces flash.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 when config.supabase.oauth_providers is 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:

FieldTypeHTML attributesNotes
emailemail_fieldrequired, autofocus, autocomplete="username", value: params[:email]Pre-fills after a WEAK_PASSWORD re-render so the user doesn't retype the email.
passwordpassword_fieldrequired, autocomplete="new-password", maxlength: 72Use autocomplete="new-password" (not current-password) so password managers offer to generate.

Customisation contract:

  • form_with url: registration_path resolves to POST /registration.
  • Field names :email and :password are read verbatim by RegistrationsController#create.
  • The flash partial surfaces the mapped error message from supabase_sign_upWEAK_PASSWORD, PKCE_ERROR, SESSION_MISSING come 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:

FieldTypeHTML attributesNotes
emailemail_fieldrequired, 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_path resolves to POST /passwords.
  • Field name :email is read verbatim by PasswordsController#create.
  • On success the controller redirects to new_session_path with the supabase.rails.passwords.reset_sent flash — 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:

FieldTypeHTML attributesNotes
passwordpassword_fieldrequired, autocomplete="new-password", maxlength: 72The new password the controller passes to supabase_update_user.
password_confirmationpassword_fieldrequired, autocomplete="new-password", maxlength: 72Client-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 :put method and the :token URL segment.
  • Field name :password is read verbatim. :password_confirmation is 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:

FieldTypeHTML attributesNotes
emailemail_fieldautofocus, 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_path resolves to POST /otp.
  • Field name :email is 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 with name="phone" and remove required on 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:

FieldTypeHTML attributesNotes
emailhidden_fieldvalue: params[:email]Round-trips the email from otp/new to otp/verify.
phonehidden_fieldvalue: params[:phone]Round-trips the phone from otp/new (when SMS OTP is in use).
typehidden_fieldvalue: params[:type] || "email"OTP type as defined by Supabase Auth ("email", "sms", "phone_change", …). Defaults to "email".
tokentext_fieldrequired, 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_path resolves to POST /otp/verify. The same route renders the form on GET — see the verify action body for the request.post? gating that turns GET into a render and POST into an exchange.
  • Field names :email, :phone, :type, :token are all read by OtpController#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 to GET /oauth/:provider/authorize — the route the OauthController#authorize action 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 the data hash.
  • The loop variable provider is a Symbol or String exactly as it appears in oauth_providers. Use to_s before 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:views

All 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 to POST /session.
  • :email and :password field 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 when oauth_providers is 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 server

Hit 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 Authentication concern and the helpers (authenticated?, current_user) available inside customised templates.

On this page