Clerk
The Clerk integration is a thin adapter. convex-angular has no dependency on any @clerk/*
package: you write a small service that exposes Clerk’s state as signals, register it under the
CLERK_AUTH token, and provideClerkAuth() bridges it to Convex.
ClerkAuthProvider
Section titled “ClerkAuthProvider”The interface your service must satisfy. Only the first three members are required.
| Member | Type | Required | Purpose |
|---|---|---|---|
isLoaded |
Signal<boolean> |
yes | true once Clerk has initialized and auth state is known. Mapped to isLoading: computed(() => !clerk.isLoaded()). |
isSignedIn |
Signal<boolean | undefined> |
yes | Whether Clerk reports a signed-in user. May be undefined while loading; mapped to isAuthenticated: computed(() => clerk.isSignedIn() ?? false). |
getToken |
(options?: { template?: string; skipCache?: boolean }) => Promise<string | null> |
yes | Returns the JWT for Convex. |
sessionId |
Signal<string | null | undefined> |
no | Current Clerk session id. Part of reauthVersion. |
orgId |
Signal<string | null | undefined> |
no | Current organization id. Part of reauthVersion. |
orgRole |
Signal<string | null | undefined> |
no | Current organization role. Part of reauthVersion. |
sessionAudience |
Signal<string | null | undefined> |
no | The session token’s aud claim. Selects the token-fetch mode (below). |
error |
Signal<Error | undefined> |
no | Provider-owned errors, mirrored onto injectAuth().error(). |
interface ClerkAuthProvider { isLoaded: Signal<boolean>; isSignedIn: Signal<boolean | undefined>; getToken(options?: { template?: string; skipCache?: boolean }): Promise<string | null>; sessionId?: Signal<string | null | undefined>; orgId?: Signal<string | null | undefined>; orgRole?: Signal<string | null | undefined>; sessionAudience?: Signal<string | null | undefined>; error?: Signal<Error | undefined>;}CLERK_AUTH
Section titled “CLERK_AUTH”const CLERK_AUTH: InjectionToken<ClerkAuthProvider>;Register your implementation under this token before calling provideClerkAuth(). Prefer
useExisting over useClass when the service is also injected elsewhere in your app, so Angular
reuses the singleton instead of constructing a second instance.
provideClerkAuth()
Section titled “provideClerkAuth()”function provideClerkAuth(): EnvironmentProviders;It registers a CONVEX_AUTH factory that adapts CLERK_AUTH, and then provideConvexAuth().
Token fetching
Section titled “Token fetching”The adapter decides how to ask Clerk for a token from sessionAudience():
sessionAudience() |
Call made |
|---|---|
'convex' |
clerk.getToken({ skipCache }) — Clerk’s native Convex integration, with no JWT template. |
| anything else, or absent | clerk.getToken({ template: 'convex', skipCache }) — requests the 'convex' JWT template. |
forceRefreshToken from Convex maps directly to skipCache: when Convex needs a genuinely fresh
token it passes forceRefreshToken: true, and the adapter sets skipCache: true so Clerk bypasses
its own token cache.
reauthVersion
Section titled “reauthVersion”The adapter builds it for you:
reauthVersion: computed(() => [clerk.sessionId?.(), clerk.orgId?.(), clerk.orgRole?.()]);Any change to that tuple makes provideConvexAuth() tear down the current auth wiring and re-run
setup with a fresh token, while the user stays signed in.
orgId and orgRole cover organization switching, where the token’s claims change but the user
does not. sessionId is the one that is easy to omit and expensive to miss. Signing out and
back in replaces the Clerk session while isSignedIn may never dip to false for long enough to
retrigger setup. Without sessionId in the tuple, Convex keeps fetching tokens for the dead
session: auth looks loaded, but the app stays unauthenticated until a full page reload. Expose
sessionId unless you have a specific reason not to.
Example
Section titled “Example”import { Injectable, InjectionToken, Signal, computed, inject } from '@angular/core';import { ClerkAuthProvider } from 'convex-angular';
/** * These docs do not depend on `@clerk/clerk-js`, so the small slice of the * Clerk SDK used below is declared locally. In your application these values * come from your Clerk instance instead. */interface ClerkSession { id: string; /** The session token's `aud` claim. */ audience?: string; getToken(options?: { template?: string; skipCache?: boolean }): Promise<string | null>;}
interface ClerkSdk { loaded: Signal<boolean>; user: Signal<{ id: string } | null>; session: Signal<ClerkSession | null>; organization: Signal<{ id: string; membership?: { role: string } } | null>;}
const CLERK_SDK = new InjectionToken<ClerkSdk>('CLERK_SDK');
@Injectable({ providedIn: 'root' })export class ClerkAuthService implements ClerkAuthProvider { private readonly clerk = inject(CLERK_SDK);
readonly isLoaded = computed(() => this.clerk.loaded()); readonly isSignedIn = computed(() => !!this.clerk.user());
// `sessionId` is what makes a replaced session (sign out, then sign back in) // re-run Convex auth setup instead of stranding it on the dead session. readonly sessionId = computed(() => this.clerk.session()?.id); readonly orgId = computed(() => this.clerk.organization()?.id); readonly orgRole = computed(() => this.clerk.organization()?.membership?.role);
// When this is 'convex', the adapter uses Clerk's native Convex integration // and requests no JWT template. readonly sessionAudience = computed(() => this.clerk.session()?.audience);
async getToken(options?: { template?: string; skipCache?: boolean }): Promise<string | null> { return (await this.clerk.session()?.getToken(options)) ?? null; }}Register it in your root providers:
import { ApplicationConfig } from '@angular/core';import { CLERK_AUTH, provideClerkAuth, provideConvex } from 'convex-angular';
import { ClerkAuthService } from './auth-clerk';
export const appConfig: ApplicationConfig = { providers: [ provideConvex('https://example-123.convex.cloud'), // `useExisting` so Angular reuses the singleton instead of constructing a // second ClerkAuthService for the token. { provide: CLERK_AUTH, useExisting: ClerkAuthService }, // Already includes provideConvexAuth(). Do not register that as well. provideClerkAuth(), ],};In a real application the SDK bindings come from Clerk directly, for example:
import { Clerk } from '@clerk/clerk-js';Then read the state with injectAuth(), gate templates with
the auth directives, and protect routes with
the route guards.