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.callThe 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
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:
| Event | Fired by |
|---|---|
SIGNED_IN | sign_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_OUT | sign_out (any scope other than "others"). |
TOKEN_REFRESHED | Auto-refresh timer firing, or an explicit refresh_session call. |
USER_UPDATED | A successful update_user PUT. |
MFA_CHALLENGE_VERIFIED | mfa.verify / mfa.challenge_and_verify succeeded and rotated the bearer to aal2. |
PASSWORD_RECOVERY | initialize_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.callExample — unsubscribe cleanup with ensure
subscription = supabase.auth.on_auth_state_change { |event, _| audit(event) }
begin
run_authenticated_workflow(supabase)
ensure
subscription.unsubscribe.call
endExample — 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
endThis 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 aQueueand 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_subscribersdoes notrescue— a raise inside your block aborts iteration over remaining subscribers and propagates up. On the Timer thread it is swallowed by the timer's outerrescue StandardError, but on caller threads it bubbles into the method that triggered the event (e.g.sign_in_with_passwordwould re-raise your error). Wrap risky work inbegin/rescueyourself. - The async variant (
Supabase::Auth::Async::Client) dispatches on the same Ruby threads described above — there is no separate fiber-based dispatch.