supabase-rb-rb
Controllers

PasswordsController

Supabase::Rails::PasswordsController — the two-step password reset flow.

Supabase::Rails::PasswordsController handles the two-step password reset flow: request a reset email, then set the new password via the recovery deep-link. The install generator writes a top-level PasswordsController that inherits from it.

Under the hood, create wraps the supabase-rb auth.reset_password_for_email call (via supabase_reset_password); update wraps auth.update_user (via supabase_update_user).

# app/controllers/passwords_controller.rb (written by `bin/rails generate supabase:install`)
class PasswordsController < Supabase::Rails::PasswordsController
end

Source

# app/controllers/supabase/rails/passwords_controller.rb (in the gem)
class PasswordsController < BaseController
  allow_unauthenticated_access only: %i[new create edit update]

  def new; end

  def create
    result = supabase_reset_password(email: params[:email])
    if result.success?
      redirect_to new_session_path,
                  notice: I18n.t("supabase.rails.passwords.reset_sent")
    else
      flash.now[:alert] = result.error.message
      render :new, status: :unprocessable_entity
    end
  end

  def edit; end

  def update
    result = supabase_update_user(password: params[:password])
    if result.success?
      redirect_to new_session_path,
                  notice: I18n.t("supabase.rails.passwords.updated")
    else
      flash.now[:alert] = result.error.message
      render :edit, status: :unprocessable_entity
    end
  end
end

Routes

HelperVerbURLAction
new_password_pathGET/passwords/newnew
passwords_pathPOST/passwordscreate
edit_password_path(token)GET/passwords/:token/editedit
password_path(token)PATCH/PUT/passwords/:tokenupdate

Mounted by resources :passwords, only: %i[new create edit update], param: :token inside supabase_authentication_routes. The param: :token makes the dynamic segment :token instead of the default :id.

The `:token` segment is opaque to the gem

The :token route segment exists so the URL the recovery email points the user at is shaped like /passwords/<recovery_token>/edit. The action body never reads params[:token] — Supabase Auth uses a session cookie (set by the recovery deep-link redirect) as the credential. The path param is presentational only; you can leave it any value the recovery email puts there.

Actions

new

Renders the "Forgot your password?" form at /passwords/new. Action body is empty. Allowed without authentication.

The shipped view at app/views/supabase/rails/passwords/new.html.erb submits to passwords_path with one field (email).

create

Triggers the password-reset email. Allowed without authentication.

Params

ParamSourceNotes
emailPOST bodyRequired. Forwarded to Supabase Auth verbatim.

Outcome dispatch

BranchRedirect / renderStatusFlash
Successredirect_to new_session_path302notice: I18n.t("supabase.rails.passwords.reset_sent")
Failure — 4xxrender :new422flash.now[:alert] = result.error.message
Failure — 5xxrender :new422flash.now[:alert] = result.error.message

The success branch fires whether or not the email address exists in Supabase Auth — the upstream call returns success either way, preventing user enumeration. The Supabase project's email templates control the recovery email content and link.

The recovery deep-link must point back at `edit_password_path`

By default Supabase Auth sends the user to <SUPABASE_URL>/auth/v1/verify?... which redirects to the Site URL configured for the project (Authentication → URL Configuration in the dashboard). Set that Site URL to your app's origin so the user lands on <your-app>/passwords/<token>/edit with a recovery session cookie already set. Add additional callback URLs under Redirect URLs for staging / preview environments.

edit

Renders the "Update your password" form at /passwords/:token/edit. Action body is empty — Rails renders passwords/edit.html.erb. Allowed without authentication.

The user arrives here after clicking the recovery email link. Supabase Auth's verify endpoint redirects to this URL with a recovery session cookie already set on the host's domain, so update can talk to Supabase as the authenticated user even though no manual sign-in has happened.

The shipped view at app/views/supabase/rails/passwords/edit.html.erb submits to password_path(params[:token]) with two fields (password, password_confirmation).

