ngx-better-auth
June 24, 2026 ยท View on GitHub
An Angular 20+ wrapper for Better Auth. Provides reactive session handling with signals, clean DI provider setup with observables, and modern guards.
๐ Compatibility
| ngx-better-auth | Angular | Better Auth |
|---|---|---|
1.6.x | >=20 | >=1.6.10 <1.7.0 |
0.11.x | >=20 | >=1.3.7 |
๐ฆ Installation
npm install ngx-better-auth better-auth
โ๏ธ Setup Provider
First, configure your Better Auth client in your application:
// app.config.ts
import { ApplicationConfig } from '@angular/core'
import { provideBetterAuth } from 'ngx-better-auth'
import { environment } from './environments/environment'
import { adminClient, siweClient, twoFactorClient, usernameClient } from 'better-auth/client/plugins'
import { passkeyClient } from '@better-auth/passkey/client'
import { stripeClient } from '@better-auth/stripe/client'
export const appConfig: ApplicationConfig = {
providers: [
provideBetterAuth({
baseURL: environment.apiUrl, // it works also with proxy config
basePath: '/auth', // optional, default is '/api/auth'
// Example with plugins
plugins: [
usernameClient(),
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = '/two-factor-auth'
},
}),
adminClient({
ac: accessControl,
roles: {
admin,
moderator,
user,
},
}),
passkeyClient(),
stripeClient({ subscription: true }),
siweClient(),
],
})
]
}
๐งฉ Different services
Migrating to 1.6.x
ngx-better-auth 1.6.x targets Better Auth >=1.6.10 <1.7.0.
Optional plugin services can be imported from secondary entrypoints when they rely on optional peer dependencies:
import { AuthService, provideBetterAuth } from 'ngx-better-auth'
import { ApiKeyService } from 'ngx-better-auth/api-key'
import { OAuthProviderService } from 'ngx-better-auth/oauth-provider'
import { PasskeyService } from 'ngx-better-auth/passkey'
import { ScimService } from 'ngx-better-auth/scim'
import { SsoService } from 'ngx-better-auth/sso'
import { StripeService } from 'ngx-better-auth/stripe'
๐ Plugin compatibility
Authentication
- โ
Two Factor โก๏ธ
TwoFactorService - โ
Username โก๏ธ
UsernameService - โ
Anonymous โก๏ธ
AnonymousService - โ
Phone Number โก๏ธ
PhoneNumberService - โ
Magic Link โก๏ธ
MagicLinkService - โ
Email OTP โก๏ธ
EmailOtpService - โ
Passkey โก๏ธ
PasskeyServicefromngx-better-auth/passkey - โ
Generic OAuth โก๏ธ
GenericOauthService - โ
OAuth Popup โก๏ธ
OauthPopupService - โ
One Tap โก๏ธ
OneTapService - โ
Sign In With Ethereum (SIWE) โก๏ธ
SiweService
Authorization & Management
- โ
Admin โก๏ธ
AdminService - โ
Organization โก๏ธ
OrganizationService
API & Tokens
- โ
Last Login Method โก๏ธ
LastLoginMethodService - โ
Multi Session โก๏ธ
MultiSessionService - โ
One Time Token โก๏ธ
OneTimeTokenService - โ
JWT โก๏ธ
JwtService - โ
Bearer โก๏ธ
BearerService,bearerHeaders(),bearerFetchOptions() - โ
API Key โก๏ธ
ApiKeyServicefromngx-better-auth/api-key
OAuth & OIDC Providers
- โ
Device Authorization โก๏ธ
DeviceAuthorizationService - โ
OAuth 2.1 Provider โก๏ธ
OAuthProviderServicefromngx-better-auth/oauth-provider - โ
SSO โก๏ธ
SsoServicefromngx-better-auth/sso
Payments & Billing
- โ
Stripe โก๏ธ
StripeServicefromngx-better-auth/stripe
Security & Utilities
- โ
Captcha โก๏ธ
captchaHeaders(),captchaFetchOptions()for thex-captcha-responseheader - โ
Open API โก๏ธ
OpenApiService - โ
SCIM โก๏ธ
ScimServicefromngx-better-auth/scim
๐ Real-time Session
AuthService keeps the session in sync automatically
sessionโ a signal with the current session or nullisLoggedInโ a computed boolean
Demonstration of usage in a component
import { AuthService } from "ngx-better-auth"
import { inject } from "@angular/core"
@Component({
// ...
})
export class MyComponent {
private readonly authService = inject(AuthService)
get isLoggedIn() {
return this.authService.isLoggedIn()
}
get userName() {
return this.authService.session()?.user.name
}
}
โก Signal resources for reads
GET/list-style methods keep their existing Observable API and also expose Angular resource factories for zoneless-friendly templates.
Mutations such as sign in, sign out, update, delete, revoke, invite, and verify still use Observable because they are command workflows.
Available resource factories
SessionService.sessionsResource()AccountService.accountsResource()PasskeyService.userPasskeysResource()OrganizationService.organizationsResource()OrganizationService.fullOrganizationResource(() => params)OrganizationService.invitationResource(() => params)OrganizationService.invitationsResource(() => params)OrganizationService.userInvitationsResource()OrganizationService.activeMemberResource()AdminService.usersResource(() => params)AdminService.userSessionsResource(() => params)StripeService.listResource(() => params)MultiSessionService.deviceSessionsResource()OrganizationService.activeMemberRoleResource(() => params)OrganizationService.rolesResource(() => params)OrganizationService.teamsResource(() => params)OrganizationService.userTeamsResource()OrganizationService.teamMembersResource(() => params)
Simple read resource
import { Component, inject } from '@angular/core'
import { SessionService } from 'ngx-better-auth'
@Component({
// ...
})
export class SessionsComponent {
private readonly sessionService = inject(SessionService)
readonly sessions = this.sessionService.sessionsResource()
}
@if (sessions.isLoading()) {
<p>Loading sessions...</p>
} @else if (sessions.error()) {
<p>Unable to load sessions.</p>
} @else {
@for (session of sessions.value() ?? []; track session.token) {
<p>{{ session.ipAddress }}</p>
}
}
Parameterized read resource
import { Component, inject, signal } from '@angular/core'
import { OrganizationService } from 'ngx-better-auth'
@Component({
// ...
})
export class OrganizationComponent {
private readonly organizationService = inject(OrganizationService)
readonly organizationId = signal<string | undefined>(undefined)
readonly organization = this.organizationService.fullOrganizationResource(() => ({
organizationId: this.organizationId(),
membersLimit: 20,
}))
selectOrganization(organizationId: string) {
this.organizationId.set(organizationId)
}
}
Call resource.reload() after a mutation when you need to refresh a read resource manually.
๐ก๏ธ Guards
This library ships with guards to quickly set up route protection.
Helpers
redirectUnauthorizedTo(['/login'])โ redirect if not logged inredirectLoggedInTo(['/'])โ redirect if already logged inhasRole(['admin'], ['/unauthorized'])โ restrict access by role and redirect if not authorizedhasOrganizationRole(['owner', 'admin'], ['/unauthorized'])โ restrict access by active organization rolehasActiveOrganization(['/select-organization'])โ require an active organization member
Usage in routes
import { Routes } from '@angular/router'
import { canActivate, redirectLoggedInTo, redirectUnauthorizedTo, hasRole, hasOrganizationRole, hasActiveOrganization } from 'ngx-better-auth'
export const routes: Routes = [
{
path: '',
component: SomeComponent,
...canActivate(redirectUnauthorizedTo(['/login']))
},
{
path: 'admin',
component: AdminComponent,
...canActivate(hasRole(['admin'], ['/unauthorized']))
},
{
path: 'organization',
component: OrganizationComponent,
...canActivate(hasOrganizationRole(['owner', 'admin'], ['/unauthorized']))
},
{
path: 'organization/select',
component: SelectOrganizationComponent,
...canActivate(hasActiveOrganization(['/select-organization']))
},
{
path: 'login',
component: LoginComponent,
...canActivate(redirectLoggedInTo(['/']))
}
]
โ Validators
The username plugin provides validators that work seamlessly with both reactive and template-driven forms.
import { FormControl } from '@angular/forms'
import { inject } from '@angular/core'
import { UsernameAvailableValidator } from 'ngx-better-auth'
const usernameService = inject(UsernameService)
const initialUsername = 'thomas-orgeval'
const usernameControl = new FormControl('', {
asyncValidators: [usernameAvailableValidator(usernameService, initialUsername)],
updateOn: 'change'
})
๐ฐ Stripe subscriptions
Configure Better Auth with stripeClient({ subscription: true }), then inject StripeService to start checkouts, list subscriptions, cancel, restore, or open the billing portal.
import { inject } from '@angular/core'
import { StripeService } from 'ngx-better-auth/stripe'
export class UsageComponent {
private readonly subscriptions = inject(StripeService)
startCheckout(orgId: string) {
return this.subscriptions.upgrade({
plan: 'starter',
annual: false,
customerType: 'organization',
referenceId: orgId,
successUrl: '/organization/usage?checkout=success',
cancelUrl: '/organization/usage?checkout=cancelled',
})
}
}