supabase-rb-rb
Generators

supabase:views

Copies the gem's eight default auth view templates into app/views/supabase/rails/ so they can be customised — Rails' view-resolution order picks the host copies ahead of the gem's.

bin/rails generate supabase:views copies the eight default ERB templates the gem ships under app/views/supabase/rails/ into the host app at the same path. Standard Rails view resolution then picks the host copies ahead of the engine's — there is no prepend_view_path to manage, no opt-in flag to flip — so any edit you make to a copied file takes effect on the next request.

bin/rails generate supabase:views

Expected output on a fresh app that has already run supabase:install:

   create  app/views/supabase/rails/oauth/_buttons.html.erb
   create  app/views/supabase/rails/otp/new.html.erb
   create  app/views/supabase/rails/otp/verify.html.erb
   create  app/views/supabase/rails/passwords/edit.html.erb
   create  app/views/supabase/rails/passwords/new.html.erb
   create  app/views/supabase/rails/registrations/new.html.erb
   create  app/views/supabase/rails/sessions/new.html.erb
   create  app/views/supabase/rails/shared/_flash.html.erb

When to run this

Run it when you want to restyle the default forms, add fields, swap in your design system, or replace the bare ERB with Hotwire/View Components. Skip it if the gem's defaults are good enough — the templates are functional out of the box and you can ship to production without ever touching them.

Files created

Eight files, all copied byte-verbatim from the gem's app/views/supabase/rails/ directory. Files have .html.erb extensions (not .tt), so there is no ERB pre-processing pass — what's in the gem is what lands in your repo.

PathWhat it rendersMounted at
app/views/supabase/rails/sessions/new.html.erbSign-in form: email + password, plus links to "Forgot password?" / "Create an account" and the oauth/_buttons partial.GET /session/new
app/views/supabase/rails/registrations/new.html.erbSign-up form: email + password, plus a link back to the sign-in page.GET /registration/new
app/views/supabase/rails/passwords/new.html.erb"Forgot password" request form: email only, posts to POST /passwords to trigger the reset email.GET /passwords/new
app/views/supabase/rails/passwords/edit.html.erb"Reset password" form: password + confirmation. Posts to PUT /passwords/:token. Rendered when the user clicks the reset link in their email.GET /passwords/:token/edit
app/views/supabase/rails/otp/new.html.erbOne-time-code request form: email only, posts to POST /otp to send the code.GET /otp/new
app/views/supabase/rails/otp/verify.html.erbOne-time-code verify form: hidden email/phone/type fields + the visible 6-digit token field. Posts to POST /otp/verify.GET /otp/verify
app/views/supabase/rails/oauth/_buttons.html.erbPartial. Iterates Rails.application.config.supabase.oauth_providers (defaults to []) and renders one "Sign in with <Provider>" link per entry. Each link kicks off the PKCE flow via OauthController#authorize. Rendered by sessions/new.html.erb unconditionally.partial
app/views/supabase/rails/shared/_flash.html.erbPartial. Renders flash[:alert] in red and flash[:notice] in green. Rendered by every gem-shipped form so sign-in errors and "check your email" success messages surface consistently.partial

oauth/_buttons.html.erb reads Rails.application.config.supabase.oauth_providers — the default is an empty array, so the sign-in form renders no OAuth buttons until you populate that list in config/initializers/supabase.rb. See Configuration → oauth_providers for the list of supported providers and what each link's href resolves to.

Override precedence

Rails resolves view templates in the order their paths appear on ActionController::Base.view_paths. The host app's app/views/ is always first; engine-added paths come after. The gem's Supabase::Rails::Engine adds its own app/views/ to the path, but after the host's, so any file the host ships at the same relative path wins.

The resolution order for render "supabase/rails/sessions/new" is:

  1. <your-app>/app/views/supabase/rails/sessions/new.html.erb — if present, this wins.
  2. <gem>/app/views/supabase/rails/sessions/new.html.erb — the fallback the gem ships.

The generator's only job is to drop the gem's copy into bucket 1 so you have something to edit. There is no flag on the controller, no prepend_view_path call to make, no initializer toggle. Copy → edit → reload is the entire override loop.

This applies file-by-file:

  • Copy only sessions/new.html.erb and the other seven still resolve from the gem.
  • Delete a host copy and Rails silently falls back to the gem version on the next request.
  • The oauth/_buttons and shared/_flash partials follow the same precedence rule — overriding them changes how every gem-shipped form renders, because each top-level template renders the shared partials by relative path (render "supabase/rails/shared/flash", render "supabase/rails/oauth/buttons").

No initializer change needed

Unlike supabase:user_model, this generator does not touch config/initializers/supabase.rb. It only writes the eight ERB files. Once they exist on disk, Rails' standard view-path resolution does the rest.

Worked example: customising the sign-in form

The default sessions/new.html.erb is intentionally minimal — bare <h1>, no styling, no field grouping. Most apps will want to wrap it in their layout's form components. Here's the full edit loop end to end.

Step 1 — Run the generator

bin/rails generate supabase:views

This drops the eight templates into app/views/supabase/rails/. Confirm with:

