Authentication overview
convex-angular keeps a single, reactive picture of authentication in the root injector. An
external identity provider (Clerk, Auth0, Better Auth, or your own service) reports whether a
user is signed in and hands over a JWT; the Convex client validates that JWT against your
deployment. Only when both sides agree does the library report the user as authenticated.
injectAuth() is how you read that picture.
injectAuth()
Section titled “injectAuth()”function injectAuth(options?: InjectAuthOptions): ConvexAuthState;
interface InjectAuthOptions { /** * Environment injector used to create the auth helper outside the current * injection context. */ injectRef?: EnvironmentInjector;}The state object is created once per root injector and shared: repeated injectAuth() calls
return the same ConvexAuthState instance, including from child injectors.
import { Component } from '@angular/core';import { injectAuth } from 'convex-angular';
@Component({ selector: 'app-auth-status', template: ` @switch (auth.status()) { @case ('loading') { <p>Checking your session…</p> } @case ('authenticated') { <p>Signed in.</p> } @case ('refreshing') { <!-- Still signed in: the socket is paused while a replacement token is fetched. --> <p>Signed in.</p> <p role="status">Reconnecting your session…</p> } @case ('unauthenticated') { <p>Signed out.</p> } }
@if (auth.error(); as error) { <p role="alert">{{ error.message }}</p> } `,})export class AuthStatusComponent { readonly auth = injectAuth();
// `getAuth()` is a snapshot method, not a signal: read it on demand, right // before you need the token. It returns undefined when no token is set and // during server-side rendering. callExternalApi(): Promise<Response> { const snapshot = this.auth.getAuth(); if (!snapshot) { return Promise.reject(new Error('No Convex token available.')); }
const userId = snapshot.decoded['sub']; return fetch(`/api/reports/${String(userId)}`, { headers: { Authorization: `Bearer ${snapshot.token}` }, }); }}ConvexAuthStatus
Section titled “ConvexAuthStatus”type ConvexAuthStatus = 'loading' | 'authenticated' | 'refreshing' | 'unauthenticated';| Status | Meaning |
|---|---|
'loading' |
Auth state is not yet known. Either the upstream provider is still initializing, or the provider reports the user as signed in and Convex has not yet confirmed or rejected the token. |
'authenticated' |
The provider reports the user as signed in and the Convex backend confirmed the token. Queries and mutations run with an identity. |
'refreshing' |
The server rejected a previously-confirmed token and Convex paused the socket while it fetches a replacement. The user remains authenticated throughout. |
'unauthenticated' |
Auth has settled and there is no confirmed identity — the provider reports signed out, no token was available, or Convex rejected the token. |
ConvexAuthState
Section titled “ConvexAuthState”interface ConvexAuthState { isLoading: Signal<boolean>; isAuthenticated: Signal<boolean>; isRefreshing: Signal<boolean>; error: Signal<Error | undefined>; status: Signal<ConvexAuthStatus>; getAuth: () => { token: string; decoded: Record<string, unknown> } | undefined;}Two inputs feed every member: the upstream provider’s own isLoading / isAuthenticated
signals, and Convex’s backend confirmation, which is internally tri-state — null while a
decision is pending, true once confirmed, false once rejected or absent.
| Member | Type | Exact derivation |
|---|---|---|
isLoading |
Signal<boolean> |
provider.isLoading() || (provider.isAuthenticated() && backendConfirmation === null). True while the provider initializes, and also while the provider says “signed in” but Convex has not answered yet. |
isAuthenticated |
Signal<boolean> |
provider.isAuthenticated() && backendConfirmation === true. Requires both sides. Stays true while isRefreshing() is true. |
isRefreshing |
Signal<boolean> |
isAuthenticated() && backendRefreshing. Can only ever be true while isAuthenticated() is true. |
error |
Signal<Error | undefined> |
The most recent of two independently tracked sources — the optional provider-owned error signal, and internal token/sync failures — ordered by a shared monotonic sequence so the newer one wins. Normal signed-out outcomes (a null token) never set it. |
status |
Signal<ConvexAuthStatus> |
isLoading() → 'loading'; else isAuthenticated() → isRefreshing() ? 'refreshing' : 'authenticated'; else 'unauthenticated'. |
getAuth |
() => { token, decoded } | undefined |
Delegates to the Convex client. Returns the JWT currently held by the client together with its decoded claims, or undefined when no token is set. |
isAuthenticated does not flicker during a refresh
Section titled “isAuthenticated does not flicker during a refresh”Because isRefreshing() is gated on isAuthenticated(), a refresh never drops the user out of
the authenticated branch of your template. Content rendered for signed-in users stays mounted;
you layer a “reconnecting” affordance on top of it rather than swapping the whole view. See
the auth directives for the *cvaAuthRefreshing pattern.
getAuth() is a snapshot, not a signal
Section titled “getAuth() is a snapshot, not a signal”The Convex client emits no token-change events, so getAuth() cannot be a signal. Call it on
demand — typically right before handing the Convex token to an external API. It returns
undefined when no token is set, and during server-side rendering, where the WebSocket
client is disabled. Never read it in a computed() and expect it to update.
State transitions
Section titled “State transitions”provider is the upstream ConvexAuthProvider; “backend” is Convex’s confirmation of the
current token.
provider.isLoading() |
provider.isAuthenticated() |
Backend | isLoading() |
isAuthenticated() |
isRefreshing() |
status() |
|---|---|---|---|---|---|---|
true |
any | reset to pending | true |
false |
false |
'loading' |
false |
false |
rejected | false |
false |
false |
'unauthenticated' |
false |
true |
pending | true |
false |
false |
'loading' |
false |
true |
confirmed | false |
true |
false |
'authenticated' |
false |
true |
confirmed, socket paused | false |
true |
true |
'refreshing' |
false |
true |
rejected | false |
false |
false |
'unauthenticated' |
Notable edges:
- Sign-in.
'loading'→ (provider confirms, Convex pending)'loading'→'authenticated'. There is no intermediate'authenticated'before the backend answers. - Token rejected mid-session.
'authenticated'→'refreshing'→'authenticated'when the replacement token is accepted, or →'unauthenticated'when it is not. - Sign-out. Any state →
'unauthenticated'. The library also clears auth on the Convex client and resets the refreshing flag. - Context switch. When the provider exposes
reauthVersionand it changes, auth setup re-runs: the backend confirmation resets to pending, sostatus()returns to'loading'and then settles again. Callbacks from the superseded attempt are ignored.
Errors
Section titled “Errors”error() is deliberately quiet. A fetchAccessToken that resolves to null or undefined is
the ordinary signed-out outcome and sets nothing. What does set it:
| Source | Message |
|---|---|
Your fetchAccessToken threw |
[convex-angular auth] Token fetch failed: <message> |
| Wiring or clearing auth on the Convex client threw | [convex-angular auth] Convex auth sync failed: <message> |
The provider’s optional error signal is set |
Mirrored verbatim |
Internal errors are cleared when Convex confirms a token, when a new auth attempt starts, and when the provider signs out.
provideConvexAuth()
Section titled “provideConvexAuth()”function provideConvexAuth(): EnvironmentProviders;function provideConvexAuthFromExisting(authProviderType: Type<ConvexAuthProvider>): EnvironmentProviders;provideConvexAuth() reads a ConvexAuthProvider from the CONVEX_AUTH token and the client
from CONVEX, then keeps the two in sync. It is root-only and once-only, and it validates
that eagerly at bootstrap through an environment initializer — a misplaced registration fails
immediately rather than on first use.
Exact error messages
Section titled “Exact error messages”Two registrations in the same injector:
`provideConvexAuth()` was registered more than once in the same injector. Register it exactly once in your root application providers (for example, in `app.config.ts`).Registered in a child or route-level injector below an existing registration:
`provideConvexAuth()` must be configured only in your root application providers (for example, in `app.config.ts`). Remove nested or route-level registrations.No CONVEX_AUTH provider was registered:
Could not find `CONVEX_AUTH`. Make sure to provide an auth provider using `CONVEX_AUTH`, `provideClerkAuth()`, or `provideAuth0Auth()` before calling `provideConvexAuth()`.No Convex client — provideConvex(...) is missing:
Could not find `CONVEX`. Make sure to call `provideConvex(...)` once in your root application providers before calling `provideConvexAuth()`.injectAuth() called with no auth configured at all:
Could not find Convex auth state. Make sure to call `provideConvexAuth()`, `provideClerkAuth()`, or `provideAuth0Auth()` in your application providers.