Documentation
ReferenceJavaScript SDKEvents

Events

React to sign-in, sign-out, token refresh and sudo, wherever they happen.

Authentication changes in places your components did not initiate: a token refreshes on a timer, a refresh is rejected while the user is reading, another tab signs out. Events are how the rest of your app finds out.

Events exist on the browser client only. createServerClient has no on() — a server holds no session to change, so there is nothing to broadcast.

on

on(event, handler) → () => void

Subscribes, and returns the function that unsubscribes. Return it from an effect and the framework does the cleanup:

useEffect(() => {
  const off = nulvless.auth.on('signed-in', (session) => {
    setUser(session.user);
  });

  return off;
}, []);

Forgetting to unsubscribe is the usual bug here — a component that remounts ten times ends up handling every event ten times.

A handler that throws is logged and skipped. The other handlers still run, and the call that emitted the event — a signIn, a refresh — is not affected.

const offIn = nulvless.auth.on('signed-in', (session) => setUser(session.user));
const offOut = nulvless.auth.on('signed-out', () => setUser(null));
const offDead = nulvless.auth.on('unauthorized', () => setUser(null));

// later
offIn();
offOut();
offDead();

The events

signed-inAuthenticatedOptional

A sign-in or sign-up succeeded and the token was stored. Carries the full session, so you rarely need to fetch anything in the handler.

signed-out{ userId? }Optional

signOut succeeded. Fires after the server confirms, not when you call it — a failed sign-out emits nothing, and the client stays consistent.

challengeAuthenticatedOptional

The server answered with a challenge and no token: MFA, an OTP step, a sign-up confirmation. Useful for routing to the verification screen from one place instead of at every call site.

token-refreshedAuthenticatedOptional

The access token was renewed in the background — in this tab or in another tab of the same project. You do not need to act on it — it is there for logging, or to re-send a token you handed to something else.

unauthorizedAuthenticatedOptional

The SDK cannot authenticate any more: there is no refresh token, or a refresh was rejected. This is the session-is-over signal — clear your state and route to sign-in.

payload

Carries the session that failed, including its user when known, so a multi-account app can remove the right entry.

sudo-requiredvoidOptional

A request failed with 403 protected_action_denied. Start the sudo flow and retry. No payload — the event is the whole message.

account-switched{ userId }Optional

auth.switch() succeeded. Only meaningful with multi-account enabled.

The two that matter most

Everything else is optional. These two are not:

nulvless.auth.on('unauthorized', () => {
  clearAppState();
  router.push('/sign-in');
});

nulvless.auth.on('sudo-required', async () => {
  await nulvless.auth.sessions.sudo();
  openSudoPrompt();
});

A token-refreshed handler is worth having when something outside the SDK holds the token:

nulvless.auth.on('token-refreshed', (session) => {
  socket.updateAuth(session.access_token);
});

unauthorized is the only reliable way to learn the session died without you doing anything — polling getSession is both slower and wrong. And handling sudo-required centrally means you never have to remember which calls are protected, which is per-environment configuration you do not control.

unauthorized is not the same as a 401 from one call. A single 401 may just mean an expired access token, which the SDK refreshes and retries. This event fires only once refreshing has failed — the session is genuinely over.

Next

On this page