Anarchitecture Bricks Docs

Repository documentation hub for packages, guides, and generated references.

@anarchitects/auth-angular

Angular domain libraries for the Anarchitecture auth domain. The package is organized into standalone slices (config, data-access, feature, state, util, ui) that compose implementation-aligned authentication flows for Angular applications.

Migration guidance for the Better Auth realignment lives in the auth migration guide. Migration guidance for the contract-driven auth profile model lives in the auth contract migration guide. Migration guidance for the Angular 22 Signal Forms and Tailwind transition lives in the frontend migration guide.

Developer + AI Agent Start Here

Features

Optional passkeys

See the consumer integration and contract-test guide for public composition, deployment requirements, supported routes, and failure behavior.

Import AuthPasskeyStore and provideAuthPasskeyState from @anarchitects/auth-angular/state/passkeys. Register core provideAuthState() at your chosen session scope, then add ...provideAuthPasskeyState() to the feature route or component providers. The passkey store inherits that core session and provides its own HTTP/browser adapters explicitly.

Trigger enroll({ name: 'My laptop' }) or signIn() from a user action. Observe loading(), success(), cancelled(), and error(); the commands are reactive, not awaitable. cancel() aborts pending browser work, as does destruction of the providing scope. Enrollment keeps the current session; verified sign-in hydrates the core user, RBAC, and CASL ability.

The browser must support secure-context WebAuthn and its native JSON conversion APIs. Check isSupported() and retain a password fallback for unsupported browsers and SSR. Enable the Nest passkeys plugin and run its migrations before use. See the state usage guide for scope, cancellation, and host UX responsibilities, and the adapters guide for advanced composition. The public feature provides AnarchitectsAuthPasskeys and provideAuthPasskeyFeature() for ready-to-use orchestration. The UI entry point exports AnarchitectsAuthUiPasskeys for custom state composition. Both preserve explicit state scope and host-owned navigation.

Authorization Model

CASL integration in @anarchitects/auth-angular mirrors the backend split instead of pretending every check is the same:

Angular should hide or redirect on unauthorized work, but Nest remains the final enforcement boundary for instance-sensitive access.

That mirrors the package split on the backend:

Installation

npm install @anarchitects/auth-angular @anarchitects/tailwind @angular/common @angular/core @angular/forms @angular/router @ngrx/operators @ngrx/signals rxjs
# or
yarn add @anarchitects/auth-angular @anarchitects/tailwind @angular/common @angular/core @angular/forms @angular/router @ngrx/operators @ngrx/signals rxjs

Peer requirements:

Import the Tailwind foundation and include the published auth and forms templates in source detection:

/* styles.css */
@import '@anarchitects/tailwind';

@source './app';
@source '../node_modules/@anarchitects/auth-angular';
@source '../node_modules/@anarchitects/forms-angular';

The internal @anarchitects/auth-ts, @anarchitects/forms-angular, and @anarchitects/forms-ts packages are installed transitively. Runtime utilities such as jwt-decode and @casl/ability are bundled as direct dependencies.

Usage

Quick start

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideAuthConfig, provideAuthContracts } from '@anarchitects/auth-angular/config';
import { provideAuthState } from '@anarchitects/auth-angular/state';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withFetch()),
    ...provideAuthConfig({
      apiResourcePath: 'auth',
    }),
    ...provideAuthContracts(),
    ...provideAuthState(),
  ],
};
// app.component.ts
import { Component, inject } from '@angular/core';
import { AuthStore } from '@anarchitects/auth-angular/state';

@Component({
  selector: 'app-root',
  template: `
    <button (click)="login()">Login</button>
    <p *ngIf="store.isLoggedIn()">Welcome {{ store.loggedInUser()?.email }}</p>
  `,
})
export class AppComponent {
  readonly store = inject(AuthStore);

  login() {
    this.store.login({ credential: 'user@example.com', password: 'secret' });
  }
}

AuthStore.login() is a reactive trigger, not a completion promise. For redirect-after-login flows, observe the state signals using the scoped redirect recipe.

// app.routes.ts
import { Routes } from '@angular/router';
import { policyGuard, resourcePolicyGuard } from '@anarchitects/auth-angular/feature';

export const routes: Routes = [
  {
    path: 'admin',
    canMatch: [policyGuard],
    data: { action: 'manage', subject: 'admin-section' },
    loadComponent: () => import('./admin.component').then((m) => m.AdminComponent),
  },
  {
    path: 'posts/:postId/edit',
    canMatch: [policyGuard],
    canActivate: [resourcePolicyGuard],
    data: {
      action: 'update',
      subject: 'Post',
      resourceKey: 'post',
      unauthorizedRedirectTo: '/posts',
    },
    loadComponent: () => import('./post-edit.component').then((m) => m.PostEditComponent),
  },
];

policyGuard is a coarse route-attempt guard. It answers "may this user attempt work on this subject at all?" by using the shared route matcher from @anarchitects/auth-ts. Concrete ownership checks still belong to loaded resources. Use resourcePolicyGuard for resolved edit/detail routes and canAccessResource(...) / canAccessResourceField(...) for UI elements such as edit buttons.

Contract-driven auth forms

@anarchitects/auth-angular uses the same auth contract profile for rendered form rules and submit-time payload shaping.

Example: require a register name, make login passwords optional, and strip empty optional names before submit.

