convex-angular
The Angular client for Convex.
β¨ Features
- π Core providers:
provideConvex,injectQuery,injectMutation,injectAction,injectPaginatedQuery, andinjectConvex - π Authentication: Built-in support for Clerk, Auth0, and custom auth providers via
convex-angular/auth - π‘οΈ Route Guards: Protect routes with
convex-angular/router - π― Auth Directives:
*cvaAuthenticated,*cvaUnauthenticated,*cvaAuthLoading - π Pagination: Built-in support for paginated queries with
loadMoreandreset - βοΈ Conditional Queries: Use
skipTokento conditionally skip queries - π‘ Signal Integration: Angular Signals for reactive state
- π§Ή Auto Cleanup: Automatic lifecycle management
π Getting Started
Requirements: Angular >= 20, Convex >= 1.31, RxJS >= 7.8.
- Install the dependencies:
npm install convex convex-angular
- Add
provideConvexto yourapp.config.tsfile:
import { ApplicationConfig } from '@angular/core'; import { provideConvex } from 'convex-angular'; export const appConfig: ApplicationConfig = { providers: [provideConvex('https://<your-convex-deployment>.convex.cloud')], };
- π That's it! You can now use the injection providers in your app.
π Usage
Note: In the examples below,
apirefers to your generated Convex function references (usually fromconvex/_generated/api). Adjust the import path to match your project structure.
Fetching data
Use injectQuery to fetch data from the database.
import { Component } from '@angular/core'; import { injectQuery } from 'convex-angular'; // Adjust the import path to match your project structure. import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: ` @if (todos.isLoading()) { <p>Loading...</p> } @if (todos.error()) { <p>Error: {{ todos.error()?.message }}</p> } <ul> @for (todo of todos.data() ?? []; track todo._id) { <li>{{ todo.title }}</li> } </ul> `, }) export class AppComponent { readonly todos = injectQuery(api.todos.listTodos, () => ({ count: 10 })); }
Mutating data
Use injectMutation to mutate the database.
Pass { injector } if you need to create the mutation result outside an
active injection context.
import { Component } from '@angular/core'; import { injectMutation } from 'convex-angular'; import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: ` <button (click)="addTodo.mutate({ title: 'Buy groceries' })"> Add Todo </button> `, }) export class AppComponent { readonly addTodo = injectMutation(api.todos.addTodo); }
Running actions
Use injectAction to run actions.
Pass { injector } if you need to create the action result outside an
active injection context.
import { Component } from '@angular/core'; import { injectAction } from 'convex-angular'; import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: `<button (click)="completeAllTodos.run({})"> Complete All Todos </button>`, }) export class AppComponent { readonly completeAllTodos = injectAction(api.todoFunctions.completeAllTodos); }
Paginated queries
Use injectPaginatedQuery for infinite scroll or "load more" patterns.
Your Convex query must accept a paginationOpts argument.
import { Component } from '@angular/core'; import { injectPaginatedQuery } from 'convex-angular'; import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: ` <ul> @for (todo of todos.results(); track todo._id) { <li>{{ todo.title }}</li> } </ul> @if (todos.canLoadMore()) { <button (click)="todos.loadMore(10)">Load More</button> } @if (todos.isExhausted()) { <p>All items loaded</p> } `, }) export class AppComponent { readonly todos = injectPaginatedQuery( api.todos.listTodosPaginated, () => ({}), { initialNumItems: 10 }, ); }
The paginated query returns:
results()- Accumulated results from all loaded pagesisLoadingFirstPage()- True when loading the first pageisLoadingMore()- True when loading additional pagescanLoadMore()- True when more items are availableisExhausted()- True when all items have been loadedisSkipped()- True when the query is skipped viaskipTokenisSuccess()- True when the first page has loaded successfullystatus()-'pending' | 'success' | 'error' | 'skipped'error()- Error if the query failedloadMore(n)- Loadnmore itemsreset()- Reset pagination and reload from the beginning
For simple cases, pass a static options object. If initialNumItems or callbacks
need to react to signals, you can still pass a function. If you need to call
injectPaginatedQuery outside an injection context, use the static options form
and include injector.
Conditional queries with skipToken
Use skipToken to conditionally skip a query when certain conditions aren't met.
import { Component, signal } from '@angular/core'; import { injectQuery, skipToken } from 'convex-angular'; import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: ` @if (user.isSkipped()) { <p>Select a user to view profile</p> } @else if (user.isLoading()) { <p>Loading...</p> } @else { <p>{{ user.data()?.name }}</p> } `, }) export class AppComponent { readonly userId = signal<string | null>(null); // Query is skipped when userId is null readonly user = injectQuery(api.users.getProfile, () => this.userId() ? { userId: this.userId() } : skipToken, ); }
This is useful when:
- Query arguments depend on user selection
- You need to wait for authentication before fetching data
- A parent query must complete before running a dependent query
Using the Convex client
Use injectConvex to get full flexibility of the Convex client.
import { Component } from '@angular/core'; import { injectConvex } from 'convex-angular'; import { api } from '../convex/_generated/api'; @Component({ selector: 'app-root', template: `<button (click)="completeAllTodos()">Complete All Todos</button>`, }) export class AppComponent { readonly convex = injectConvex(); completeAllTodos() { this.convex.action(api.todoFunctions.completeAllTodos, {}); } }
π Authentication
Using injectAuth
Use injectAuth to access the authentication state in your components.
import { Component } from '@angular/core'; import { injectAuth } from 'convex-angular/auth'; @Component({ selector: 'app-root', template: ` @switch (auth.status()) { @case ('loading') { <p>Loading...</p> } @case ('authenticated') { <app-dashboard></app-dashboard> } @case ('unauthenticated') { <app-login></app-login> } } `, }) export class AppComponent { readonly auth = injectAuth(); }
The auth state provides:
isLoading()- True while auth is initializing or Convex is still validating the tokenisAuthenticated()- True only after the auth provider and Convex both confirm the sessionerror()- Token fetch or token validation error, if anystatus()-'loading' | 'authenticated' | 'unauthenticated'
Clerk Integration
To integrate with Clerk, create a service that implements ClerkAuthProvider and register it with provideClerkAuth().
Leave token fetch errors unhandled in getToken() so they can surface through injectAuth().error().
// clerk-auth.service.ts import { Injectable, Signal, computed, inject } from '@angular/core'; import { Clerk } from '@clerk/clerk-js'; // Your Clerk instance // app.config.ts import { provideConvex } from 'convex-angular'; import { CLERK_AUTH, ClerkAuthProvider, provideClerkAuth, } from 'convex-angular/integrations/clerk'; @Injectable({ providedIn: 'root' }) export class ClerkAuthService implements ClerkAuthProvider { private clerk = inject(Clerk); readonly isLoaded = computed(() => this.clerk.loaded()); readonly isSignedIn = computed(() => !!this.clerk.user()); readonly orgId = computed(() => this.clerk.organization()?.id); readonly orgRole = computed( () => this.clerk.organization()?.membership?.role, ); async getToken(options?: { template?: string; skipCache?: boolean }) { return (await this.clerk.session?.getToken(options)) ?? null; } } export const appConfig: ApplicationConfig = { providers: [ provideConvex('https://<your-convex-deployment>.convex.cloud'), { provide: CLERK_AUTH, useExisting: ClerkAuthService }, provideClerkAuth(), ], };
Auth0 Integration
To integrate with Auth0, create a service that implements Auth0AuthProvider and register it with provideAuth0Auth().
Leave token fetch errors unhandled in getAccessTokenSilently() so they can surface through injectAuth().error().
// auth0-auth.service.ts import { Injectable, inject } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { AuthService } from '@auth0/auth0-angular'; // app.config.ts import { provideConvex } from 'convex-angular'; import { AUTH0_AUTH, Auth0AuthProvider, provideAuth0Auth, } from 'convex-angular/integrations/auth0'; import { firstValueFrom } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class Auth0AuthService implements Auth0AuthProvider { private auth0 = inject(AuthService); readonly isLoading = toSignal(this.auth0.isLoading$, { initialValue: true }); readonly isAuthenticated = toSignal(this.auth0.isAuthenticated$, { initialValue: false, }); async getAccessTokenSilently(options?: { cacheMode?: 'on' | 'off' }) { return await firstValueFrom( this.auth0.getAccessTokenSilently({ cacheMode: options?.cacheMode }), ); } } export const appConfig: ApplicationConfig = { providers: [ provideConvex('https://<your-convex-deployment>.convex.cloud'), { provide: AUTH0_AUTH, useExisting: Auth0AuthService }, provideAuth0Auth(), ], };
Custom Auth Providers
For other auth providers, implement the ConvexAuthProvider interface and use provideConvexAuth().
// custom-auth.service.ts import { Injectable, signal } from '@angular/core'; // app.config.ts import { provideConvex } from 'convex-angular'; import { CONVEX_AUTH, ConvexAuthProvider, provideConvexAuthFromExisting, } from 'convex-angular/auth'; @Injectable({ providedIn: 'root' }) export class CustomAuthService implements ConvexAuthProvider { readonly isLoading = signal(true); readonly isAuthenticated = signal(false); constructor() { // Initialize your auth provider myAuthProvider.onStateChange((state) => { this.isLoading.set(false); this.isAuthenticated.set(state.loggedIn); }); } async fetchAccessToken({ forceRefreshToken, }: { forceRefreshToken: boolean; }) { return myAuthProvider.getToken({ refresh: forceRefreshToken }); } } export const appConfig: ApplicationConfig = { providers: [ provideConvex('https://<your-convex-deployment>.convex.cloud'), provideConvexAuthFromExisting(CustomAuthService), ], };
provideConvexAuthFromExisting(...) registers CONVEX_AUTH with useExisting and includes provideConvexAuth() internally.
Internally, the auth integrations and provideConvexAuth() share the same Convex auth setup path.
If you wire CONVEX_AUTH manually, use useExisting (not useClass) when the
auth provider is also injected elsewhere, otherwise you can end up with two
instances and auth signal updates wonβt reach Convex auth sync.
Convex Auth (@convex-dev/auth)
When integrating @convex-dev/auth, implement fetchAccessToken to return the
Convex-auth JWT (return null when signed out).
import { Injectable, signal } from '@angular/core'; import { ConvexAuthProvider } from 'convex-angular/auth'; @Injectable({ providedIn: 'root' }) export class ConvexAuthService implements ConvexAuthProvider { readonly isLoading = signal(true); readonly isAuthenticated = signal(false); async fetchAccessToken({ forceRefreshToken, }: { forceRefreshToken: boolean; }) { return myAuthProvider.getToken({ refresh: forceRefreshToken }); } }
With provideConvexAuth() registered, convex-angular will call
convex.setAuth(...) / convex.client.clearAuth() automatically when your
providerβs isAuthenticated changes. injectAuth().status() remains 'loading'
until Convex confirms the token with the backend.
Auth Directives
Use structural directives to conditionally render content based on auth state.
<!-- Show only when authenticated --> <nav *cvaAuthenticated> <span>Welcome back!</span> <button (click)="logout()">Sign Out</button> </nav> <!-- Show only when NOT authenticated --> <div *cvaUnauthenticated> <p>Please sign in to continue.</p> <button (click)="login()">Sign In</button> </div> <!-- Show while auth is loading --> <div *cvaAuthLoading> <p>Checking authentication...</p> </div>
Import the directives in your component:
import { CvaAuthLoadingDirective, CvaAuthenticatedDirective, CvaUnauthenticatedDirective, } from 'convex-angular/auth'; @Component({ imports: [ CvaAuthenticatedDirective, CvaUnauthenticatedDirective, CvaAuthLoadingDirective, ], // ... }) export class AppComponent {}
Route Guards
Protect routes that require authentication using convexAuthGuard.
// app.routes.ts import { Routes } from '@angular/router'; import { convexAuthGuard } from 'convex-angular/router'; export const routes: Routes = [ { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component').then( (m) => m.DashboardComponent, ), canActivate: [convexAuthGuard], }, { path: 'profile', loadComponent: () => import('./profile/profile.component').then((m) => m.ProfileComponent), canActivate: [convexAuthGuard], }, { path: 'login', loadComponent: () => import('./login/login.component').then((m) => m.LoginComponent), }, ];
By default, unauthenticated users are redirected to /login. To customize the redirect route:
// app.config.ts import { CONVEX_AUTH_GUARD_CONFIG } from 'convex-angular/router'; export const appConfig: ApplicationConfig = { providers: [ // ... other providers { provide: CONVEX_AUTH_GUARD_CONFIG, useValue: { loginRoute: '/auth/signin' }, }, ], };
π€ Contributing
Contributions are welcome! Please feel free to submit a pull request.
Repo development
pnpm install pnpm dev:backend pnpm dev:frontend pnpm test:library pnpm build:library