https://github.com/azhukaudev/convex-angular
The Angular client for Convex.
https://github.com/azhukaudev/convex-angular
angular angular-signals convex convex-angular convex-auth convex-backend convex-database frontend nx open-source primeng rxjs tailwindcss typescript
Last synced: 24 days ago
JSON representation
The Angular client for Convex.
- Host: GitHub
- URL: https://github.com/azhukaudev/convex-angular
- Owner: azhukaudev
- License: mit
- Created: 2025-07-14T17:09:53.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-02-26T22:31:36.000Z (about 1 month ago)
- Last Synced: 2026-02-27T02:06:45.353Z (about 1 month ago)
- Topics: angular, angular-signals, convex, convex-angular, convex-auth, convex-backend, convex-database, frontend, nx, open-source, primeng, rxjs, tailwindcss, typescript
- Language: TypeScript
- Homepage: https://convex-angular.vercel.app
- Size: 479 KB
- Stars: 26
- Watchers: 4
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# convex-angular
[](https://www.npmjs.com/package/convex-angular)
[](https://github.com/azhukaudev/convex-angular/blob/main/LICENSE)
[](https://www.npmjs.com/package/convex-angular)
The Angular client for Convex.
## β¨ Features
- π Core providers: `provideConvex`, `injectQuery`, `injectMutation`, `injectAction`, `injectPaginatedQuery`, and `injectConvex`
- π 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 `loadMore` and `reset`
- βοΈ Conditional Queries: Use `skipToken` to conditionally skip queries
- π‘ Signal Integration: [Angular Signals](https://angular.dev/guide/signals) for reactive state
- π§Ή Auto Cleanup: Automatic lifecycle management
## π Getting Started
> Requirements: Angular >= 20, Convex >= 1.31, RxJS >= 7.8.
1. Install the dependencies:
```bash
npm install convex convex-angular
```
2. Add `provideConvex` once to your root `app.config.ts` providers:
```typescript
import { ApplicationConfig } from '@angular/core';
import { provideConvex } from 'convex-angular';
export const appConfig: ApplicationConfig = {
providers: [provideConvex('https://.convex.cloud')],
};
```
`provideConvex(...)` must be configured only once at the root application level.
Do not register it again in nested or route-level providers.
3. π That's it! You can now use the injection providers in your app.
## π Usage
> Note: In the examples below, `api` refers to your generated Convex function references (usually from `convex/_generated/api`). Adjust the import path to match your project structure.
### Fetching data
Use `injectQuery` to fetch data from the database.
```typescript
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()) {
Loading...
}
@if (todos.error()) {
Error: {{ todos.error()?.message }}
}
- {{ todo.title }}
@for (todo of todos.data() ?? []; track todo._id) {
}
`,
})
export class AppComponent {
readonly todos = injectQuery(api.todos.listTodos, () => ({ count: 10 }));
}
```
### Mutating data
Use `injectMutation` to mutate the database.
```typescript
import { Component } from '@angular/core';
import { injectMutation } from 'convex-angular';
import { api } from '../convex/_generated/api';
@Component({
selector: 'app-root',
template: `
Add Todo
`,
})
export class AppComponent {
readonly addTodo = injectMutation(api.todos.addTodo);
}
```
### Running actions
Use `injectAction` to run actions.
```typescript
import { Component } from '@angular/core';
import { injectAction } from 'convex-angular';
import { api } from '../convex/_generated/api';
@Component({
selector: 'app-root',
template: `
Complete All Todos
`,
})
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.
```typescript
import { Component } from '@angular/core';
import { injectPaginatedQuery } from 'convex-angular';
import { api } from '../convex/_generated/api';
@Component({
selector: 'app-root',
template: `
- {{ todo.title }}
@for (todo of todos.results(); track todo._id) {
}
@if (todos.canLoadMore()) {
Load More
}
@if (todos.isExhausted()) {
All items loaded
}
`,
})
export class AppComponent {
readonly todos = injectPaginatedQuery(
api.todos.listTodosPaginated,
() => ({}),
{ initialNumItems: 10 },
);
}
```
The paginated query returns:
- `results()` - Accumulated results from all loaded pages
- `isLoadingFirstPage()` - True when loading the first page
- `isLoadingMore()` - True when loading additional pages
- `canLoadMore()` - True when more items are available
- `isExhausted()` - True when all items have been loaded
- `isSkipped()` - True when the query is skipped via `skipToken`
- `isSuccess()` - True when the first page has loaded successfully
- `status()` - `'pending' | 'success' | 'error' | 'skipped'`
- `error()` - Error if the query failed
- `loadMore(n)` - Load `n` more items
- `reset()` - Reset pagination and reload from the beginning
### Conditional queries with skipToken
Use `skipToken` to conditionally skip a query when certain conditions aren't met.
```typescript
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()) {
Select a user to view profile
} @else if (user.isLoading()) {
Loading...
} @else {
{{ user.data()?.name }}
}
`,
})
export class AppComponent {
readonly userId = signal(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.
```typescript
import { Component } from '@angular/core';
import { injectConvex } from 'convex-angular';
import { api } from '../convex/_generated/api';
@Component({
selector: 'app-root',
template: `Complete All Todos`,
})
export class AppComponent {
readonly convex = injectConvex();
completeAllTodos() {
this.convex.action(api.todoFunctions.completeAllTodos, {});
}
}
```
### Creating helpers outside the initial injection context
If you need to create a Convex helper later from plain code, capture an
`EnvironmentInjector` in DI and pass it as `injectRef`.
```typescript
import { Component, EnvironmentInjector, inject } from '@angular/core';
import { injectMutation } from 'convex-angular';
import { api } from '../convex/_generated/api';
@Component({
selector: 'app-root',
template: `Save`,
})
export class AppComponent {
private readonly injectRef = inject(EnvironmentInjector);
submit() {
const mutation = injectMutation(api.todos.addTodo, {
injectRef: this.injectRef,
});
mutation.mutate({ title: 'Created outside the initial scope' });
}
}
```
This works for all public `inject*` helpers, including `injectQuery`,
`injectPaginatedQuery`, `injectMutation`, `injectAction`, `injectConvex`, and
`injectAuth`.
## π Authentication
### Using injectAuth
Use `injectAuth` to access the authentication state in your components.
```typescript
import { Component } from '@angular/core';
import { injectAuth } from 'convex-angular';
@Component({
selector: 'app-root',
template: `
@switch (auth.status()) {
@case ('loading') {
Loading...
}
@case ('authenticated') {
}
@case ('unauthenticated') {
}
}
`,
})
export class AppComponent {
readonly auth = injectAuth();
}
```
The auth state provides:
- `isLoading()` - True while auth is initializing
- `isAuthenticated()` - True when user is authenticated
- `error()` - Authentication error, if any
- `status()` - `'loading' | 'authenticated' | 'unauthenticated'`
### Clerk Integration
To integrate with Clerk, create a service that implements `ClerkAuthProvider` and register it with `provideClerkAuth()`.
```typescript
// 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://.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()`.
```typescript
// 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://.convex.cloud'),
{ provide: AUTH0_AUTH, useExisting: Auth0AuthService },
provideAuth0Auth(),
],
};
```
### Custom Auth Providers
For other auth providers, implement the `ConvexAuthProvider` interface and use `provideConvexAuth()`.
```typescript
// custom-auth.service.ts
import { Injectable, signal } from '@angular/core';
// app.config.ts
import {
CONVEX_AUTH,
ConvexAuthProvider,
provideConvex,
provideConvexAuthFromExisting,
} 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://.convex.cloud'),
provideConvexAuthFromExisting(CustomAuthService),
],
};
```
`provideConvexAuthFromExisting(...)` registers `CONVEX_AUTH` with `useExisting` and includes `provideConvexAuth()` internally.
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).
```typescript
import { Injectable, signal } from '@angular/core';
import { ConvexAuthProvider } from 'convex-angular';
@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.
### Auth Directives
Use structural directives to conditionally render content based on auth state.
```html
Welcome back!
Sign Out
Please sign in to continue.
Sign In
Checking authentication...
```
Import the directives in your component:
```typescript
import {
CvaAuthLoadingDirective,
CvaAuthenticatedDirective,
CvaUnauthenticatedDirective,
} from 'convex-angular';
@Component({
imports: [
CvaAuthenticatedDirective,
CvaUnauthenticatedDirective,
CvaAuthLoadingDirective,
],
// ...
})
export class AppComponent {}
```
### Route Guards
Protect routes that require authentication using `convexAuthGuard`.
```typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { convexAuthGuard } from 'convex-angular';
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:
```typescript
// 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' },
},
],
};
```
## π€ Contributing
Contributions are welcome! Please feel free to submit a pull request.
### Repo development
```bash
pnpm install
pnpm dev:backend
pnpm dev:frontend
pnpm test:library
pnpm build:library
```
## βοΈ License
[MIT](https://github.com/azhukaudev/convex-angular/blob/main/LICENSE)