import { ApplicationConfig } from '@angular/core';
import { provideAuthContracts } from '@anarchitects/auth-angular/config';

export const appConfig: ApplicationConfig = {
  providers: [
    ...provideAuthContracts({
      register: {
        name: {
          required: true,
          minLength: 3,
          emptyStringPolicy: 'strip',
        },
      },
      login: {
        password: {
          required: false,
        },
      },
    }),
  ],
};

The auth UI components consume that profile automatically:

import { Component } from '@angular/core';
import { AnarchitectsAuthUiRegisterForm } from '@anarchitects/auth-angular/ui';

@Component({
  selector: 'app-register-page',
  imports: [AnarchitectsAuthUiRegisterForm],
  template: `<anarchitects-auth-ui-register-form />`,
})
export class RegisterPageComponent {}

If you need the resolved contract object directly, use injectAuthContracts() from @anarchitects/auth-angular/config.

All auth UI and feature form components also accept schemaExtensions. Use it for host-only Angular Signal Forms rules that do not belong in the portable auth contract:

import { validate } from '@angular/forms/signals';
import type { FormsSchemaExtension } from '@anarchitects/forms-angular/ui';

export const rejectBlockedAccount: FormsSchemaExtension = (path) => {
  validate(path['credential'], ({ value }) => (value() === 'blocked@example.com' ? { kind: 'blockedAccount', message: 'This account is blocked.' } : undefined));
};
<anarchitects-auth-feature-login [schemaExtensions]="[rejectBlockedAccount]" />

Payload shaping

Submit raw DTOs from components and let AuthStore apply the profile's emptyStringPolicy rules before the HTTP call.

import { Component, inject } from '@angular/core';
import { AuthStore } from '@anarchitects/auth-angular/state';

@Component({
  selector: 'app-register-action',
  template: `<button type="button" (click)="register()">Register</button>`,
})
export class RegisterActionComponent {
  private readonly authStore = inject(AuthStore);

  register() {
    this.authStore.registerUser({
      name: '',
      email: 'jane@example.com',
      password: 'secret123',
      confirmPassword: 'secret123',
    });
  }
}

With a contract such as register.name.required = false plus emptyStringPolicy = 'strip', the store omits name from the outgoing request body instead of sending name: ''.

AuthUser compatibility

Optional JWT plugin wiring

Keep the root auth surface session-first. Only wire the JWT plugin when the host app explicitly needs token-based flows:

import { provideHttpClient } from '@angular/common/http';
import { withJwtAuthHttpInterceptors } from '@anarchitects/auth-angular/data-access/jwt';
import { provideAuthJwtState } from '@anarchitects/auth-angular/state/jwt';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient(withJwtAuthHttpInterceptors()), ...provideAuthJwtState()],
};

Blog ownership example:

import { Component, computed, inject, input } from '@angular/core';
import { canAccessResource, canAccessResourceField } from '@anarchitects/auth-angular/util';
import { AuthStore } from '@anarchitects/auth-angular/state';

@Component({
  selector: 'app-post-actions',
  template: `
    @if (canEdit()) {
      <a [routerLink]="['/posts', post().id, 'edit']">Edit</a>
    }
    @if (canEditTitle()) {
      <button type="button">Rename title</button>
    }
  `,
})
export class PostActionsComponent {
  readonly post = input.required<{ id: string; authorId: string }>();
  private readonly authStore = inject(AuthStore);

  readonly canEdit = computed(() => canAccessResource(this.authStore.ability(), 'update', 'Post', this.post()));

  readonly canEditTitle = computed(() => canAccessResourceField(this.authStore.ability(), 'update', 'Post', 'title', this.post()));
}

Entry points

Import path Description
@anarchitects/auth-angular/config DI tokens and providers
@anarchitects/auth-angular/data-access Generated API clients and HTTP adapters
@anarchitects/auth-angular/data-access/jwt JWT plugin APIs and interceptor helpers
@anarchitects/auth-angular/state Signal store, eager restore, CASL ability sync
@anarchitects/auth-angular/state/jwt JWT refresh-token state orchestration
@anarchitects/auth-angular/feature Coarse and resource-aware router guards
@anarchitects/auth-angular/feature/jwt JWT refresh-token orchestration components
@anarchitects/auth-angular/ui Auth domain form UI components
@anarchitects/auth-angular/ui/jwt JWT refresh-token form components
@anarchitects/auth-angular/util CASL ability/resource helpers and typings
@anarchitects/auth-angular/data-access/passkeys Passkey HTTP and browser adapters
@anarchitects/auth-angular/state/passkeys Explicitly scoped passkey state
@anarchitects/auth-angular/feature/passkeys Enrollment/sign-in component and provider helper
@anarchitects/auth-angular/ui/passkeys Presentational passkey actions and status

Nx scripts

Development notes

Migrating to the Signal Forms release

This is a breaking Angular 22-only release:

Auth DTOs, contract profiles, selectors, outputs, provider helpers, orchestration, explicit state scoping, and public workflows remain unchanged.

License

Licensed under the Apache License, Version 2.0.

Canonical version and verification

This source line consumes @anarchitects/auth-contracts@0.1.0 through the domain TS compatibility bindings. See the canonical conformance guide for exact checks, configured-host obligations and remaining capability gates. Generated implementation OpenAPI and passing fixtures do not approve a deployed host's security profile. Existing runtime support and TypeBox exports remain.

Source Links