Custom auth providers
Every bundled integration — Clerk, Auth0,
Better Auth — is a ConvexAuthProvider under the hood. Implement that
interface yourself to bridge any identity system: a session cookie, a bespoke OIDC client, a
native shell, or a test double.
ConvexAuthProvider
Section titled “ConvexAuthProvider”interface ConvexAuthProvider { isLoading: Signal<boolean>; isAuthenticated: Signal<boolean>; fetchAccessToken: AuthTokenFetcher; reauthVersion?: Signal<unknown>; error?: Signal<Error | undefined>;}
type AuthTokenFetcher = (args: { forceRefreshToken: boolean }) => Promise<string | null | undefined>;| Member | Type | Required | Contract |
|---|---|---|---|
isLoading |
Signal<boolean> |
yes | true while your provider is still determining initial state. injectAuth().status() is 'loading' for as long as this is true. |
isAuthenticated |
Signal<boolean> |
yes | true when your provider considers the user signed in. This alone is not enough: injectAuth() stays 'loading' until Convex confirms or rejects the token. |
fetchAccessToken |
AuthTokenFetcher |
yes | Returns the JWT. Return null or undefined for “no token available” — that is the ordinary signed-out outcome, not an error. forceRefreshToken is true when the server rejected the previous token or it is close to expiring; bypass any cache you keep. |
reauthVersion |
Signal<unknown> |
no | Any change re-runs Convex auth setup while the user stays signed in. Use it for auth context changes — organization switch, workspace switch, session replacement — that need a different token. |
error |
Signal<Error | undefined> |
no | Provider-owned errors. injectAuth().error() mirrors it unless a newer internal auth error was recorded. |
The recorded error is cleared when Convex subsequently confirms a token, when a new auth attempt starts, and when the provider signs out.
Registration
Section titled “Registration”provideConvexAuthFromExisting() (recommended)
Section titled “provideConvexAuthFromExisting() (recommended)”function provideConvexAuthFromExisting(authProviderType: Type<ConvexAuthProvider>): EnvironmentProviders;Registers { provide: CONVEX_AUTH, useExisting: authProviderType } and provideConvexAuth()
in one call. This is the convenient path whenever your auth service is a normal injectable that
other parts of the app inject too.
CONVEX_AUTH + provideConvexAuth()
Section titled “CONVEX_AUTH + provideConvexAuth()”const CONVEX_AUTH: InjectionToken<ConvexAuthProvider>;
providers: [ provideConvex(environment.convexUrl), { provide: CONVEX_AUTH, useExisting: MyAuthService }, provideConvexAuth(),];Example
Section titled “Example”import { ApplicationConfig, Injectable, computed, signal } from '@angular/core';import { ConvexAuthProvider, provideConvex, provideConvexAuthFromExisting } from 'convex-angular';
interface Session { id: string; organizationId: string; accessToken: string;}
@Injectable({ providedIn: 'root' })export class MyAuthService implements ConvexAuthProvider { private readonly session = signal<Session | null>(null); private readonly loading = signal(true);
readonly isLoading = this.loading.asReadonly(); readonly isAuthenticated = computed(() => this.session() !== null);
// Optional. Any change re-runs Convex auth setup while the user stays signed // in — use it for context switches (organization, workspace, impersonation) // that require a different token. readonly reauthVersion = computed(() => [this.session()?.id, this.session()?.organizationId]);
// Optional. `injectAuth().error()` mirrors this signal unless a newer // internal auth error was recorded. readonly error = signal<Error | undefined>(undefined);
async fetchAccessToken({ forceRefreshToken }: { forceRefreshToken: boolean }): Promise<string | null> { const session = this.session(); if (!session) { // Returning null is the ordinary signed-out outcome; it does not set an error. return null; }
if (!forceRefreshToken) { return session.accessToken; }
// Anything thrown here surfaces on `injectAuth().error()` prefixed with // '[convex-angular auth] Token fetch failed: '. const response = await fetch('/api/token', { method: 'POST' }); if (!response.ok) { throw new Error(`Token endpoint returned ${response.status}`); }
const { token } = (await response.json()) as { token: string }; this.session.update((current) => (current ? { ...current, accessToken: token } : current)); return token; }}
export const appConfig: ApplicationConfig = { providers: [ provideConvex('https://example-123.convex.cloud'), // Registers `{ provide: CONVEX_AUTH, useExisting: MyAuthService }` and // `provideConvexAuth()` together. Root-only, exactly once. provideConvexAuthFromExisting(MyAuthService), ],};Once registered, everything else in the library works unchanged: injectAuth(),
the auth directives, and the route guards.