Skip to content

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.

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.

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.

const CONVEX_AUTH: InjectionToken<ConvexAuthProvider>;
providers: [
provideConvex(environment.convexUrl),
{ provide: CONVEX_AUTH, useExisting: MyAuthService },
provideConvexAuth(),
];
my-auth.service.ts + app.config.ts
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.