update

Sets the new password and redirects to sign-in. Allowed without authentication (the recovery session cookie is what's verifying the user).

Params

ParamSourceNotes
passwordPATCH/PUT bodyRequired. Max 72 bytes (bcrypt limit).
tokenURL pathIgnored by the action body. Presentational — Supabase Auth uses the recovery session cookie.

Outcome dispatch

BranchRedirect / renderStatusFlash
Successredirect_to new_session_path302notice: I18n.t("supabase.rails.passwords.updated")
Failure — weak passwordrender :edit422flash.now[:alert] = result.error.message (WEAK_PASSWORD mapper message)
Failure — missing sessionrender :edit422flash.now[:alert] = result.error.message (SESSION_MISSING — recovery cookie expired or never set)
Failure — other 4xx/5xxrender :edit422flash.now[:alert] = result.error.message

The success branch deliberately redirects to new_session_path, not after_authentication_url — Supabase Auth invalidates the recovery session as soon as the password update lands, so the user must sign in again with the new password.

The SESSION_MISSING branch fires when the user lands on the page without a valid recovery cookie (link expired, opened in a different browser, etc.). The flash surfaces the mapper's "Session missing" message; restart the flow from new_password_path.

allow_unauthenticated_access

allow_unauthenticated_access only: %i[new create edit update]

All four actions are exempt. Even edit / update — the recovery session cookie is what Supabase Auth reads, not the standard authenticated-user cookie.

Hookable callbacks

HookDefaultWhere to override
after_authentication_urlstored_location_for_redirect || root_urlNot used by this controller — both success redirects target new_session_path directly because Supabase invalidates the recovery session on update.

For a different post-update destination (e.g. an in-app banner instead of forcing a re-sign-in flow), override update in the host subclass:

class PasswordsController < Supabase::Rails::PasswordsController
  def update
    result = supabase_update_user(password: params[:password])
    if result.success?
      redirect_to login_help_path, notice: t(".updated")
    else
      flash.now[:alert] = result.error.message
      render :edit, status: :unprocessable_entity
    end
  end
end

Override patterns

Custom redirect_to: on the reset email. supabase_reset_password accepts a redirect_to: keyword that becomes the URL the recovery email's "Reset password" button points at — useful for routing to a /auth/recovery page that performs additional verification before exposing the password form. The URL is validated against config.supabase.allowed_redirect_origins by RedirectValidator before the upstream call:

class PasswordsController < Supabase::Rails::PasswordsController
  def create
    result = supabase_reset_password(
      email: params[:email],
      redirect_to: edit_password_url(token: "recovery"),
    )
    if result.success?
      redirect_to new_session_path, notice: t(".sent")
    else
      flash.now[:alert] = result.error.message
      render :new, status: :unprocessable_entity
    end
  end
end

A redirect_to: outside the allowlist fails fast with Result.failure(AuthError(INVALID_REDIRECT, 400)) without ever bothering the upstream — an attacker who forges a sign-up form with redirect_to=https://evil.example.com cannot redirect users via this controller.

Password-confirmation parity check. The shipped view sends a password_confirmation field; the action body ignores it because Supabase Auth handles the canonical password validation. To enforce same-value before calling the upstream, override update:

class PasswordsController < Supabase::Rails::PasswordsController
  def update
    if params[:password] != params[:password_confirmation]
      flash.now[:alert] = t(".confirmation_mismatch")
      return render :edit, status: :unprocessable_entity
    end
    super
  end
end

Replacing with a from-scratch controller — point the host subclass at Supabase::Rails::BaseController and call supabase_update_user (or any of the other low-level helpers) directly. The helper requires a valid encrypted session cookie — when the cookie is missing or carries no access token, it fast-fails with Result.failure(AuthError.session_missing) (401, SESSION_MISSING) before reaching the upstream.

See also

On this page