The Angular client for Convex.
- 🔌 Core providers:
injectQuery,injectMutation,injectAction,injectPaginatedQuery, andinjectConvex - 🔐 Authentication: Built-in support for Clerk, Auth0, and custom auth providers via
injectAuth - 🛡️ Route Guards: Protect routes with
convexAuthGuard - 🎯 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
- Install the dependencies:
npm install convex convex-angular- Add
provideConvexto yourapp.config.tsfile:
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.
Use injectQuery to fetch data from the database.
import { injectQuery } from 'convex-angular';
@Component({
selector: 'app-root',
template: `
<ul>
@for (todo of todos.data(); track todo._id) {
<li>{{ todo.name }}</li>
}
</ul>
`,
})
export class AppComponent {
readonly todos = injectQuery(api.todo.listTodos, () => ({}));
}Use injectMutation to mutate the database.
import { injectMutation } from 'convex-angular';
@Component({
selector: 'app-root',
template: `
<button (click)="addTodo.mutate({ id: '1', name: 'Buy groceries' })">
Add Todo
</button>
`,
})
export class AppComponent {
readonly addTodo = injectMutation(api.todo.addTodo);
}Use injectAction to run actions.
import { injectAction } from 'convex-angular';
@Component({
selector: 'app-root',
template: `<button (click)="resetTodos.run({})">Reset Todos</button>`,
})
export class AppComponent {
readonly resetTodos = injectAction(api.todoFunctions.resetTodos);
}Use injectPaginatedQuery for infinite scroll or "load more" patterns.
import { injectPaginatedQuery } from 'convex-angular';
@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 viaskipTokenerror()- Error if the query failedloadMore(n)- Loadnmore itemsreset()- Reset pagination and reload from the beginning
Use skipToken to conditionally skip a query when certain conditions aren't met.
import { signal } from '@angular/core';
import { injectQuery, skipToken } from 'convex-angular';
@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
Use injectConvex to get full flexibility of the Convex client.
import { injectConvex } from 'convex-angular';
@Component({
selector: 'app-root',
template: `<button (click)="completeAllTodos()">Complete All Todos</button>`,
})
export class AppComponent {
readonly convex = injectConvex();
completeAllTodos() {
this.convex.mutation(api.todoFunctions.completeAllTodos, {});
}
}Use injectAuth to access the authentication state in your components.
import { injectAuth } from 'convex-angular';
@Component({
selector: 'app-root',
template: `
@switch (auth.status()) {
@case ('loading') {
<p>Loading...</p>
}
@case ('authenticated') {
<app-dashboard />
}
@case ('unauthenticated') {
<app-login />
}
}
`,
})
export class AppComponent {
readonly auth = injectAuth();
}The auth state provides:
isLoading()- True while auth is initializingisAuthenticated()- True when user is authenticatederror()- Authentication error, if anystatus()-'loading' | 'authenticated' | 'unauthenticated'
To integrate with Clerk, create a service that implements ClerkAuthProvider and register it with provideClerkAuth().
// 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 {
CLERK_AUTH,
ClerkAuthProvider,
provideClerkAuth,
provideConvex,
} from 'convex-angular';
@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 }) {
try {
return (await this.clerk.session?.getToken(options)) ?? null;
} catch {
return null;
}
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideConvex('https://<your-convex-deployment>.convex.cloud'),
{ provide: CLERK_AUTH, useClass: ClerkAuthService },
provideClerkAuth(),
],
};To integrate with Auth0, create a service that implements Auth0AuthProvider and register it with provideAuth0Auth().
// 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 {
AUTH0_AUTH,
Auth0AuthProvider,
provideAuth0Auth,
provideConvex,
} from 'convex-angular';
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 firstValueFrom(
this.auth0.getAccessTokenSilently({ cacheMode: options?.cacheMode }),
);
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideConvex('https://<your-convex-deployment>.convex.cloud'),
{ provide: AUTH0_AUTH, useClass: Auth0AuthService },
provideAuth0Auth(),
],
};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 {
CONVEX_AUTH,
ConvexAuthProvider,
provideConvex,
provideConvexAuth,
} from 'convex-angular';
@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'),
{ provide: CONVEX_AUTH, useClass: CustomAuthService },
provideConvexAuth(),
],
};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';
@Component({
imports: [
CvaAuthenticatedDirective,
CvaUnauthenticatedDirective,
CvaAuthLoadingDirective,
],
// ...
})
export class AppComponent {}Protect routes that require authentication using convexAuthGuard.
// app.routes.ts
import { Routes } from '@angular/router';
import { convexAuthGuard } from 'convex-angular';
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component'),
canActivate: [convexAuthGuard],
},
{
path: 'profile',
loadComponent: () => import('./profile/profile.component'),
canActivate: [convexAuthGuard],
},
{
path: 'login',
loadComponent: () => import('./login/login.component'),
},
];By default, unauthenticated users are redirected to /login. To customize the redirect route:
// app.config.ts
import { CONVEX_AUTH_GUARD_CONFIG } from 'convex-angular';
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
{
provide: CONVEX_AUTH_GUARD_CONFIG,
useValue: { loginRoute: '/auth/signin' },
},
],
};Contributions are welcome! Please feel free to submit a pull request.