supabase-rb-rb
Auth

Listen to auth events

Subscribe to auth state changes — SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, and friends.

Register a block that is invoked every time the auth state changes. Returns a Types::Subscription whose :unsubscribe lambda removes the callback from the emitter list. Each subscription gets a SecureRandom.uuid id, so a single client can hold many independent listeners.

The block receives two positional arguments: an event name (String) and the current session (Types::Session or nil). The block is always invoked with the latest session at the moment the event fires — including nil for SIGNED_OUT.

Signature

subscription = supabase.auth.on_auth_state_change do |event, session|  # ...endsubscription.unsubscribe.call

The block form is the only public API — there is no on_auth_state_change(callback) positional variant. Block arity is |event, session|.

Parameters

This method takes no positional arguments. It takes a single required block.

Returns

Returns
Supabase::Auth::Types::Subscription

A Struct with :id (the subscription's SecureRandom.uuid), :callback (the block itself), and :unsubscribe (a lambda that, when called with no arguments, removes this subscription from the client's emitter map). Hold onto the returned value and call subscription.unsubscribe.call when you no longer want to receive events — letting the value go out of scope does not unsubscribe.

Events

The dispatched event vocabulary, as emitted by auth/client.rb:

EventFired by
SIGNED_INsign_in_with_password, sign_in_with_otp (after verify), sign_in_with_id_token, sign_in_anonymously, set_session, exchange_code_for_session, initialize_from_storage (when a stored session is recovered), initialize_from_url (implicit OAuth redirect).
SIGNED_OUTsign_out (any scope other than "others").
TOKEN_REFRESHEDAuto-refresh timer firing, or an explicit refresh_session call.
USER_UPDATEDA successful update_user PUT.
MFA_CHALLENGE_VERIFIEDmfa.verify / mfa.challenge_and_verify succeeded and rotated the bearer to aal2.
PASSWORD_RECOVERYinitialize_from_url detected a redirect_type=recovery fragment.

Example — react to sign-in, sign-out, and token refresh

require "supabase"

supabase = Supabase.create_client(
  supabase_url: ENV.fetch("SUPABASE_URL"),
  supabase_key: ENV.fetch("SUPABASE_ANON_KEY")
)

subscription = supabase.auth.on_auth_state_change do |event, session|
  case event
  when "SIGNED_IN"
    puts "Signed in as #{session.user.email}"
  when "SIGNED_OUT"
    puts "Signed out"
  when "TOKEN_REFRESHED"
    puts "Token rotated; new access_token expires at #{Time.at(session.expires_at)}"
  end
end

supabase.auth.sign_in_with_password(email: "ada@example.com", password: "secret")
# => SIGNED_IN

supabase.auth.sign_out
# => SIGNED_OUT

subscription.unsubscribe.call

Example — unsubscribe cleanup with ensure

subscription = supabase.auth.on_auth_state_change { |event, _| audit(event) }

begin
  run_authenticated_workflow(supabase)
ensure
  subscription.unsubscribe.call
end

Example — fan out into a Queue for thread-safe consumption

events = Queue.new

supabase.auth.on_auth_state_change do |event, session|
  events.push([event, session])
end

# Consume from the main thread (or any other thread of your choice).
Thread.new do
  loop do
    event, session = events.pop
    handle_event(event, session)
  end
end

This pattern decouples the dispatcher thread (which may be the auto-refresh Timer thread — see the callout below) from your business logic.

Threading semantics — your block may run on the auto-refresh Timer thread

_notify_all_subscribers is called inline from the method that triggered the event, so SIGNED_IN / SIGNED_OUT / USER_UPDATED / MFA_CHALLENGE_VERIFIED arrive on the thread that called sign_in_with_password / sign_out / etc., while TOKEN_REFRESHED from the auto-refresh path arrives on the background Supabase::Auth::Timer thread (auth/timer.rb, a plain Thread.new).

Practical consequences:

  • Treat the block as multi-threaded. Any mutable state you touch from it (instance vars, arrays, hashes) needs a Mutex — or push events to a Queue and consume them from a single owner thread, as in the example above.
  • Don't do long-running work in the block. A blocking callback delays every subsequent subscriber and, if it runs on the Timer thread, delays the next auto-refresh schedule.
  • Exceptions are not caught. _notify_all_subscribers does not rescue — a raise inside your block aborts iteration over remaining subscribers and propagates up. On the Timer thread it is swallowed by the timer's outer rescue StandardError, but on caller threads it bubbles into the method that triggered the event (e.g. sign_in_with_password would re-raise your error). Wrap risky work in begin/rescue yourself.
  • The async variant (Supabase::Auth::Async::Client) dispatches on the same Ruby threads described above — there is no separate fiber-based dispatch.

On this page