{"id":36983326,"url":"https://github.com/azhukaudev/convex-angular","last_synced_at":"2026-03-07T15:00:58.281Z","repository":{"id":304718234,"uuid":"1019661061","full_name":"azhukaudev/convex-angular","owner":"azhukaudev","description":"The Angular client for Convex.","archived":false,"fork":false,"pushed_at":"2026-02-26T22:31:36.000Z","size":491,"stargazers_count":26,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-02-27T02:06:45.353Z","etag":null,"topics":["angular","angular-signals","convex","convex-angular","convex-auth","convex-backend","convex-database","frontend","nx","open-source","primeng","rxjs","tailwindcss","typescript"],"latest_commit_sha":null,"homepage":"https://convex-angular.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/azhukaudev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-07-14T17:09:53.000Z","updated_at":"2026-02-26T22:31:39.000Z","dependencies_parsed_at":"2025-07-14T23:47:49.965Z","dependency_job_id":"517c0287-192a-4657-8b09-50cf888bdf00","html_url":"https://github.com/azhukaudev/convex-angular","commit_stats":null,"previous_names":["azhukau-dev/convex-angular","azhukaudev/convex-angular"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/azhukaudev/convex-angular","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azhukaudev%2Fconvex-angular","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azhukaudev%2Fconvex-angular/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azhukaudev%2Fconvex-angular/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azhukaudev%2Fconvex-angular/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/azhukaudev","download_url":"https://codeload.github.com/azhukaudev/convex-angular/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/azhukaudev%2Fconvex-angular/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30219234,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T14:02:48.375Z","status":"ssl_error","status_checked_at":"2026-03-07T14:02:43.192Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","angular-signals","convex","convex-angular","convex-auth","convex-backend","convex-database","frontend","nx","open-source","primeng","rxjs","tailwindcss","typescript"],"created_at":"2026-01-13T22:56:36.112Z","updated_at":"2026-03-07T15:00:58.268Z","avatar_url":"https://github.com/azhukaudev.png","language":"TypeScript","readme":"# convex-angular\n\n[![NPM version](https://img.shields.io/npm/v/convex-angular?color=limegreen\u0026label=npm)](https://www.npmjs.com/package/convex-angular)\n[![GitHub license](https://img.shields.io/badge/license-MIT-limegreen.svg)](https://github.com/azhukaudev/convex-angular/blob/main/LICENSE)\n[![NPM downloads](https://img.shields.io/npm/dm/convex-angular?color=limegreen\u0026label=downloads)](https://www.npmjs.com/package/convex-angular)\n\nThe Angular client for Convex.\n\n## ✨ Features\n\n- 🔌 Core providers: `provideConvex`, `injectQuery`, `injectMutation`, `injectAction`, `injectPaginatedQuery`, and `injectConvex`\n- 🔐 Authentication: Built-in support for Clerk, Auth0, and custom auth providers via `injectAuth`\n- 🛡️ Route Guards: Protect routes with `convexAuthGuard`\n- 🎯 Auth Directives: `*cvaAuthenticated`, `*cvaUnauthenticated`, `*cvaAuthLoading`\n- 📄 Pagination: Built-in support for paginated queries with `loadMore` and `reset`\n- ⏭️ Conditional Queries: Use `skipToken` to conditionally skip queries\n- 📡 Signal Integration: [Angular Signals](https://angular.dev/guide/signals) for reactive state\n- 🧹 Auto Cleanup: Automatic lifecycle management\n\n## 🚀 Getting Started\n\n\u003e Requirements: Angular \u003e= 20, Convex \u003e= 1.31, RxJS \u003e= 7.8.\n\n1. Install the dependencies:\n\n```bash\nnpm install convex convex-angular\n```\n\n2. Add `provideConvex` once to your root `app.config.ts` providers:\n\n```typescript\nimport { ApplicationConfig } from '@angular/core';\nimport { provideConvex } from 'convex-angular';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [provideConvex('https://\u003cyour-convex-deployment\u003e.convex.cloud')],\n};\n```\n\n`provideConvex(...)` must be configured only once at the root application level.\nDo not register it again in nested or route-level providers.\n\n3. 🎉 That's it! You can now use the injection providers in your app.\n\n## 📖 Usage\n\n\u003e 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.\n\n### Fetching data\n\nUse `injectQuery` to fetch data from the database.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectQuery } from 'convex-angular';\n\n// Adjust the import path to match your project structure.\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    @if (todos.isLoading()) {\n      \u003cp\u003eLoading...\u003c/p\u003e\n    }\n\n    @if (todos.error()) {\n      \u003cp\u003eError: {{ todos.error()?.message }}\u003c/p\u003e\n    }\n\n    \u003cul\u003e\n      @for (todo of todos.data() ?? []; track todo._id) {\n        \u003cli\u003e{{ todo.title }}\u003c/li\u003e\n      }\n    \u003c/ul\u003e\n  `,\n})\nexport class AppComponent {\n  readonly todos = injectQuery(api.todos.listTodos, () =\u003e ({ count: 10 }));\n}\n```\n\n### Mutating data\n\nUse `injectMutation` to mutate the database.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectMutation } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003cbutton (click)=\"addTodo.mutate({ title: 'Buy groceries' })\"\u003e\n      Add Todo\n    \u003c/button\u003e\n  `,\n})\nexport class AppComponent {\n  readonly addTodo = injectMutation(api.todos.addTodo);\n}\n```\n\n### Running actions\n\nUse `injectAction` to run actions.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectAction } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\u003cbutton (click)=\"completeAllTodos.run({})\"\u003e\n    Complete All Todos\n  \u003c/button\u003e`,\n})\nexport class AppComponent {\n  readonly completeAllTodos = injectAction(api.todoFunctions.completeAllTodos);\n}\n```\n\n### Paginated queries\n\nUse `injectPaginatedQuery` for infinite scroll or \"load more\" patterns.\nYour Convex query must accept a `paginationOpts` argument.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectPaginatedQuery } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    \u003cul\u003e\n      @for (todo of todos.results(); track todo._id) {\n        \u003cli\u003e{{ todo.title }}\u003c/li\u003e\n      }\n    \u003c/ul\u003e\n\n    @if (todos.canLoadMore()) {\n      \u003cbutton (click)=\"todos.loadMore(10)\"\u003eLoad More\u003c/button\u003e\n    }\n\n    @if (todos.isExhausted()) {\n      \u003cp\u003eAll items loaded\u003c/p\u003e\n    }\n  `,\n})\nexport class AppComponent {\n  readonly todos = injectPaginatedQuery(\n    api.todos.listTodosPaginated,\n    () =\u003e ({}),\n    { initialNumItems: 10 },\n  );\n}\n```\n\nThe paginated query returns:\n\n- `results()` - Accumulated results from all loaded pages\n- `isLoadingFirstPage()` - True when loading the first page\n- `isLoadingMore()` - True when loading additional pages\n- `canLoadMore()` - True when more items are available\n- `isExhausted()` - True when all items have been loaded\n- `isSkipped()` - True when the query is skipped via `skipToken`\n- `isSuccess()` - True when the first page has loaded successfully\n- `status()` - `'pending' | 'success' | 'error' | 'skipped'`\n- `error()` - Error if the query failed\n- `loadMore(n)` - Load `n` more items\n- `reset()` - Reset pagination and reload from the beginning\n\n### Conditional queries with skipToken\n\nUse `skipToken` to conditionally skip a query when certain conditions aren't met.\n\n```typescript\nimport { Component, signal } from '@angular/core';\nimport { injectQuery, skipToken } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    @if (user.isSkipped()) {\n      \u003cp\u003eSelect a user to view profile\u003c/p\u003e\n    } @else if (user.isLoading()) {\n      \u003cp\u003eLoading...\u003c/p\u003e\n    } @else {\n      \u003cp\u003e{{ user.data()?.name }}\u003c/p\u003e\n    }\n  `,\n})\nexport class AppComponent {\n  readonly userId = signal\u003cstring | null\u003e(null);\n\n  // Query is skipped when userId is null\n  readonly user = injectQuery(api.users.getProfile, () =\u003e\n    this.userId() ? { userId: this.userId() } : skipToken,\n  );\n}\n```\n\nThis is useful when:\n\n- Query arguments depend on user selection\n- You need to wait for authentication before fetching data\n- A parent query must complete before running a dependent query\n\n### Using the Convex client\n\nUse `injectConvex` to get full flexibility of the Convex client.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectConvex } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\u003cbutton (click)=\"completeAllTodos()\"\u003eComplete All Todos\u003c/button\u003e`,\n})\nexport class AppComponent {\n  readonly convex = injectConvex();\n\n  completeAllTodos() {\n    this.convex.action(api.todoFunctions.completeAllTodos, {});\n  }\n}\n```\n\n### Creating helpers outside the initial injection context\n\nIf you need to create a Convex helper later from plain code, capture an\n`EnvironmentInjector` in DI and pass it as `injectRef`.\n\n```typescript\nimport { Component, EnvironmentInjector, inject } from '@angular/core';\nimport { injectMutation } from 'convex-angular';\n\nimport { api } from '../convex/_generated/api';\n\n@Component({\n  selector: 'app-root',\n  template: `\u003cbutton (click)=\"submit()\"\u003eSave\u003c/button\u003e`,\n})\nexport class AppComponent {\n  private readonly injectRef = inject(EnvironmentInjector);\n\n  submit() {\n    const mutation = injectMutation(api.todos.addTodo, {\n      injectRef: this.injectRef,\n    });\n\n    mutation.mutate({ title: 'Created outside the initial scope' });\n  }\n}\n```\n\nThis works for all public `inject*` helpers, including `injectQuery`,\n`injectPaginatedQuery`, `injectMutation`, `injectAction`, `injectConvex`, and\n`injectAuth`.\n\n## 🔐 Authentication\n\n### Using injectAuth\n\nUse `injectAuth` to access the authentication state in your components.\n\n```typescript\nimport { Component } from '@angular/core';\nimport { injectAuth } from 'convex-angular';\n\n@Component({\n  selector: 'app-root',\n  template: `\n    @switch (auth.status()) {\n      @case ('loading') {\n        \u003cp\u003eLoading...\u003c/p\u003e\n      }\n      @case ('authenticated') {\n        \u003capp-dashboard\u003e\u003c/app-dashboard\u003e\n      }\n      @case ('unauthenticated') {\n        \u003capp-login\u003e\u003c/app-login\u003e\n      }\n    }\n  `,\n})\nexport class AppComponent {\n  readonly auth = injectAuth();\n}\n```\n\nThe auth state provides:\n\n- `isLoading()` - True while auth is initializing\n- `isAuthenticated()` - True when user is authenticated\n- `error()` - Authentication error, if any\n- `status()` - `'loading' | 'authenticated' | 'unauthenticated'`\n\n### Clerk Integration\n\nTo integrate with Clerk, create a service that implements `ClerkAuthProvider` and register it with `provideClerkAuth()`.\n\n```typescript\n// clerk-auth.service.ts\nimport { Injectable, Signal, computed, inject } from '@angular/core';\nimport { Clerk } from '@clerk/clerk-js'; // Your Clerk instance\n\n// app.config.ts\nimport {\n  CLERK_AUTH,\n  ClerkAuthProvider,\n  provideClerkAuth,\n  provideConvex,\n} from 'convex-angular';\n\n@Injectable({ providedIn: 'root' })\nexport class ClerkAuthService implements ClerkAuthProvider {\n  private clerk = inject(Clerk);\n\n  readonly isLoaded = computed(() =\u003e this.clerk.loaded());\n  readonly isSignedIn = computed(() =\u003e !!this.clerk.user());\n  readonly orgId = computed(() =\u003e this.clerk.organization()?.id);\n  readonly orgRole = computed(\n    () =\u003e this.clerk.organization()?.membership?.role,\n  );\n\n  async getToken(options?: { template?: string; skipCache?: boolean }) {\n    try {\n      return (await this.clerk.session?.getToken(options)) ?? null;\n    } catch {\n      return null;\n    }\n  }\n}\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideConvex('https://\u003cyour-convex-deployment\u003e.convex.cloud'),\n    { provide: CLERK_AUTH, useExisting: ClerkAuthService },\n    provideClerkAuth(),\n  ],\n};\n```\n\n### Auth0 Integration\n\nTo integrate with Auth0, create a service that implements `Auth0AuthProvider` and register it with `provideAuth0Auth()`.\n\n```typescript\n// auth0-auth.service.ts\nimport { Injectable, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { AuthService } from '@auth0/auth0-angular';\n// app.config.ts\nimport {\n  AUTH0_AUTH,\n  Auth0AuthProvider,\n  provideAuth0Auth,\n  provideConvex,\n} from 'convex-angular';\nimport { firstValueFrom } from 'rxjs';\n\n@Injectable({ providedIn: 'root' })\nexport class Auth0AuthService implements Auth0AuthProvider {\n  private auth0 = inject(AuthService);\n\n  readonly isLoading = toSignal(this.auth0.isLoading$, { initialValue: true });\n  readonly isAuthenticated = toSignal(this.auth0.isAuthenticated$, {\n    initialValue: false,\n  });\n\n  async getAccessTokenSilently(options?: { cacheMode?: 'on' | 'off' }) {\n    return firstValueFrom(\n      this.auth0.getAccessTokenSilently({ cacheMode: options?.cacheMode }),\n    );\n  }\n}\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideConvex('https://\u003cyour-convex-deployment\u003e.convex.cloud'),\n    { provide: AUTH0_AUTH, useExisting: Auth0AuthService },\n    provideAuth0Auth(),\n  ],\n};\n```\n\n### Custom Auth Providers\n\nFor other auth providers, implement the `ConvexAuthProvider` interface and use `provideConvexAuth()`.\n\n```typescript\n// custom-auth.service.ts\nimport { Injectable, signal } from '@angular/core';\n// app.config.ts\nimport {\n  CONVEX_AUTH,\n  ConvexAuthProvider,\n  provideConvex,\n  provideConvexAuthFromExisting,\n} from 'convex-angular';\n\n@Injectable({ providedIn: 'root' })\nexport class CustomAuthService implements ConvexAuthProvider {\n  readonly isLoading = signal(true);\n  readonly isAuthenticated = signal(false);\n\n  constructor() {\n    // Initialize your auth provider\n    myAuthProvider.onStateChange((state) =\u003e {\n      this.isLoading.set(false);\n      this.isAuthenticated.set(state.loggedIn);\n    });\n  }\n\n  async fetchAccessToken({\n    forceRefreshToken,\n  }: {\n    forceRefreshToken: boolean;\n  }) {\n    return myAuthProvider.getToken({ refresh: forceRefreshToken });\n  }\n}\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    provideConvex('https://\u003cyour-convex-deployment\u003e.convex.cloud'),\n    provideConvexAuthFromExisting(CustomAuthService),\n  ],\n};\n```\n\n`provideConvexAuthFromExisting(...)` registers `CONVEX_AUTH` with `useExisting` and includes `provideConvexAuth()` internally.\n\nIf you wire `CONVEX_AUTH` manually, use `useExisting` (not `useClass`) when the\nauth provider is also injected elsewhere, otherwise you can end up with two\ninstances and auth signal updates won’t reach Convex auth sync.\n\n### Convex Auth (`@convex-dev/auth`)\n\nWhen integrating `@convex-dev/auth`, implement `fetchAccessToken` to return the\nConvex-auth JWT (return `null` when signed out).\n\n```typescript\nimport { Injectable, signal } from '@angular/core';\nimport { ConvexAuthProvider } from 'convex-angular';\n\n@Injectable({ providedIn: 'root' })\nexport class ConvexAuthService implements ConvexAuthProvider {\n  readonly isLoading = signal(true);\n  readonly isAuthenticated = signal(false);\n\n  async fetchAccessToken({\n    forceRefreshToken,\n  }: {\n    forceRefreshToken: boolean;\n  }) {\n    return myAuthProvider.getToken({ refresh: forceRefreshToken });\n  }\n}\n```\n\nWith `provideConvexAuth()` registered, convex-angular will call\n`convex.setAuth(...)` / `convex.client.clearAuth()` automatically when your\nprovider’s `isAuthenticated` changes.\n\n### Auth Directives\n\nUse structural directives to conditionally render content based on auth state.\n\n```html\n\u003c!-- Show only when authenticated --\u003e\n\u003cnav *cvaAuthenticated\u003e\n  \u003cspan\u003eWelcome back!\u003c/span\u003e\n  \u003cbutton (click)=\"logout()\"\u003eSign Out\u003c/button\u003e\n\u003c/nav\u003e\n\n\u003c!-- Show only when NOT authenticated --\u003e\n\u003cdiv *cvaUnauthenticated\u003e\n  \u003cp\u003ePlease sign in to continue.\u003c/p\u003e\n  \u003cbutton (click)=\"login()\"\u003eSign In\u003c/button\u003e\n\u003c/div\u003e\n\n\u003c!-- Show while auth is loading --\u003e\n\u003cdiv *cvaAuthLoading\u003e\n  \u003cp\u003eChecking authentication...\u003c/p\u003e\n\u003c/div\u003e\n```\n\nImport the directives in your component:\n\n```typescript\nimport {\n  CvaAuthLoadingDirective,\n  CvaAuthenticatedDirective,\n  CvaUnauthenticatedDirective,\n} from 'convex-angular';\n\n@Component({\n  imports: [\n    CvaAuthenticatedDirective,\n    CvaUnauthenticatedDirective,\n    CvaAuthLoadingDirective,\n  ],\n  // ...\n})\nexport class AppComponent {}\n```\n\n### Route Guards\n\nProtect routes that require authentication using `convexAuthGuard`.\n\n```typescript\n// app.routes.ts\nimport { Routes } from '@angular/router';\nimport { convexAuthGuard } from 'convex-angular';\n\nexport const routes: Routes = [\n  {\n    path: 'dashboard',\n    loadComponent: () =\u003e\n      import('./dashboard/dashboard.component').then(\n        (m) =\u003e m.DashboardComponent,\n      ),\n    canActivate: [convexAuthGuard],\n  },\n  {\n    path: 'profile',\n    loadComponent: () =\u003e\n      import('./profile/profile.component').then((m) =\u003e m.ProfileComponent),\n    canActivate: [convexAuthGuard],\n  },\n  {\n    path: 'login',\n    loadComponent: () =\u003e\n      import('./login/login.component').then((m) =\u003e m.LoginComponent),\n  },\n];\n```\n\nBy default, unauthenticated users are redirected to `/login`. To customize the redirect route:\n\n```typescript\n// app.config.ts\nimport { CONVEX_AUTH_GUARD_CONFIG } from 'convex-angular';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [\n    // ... other providers\n    {\n      provide: CONVEX_AUTH_GUARD_CONFIG,\n      useValue: { loginRoute: '/auth/signin' },\n    },\n  ],\n};\n```\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit a pull request.\n\n### Repo development\n\n```bash\npnpm install\npnpm dev:backend\npnpm dev:frontend\npnpm test:library\npnpm build:library\n```\n\n## ⚖️ License\n\n[MIT](https://github.com/azhukaudev/convex-angular/blob/main/LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazhukaudev%2Fconvex-angular","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fazhukaudev%2Fconvex-angular","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazhukaudev%2Fconvex-angular/lists"}