ls app/views/supabase/rails/sessions/
# new.html.erb

Step 2 — Inspect the default template

app/views/supabase/rails/sessions/new.html.erb (the file the generator just copied):

<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" %>

The contract worth preserving across any edit:

  • form_with url: session_path — posts to POST /session, which Supabase::Rails::SessionsController#create handles. Change the URL helper and you break sign-in.
  • email and password are the field names the controller reads from params — rename them and the controller will see nil.
  • maxlength: 72 matches Supabase Auth's bcrypt password limit. Lowering it is fine; raising it lets users enter passwords Supabase will reject.
  • render "supabase/rails/shared/flash" surfaces the controller's sign-in-error messages. Drop it and failed sign-ins will silently re-render the form.

Step 3 — Replace the markup with your design system

Wrap the form in your app's standard layout helpers — here a fictional card / field_group / primary_button set:

<%# app/views/supabase/rails/sessions/new.html.erb %>
<%= render layout: "shared/card", locals: { title: "Welcome back" } do %>
  <%= render "supabase/rails/shared/flash" %>

  <%= form_with url: session_path, class: "auth-form" do |form| %>
    <%= render "shared/field_group", label: "Email", field: form.email_field(:email,
      required:     true,
      autofocus:    true,
      autocomplete: "username",
      placeholder:  "you@example.com",
      value:        params[:email],
      class:        "input input--full"
    ) %>

    <%= render "shared/field_group", label: "Password", field: form.password_field(:password,
      required:     true,
      autocomplete: "current-password",
      placeholder:  "••••••••",
      maxlength:    72,
      class:        "input input--full"
    ) %>

    <%= form.submit "Sign in", class: "btn btn--primary btn--full" %>
  <% end %>

  <div class="auth-form__links">
    <%= link_to "Forgot your password?", new_password_path, class: "link" %>
    <%= link_to "Create an account", new_registration_path, class: "link" %>
  </div>

  <%= render "supabase/rails/oauth/buttons" %>
<% end %>

The structural changes only — the four contract pieces above (URL helper, field names, maxlength: 72, flash partial render) are preserved verbatim, so the controller round-trip still works.

Step 4 — Reload and verify

No restart needed — Rails picks up view-template changes on the next request in development:

bin/rails server

Open http://localhost:3000/session/new. The host's restyled form should render. To prove the override is doing the work (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.

Step 5 — Style the shared partials too

If you styled the sign-in form, the same look almost certainly applies to the other seven templates. The two highest-leverage edits:

  • shared/_flash.html.erb — every form renders it, so a single change updates sign-in error styling, password-reset success messages, OTP "code sent" notices, and registration errors all at once.
  • oauth/_buttons.html.erb — the loop body controls how every OAuth button looks. Replace link_to "Sign in with #{provider.to_s.capitalize}" with a button_to + provider-specific icon and you have a polished social sign-in row.

The other five top-level forms (registrations/new, passwords/new+edit, otp/new+verify) follow the same shape as sessions/new: an <h1>, the flash partial, a form_with with a session_path-style URL helper, the field whitelist the controller reads from params, and a back-link. Apply the same restyling pattern to each.

Re-running and rolling back

The generator uses Thor's directory action, which compares each source file against its destination:

  • Files that don't exist in the host print create and are written.
  • Files that are byte-identical to the gem source print identical and are not rewritten.
  • Files that differ trigger Thor's standard [Ynaqdh] overwrite prompt — Y overwrites, n keeps the host copy, d shows a diff, a overwrites all remaining conflicts, q aborts. (Same prompt as supabase:install — see that page for the full key reference.)

The flags worth knowing — all inherited from Thor, the generator declares none of its own:

FlagEffect
--force, -fOverwrite every host file that has diverged, without prompting. Use when you want to reset your customisations and start from the current gem defaults.
--skip, -sKeep host copies for every file that has diverged. Useful when re-running after a gem upgrade to pick up new view files without losing your edits to existing ones.
--pretend, -pDry-run. Prints the create/identical/conflict log lines without writing anything.
--quiet, -qSuppress the log lines.

After a gem upgrade that adds a new view template, the typical workflow is:

bin/rails generate supabase:views --skip

--skip keeps every file you've customised, but new files (which don't exist in the host) are still created. This is the lowest-friction way to stay in sync with the gem's view surface without an interactive prompt for each existing file.

To roll back manually — there is no supabase:views uninstall:

  1. Delete the file you no longer want to customise (e.g. rm app/views/supabase/rails/sessions/new.html.erb).
  2. Rails immediately falls back to the gem's bundled version on the next request — no restart, no config change.

Delete the entire app/views/supabase/ tree to revert every template at once.

See also

  • supabase:install — the prerequisite generator that wires up the controllers these templates render under.
  • Configuration → oauth_providers — populate this to make the oauth/_buttons partial render anything.
  • Authentication — the Authentication concern and the helpers (authenticated?, current_user) available inside customised templates.
  • Controllers — the five base controllers that render these templates, including the params whitelist each one reads from the forms.
  • Generators — the index page for all three generators.

On this page