Anarchitecture Bricks Docs

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

Passkey consumer integration

This guide covers enrollment and sign-in through the public auth packages. The host composes one auth backend and one core Angular session store. It does not create a second Better Auth instance or import package internals.

Available public surfaces

Package entry point Consumer surface
@anarchitects/auth-ts/dtos/passkeys Begin/finish request and response DTOs and route schemas
@anarchitects/auth-nest AuthModule, AuthPasskeyService
@anarchitects/auth-nest/presentation AuthPresentationModule, PasskeyAuthController
@anarchitects/auth-nest/application AuthApplicationModule, AuthPasskeyService for advanced composition
@anarchitects/auth-nest/infrastructure-persistence PasskeyEntity and passkey migrations alongside core auth entities/migrations
@anarchitects/auth-angular/data-access/passkeys PasskeyApi, WebAuthnClient, PasskeyBrowserError
@anarchitects/auth-angular/state/passkeys AuthPasskeyStore, provideAuthPasskeyState, PasskeyEnrollmentOptions
@anarchitects/auth-angular/feature/passkeys AnarchitectsAuthPasskeys, provideAuthPasskeyFeature
@anarchitects/auth-angular/ui/passkeys AnarchitectsAuthUiPasskeys, PasskeyMode, PasskeyUiLabels

Passkey listing, rename, and removal are not exposed by the package's public HTTP surface. Do not assume Better Auth's internal management endpoints are mounted by AuthModule.

Nest facade and persistence

Configure TypeORM once at the host root, with migrations and synchronize: false. For a fresh database, the following uses only public package exports. Existing hosts append the missing migrations to their established migration list and apply them before rollout; see upgrade and rollback guidance.

import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '@anarchitects/auth-nest';
import { CreateAuthSchema1720200000000, CreateBetterAuthPasskeysTable1760200001000, AddBetterAuthAccountIssuer1788275931000, ExpandPasskeyCredentialStorage1790899200000 } from '@anarchitects/auth-nest/infrastructure-persistence';

// Resolve these values in the host configuration layer.
export function authImports(databaseUrl: string, secret: string) {
  return [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: databaseUrl,
      autoLoadEntities: true,
      synchronize: false,
      migrations: [CreateAuthSchema1720200000000, CreateBetterAuthPasskeysTable1760200001000, AddBetterAuthAccountIssuer1788275931000, ExpandPasskeyCredentialStorage1790899200000],
    }),
    AuthModule.forRoot({
      mailer: { provider: 'noop' },
      presentation: {
        application: {
          betterAuth: {
            baseUrl: 'https://api.example.com/api/auth',
            secret,
            callbackUrls: {
              verifyEmail: 'https://app.example.com/verify-email',
              resetPassword: 'https://app.example.com/reset-password',
            },
          },
          plugins: {
            passkeys: {
              enabled: true,
              rpID: 'example.com',
              rpName: 'Example',
              origin: 'https://app.example.com',
            },
          },
        },
      },
    }),
  ];
}

Use those imports in the host module. Apply migrations with the host's migration runner; listing migrations alone does not run them. The test host uses migrationsRun: true only for its disposable database. Configure a mail transport if the host needs verification/recovery emails; the snippet explicitly disables mail delivery using the no-op provider.

The enabled plugin registers its entity for Nest's autoLoadEntities. For a standalone runtime or migration DataSource, explicitly include the public entities:

import { AccountEntity, AuthUserEntity, PermissionEntity, RoleEntity, SessionEntity, VerificationEntity, PasskeyEntity } from '@anarchitects/auth-nest/infrastructure-persistence';

const entities = [AccountEntity, AuthUserEntity, PermissionEntity, RoleEntity, SessionEntity, VerificationEntity, PasskeyEntity];

Keep any cross-domain foreign keys in host integration schemas, following the repository's runtime/migrations DataSource convention. No cross-domain TypeORM relations belong in domain entities.

For configuration-driven composition, AuthModule.forRootFromConfig(overrides) reads the typed auth configuration (AUTH_PLUGIN_PASSKEYS_ENABLED, AUTH_PLUGIN_PASSKEY_RP_ID, AUTH_PLUGIN_PASSKEY_RP_NAME, AUTH_PLUGIN_PASSKEY_ORIGIN). Explicit overrides take precedence. Advanced hosts can use AuthPresentationModule or compose AuthApplicationModule and a public controller themselves; they own cookie forwarding when calling the service directly.

The expected origin is server configuration, not a value selected by a request. For separate frontend/backend origins, configure the frontend origin explicitly. The RP ID must match or be a parent domain of its hostname. A same-origin /api proxy is the simplest Angular deployment: the adapter uses relative URLs. Configure host CORS/cookie policy deliberately if using a cross-origin interceptor. Production requires HTTPS; localhost is used by the executable contract host.

HTTP contract

All four endpoints are POST and return 200 on success. Paths below include the host's /api prefix; the controller itself starts at /auth/passkeys.

Path Request Success body Session requirement
/api/auth/passkeys/registration/begin {} or { authenticatorAttachment: 'platform' | 'cross-platform' } WebAuthn JSON creation options Fresh authenticated session
/api/auth/passkeys/registration/finish { response: registrationCredential, name?: string } { success: true } Same fresh authenticated user
/api/auth/passkeys/authentication/begin {} WebAuthn JSON request options Public
/api/auth/passkeys/authentication/finish { response: authenticationCredential } { user, rbac } Public; establishes session after verification

Forward all incoming cookies and every returned Set-Cookie header. The browser must preserve challenge cookies between begin and finish. Registration binds the challenge to the authenticated user. Authentication finish returns the repository's session envelope, with no access/refresh token JSON. Call /api/auth/me to restore that session and /api/auth/logout to end it.

Malformed DTOs return 400. Anonymous/stale registration sessions and verifier failures return 4xx responses; clients must not depend on Better Auth error wording. A failed ceremony must not be treated as authentication. Disabled plugins mount none of these routes (404). Challenge expiry is five minutes and verification consumes challenges, including failed verification attempts; retry starts a new begin request. User verification follows the current preferred policy, so the capability does not promise a mandatory biometric/PIN step.

The local generated OpenAPI specification records the transitional structural baseline; canonical public HTTP authority belongs to anarchitecture-contracts under the TS Contracts Guide. Cryptographic checks and browser lifecycle behavior are exercised by the tests below; canonical adoption additionally requires conformance to the pinned contract version.

Angular host composition

Provide HttpClient and core auth state at the intended session scope:

import { provideHttpClient } from '@angular/common/http';
import { provideAuthConfig } from '@anarchitects/auth-angular/config';
import { provideAuthState } from '@anarchitects/auth-angular/state';

const providers = [provideHttpClient(), ...provideAuthConfig({ apiResourcePath: 'auth' }), ...provideAuthState()];

Use the public feature and explicitly scope its state on the host component. The helper inherits the nearest core AuthStore and includes the passkey adapters.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AnarchitectsAuthPasskeys, provideAuthPasskeyFeature } from '@anarchitects/auth-angular/feature/passkeys';

@Component({
  selector: 'app-passkey-sign-in',
  imports: [AnarchitectsAuthPasskeys],
  providers: [...provideAuthPasskeyFeature()],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <anarchitects-auth-passkeys>
      <a passkeyFallback href="/login">Use password sign-in</a>
    </anarchitects-auth-passkeys>
  `,
})
export class PasskeySignIn {}

For enrollment, use mode="enroll" and [enrollmentOptions]="{ name: 'This device' }". Enrollment is disabled until a session is present; the backend enforces freshness. Both modes wait during core auth loading/restoration. Use labels for text overrides and disabled for host conditions. The host owns fallback routing and navigation after success. Observe AuthStore/AuthPasskeyStore signals; the component never prompts on startup.

Advanced consumers can use provideAuthPasskeyState() with custom UI, or bind AnarchitectsAuthUiPasskeys inputs/outputs directly. Components introduce no hidden providers that mask host adapter overrides. Component-scoped providers abort pending prompts on destruction. Longer-lived route/application scopes can outlive a view; explicitly cancel pending work when leaving that view.

When composing state directly, authenticate first and call passkeys.enroll({ name: 'This device' }) from a user action. Enrollment leaves the existing session intact; verified sign-in hydrates the core user, RBAC, and CASL ability. The commands are reactive and are not awaitable promises. Observe signals for completion and own navigation in the host.

Only one ceremony runs per store at a time. Cancellation and scope destruction abort browser work and unsubscribe pending HTTP requests; they cannot undo a request already processed by the server. Cancel before starting another auth workflow, such as logout. Cancellation/timeout sets cancelled; other failures set error, and either outcome permits a new attempt.

Secure-context WebAuthn and native parseCreationOptionsFromJSON, parseRequestOptionsFromJSON, and credential toJSON() APIs are required. isSupported() returns false on SSR or unsupported browsers. Keep password fallback; do not prompt automatically during startup or SSR. Credentials/challenges are not written to localStorage by the package.

The executable example shows enrollment, sign-in, logout, session display, cancellation, and fallback using only public Angular imports.

Contract validation

Start Docker and install this workspace's Chromium once:

yarn playwright install chromium
yarn nx run auth-angular-example-e2e:passkey-contract-test

The target builds the example and backend, starts disposable PostgreSQL with real migrations, serves the consumer at http://localhost:4318, and cleans up on exit. Set PASSKEY_CONTRACT_PORT if that port is occupied. It never connects to a host's existing database. The dedicated CI workflow installs Chromium with system dependencies and runs the target without distributed execution.

Target Evidence
auth-angular-example-e2e:passkey-contract-test Native Chromium JSON conversion and virtual authenticator → public Angular store/API → public Nest facade → published TypeORM adapter/PostgreSQL; enrollment, cookie session/restore, cancellation, feature teardown, signature rejection/retry, replay, missing cookies, anonymous enrollment, malformed requests, unsupported-browser fallback
auth-nest:test-passkeys Real verifier checks for challenge/origin/RP ID, expiry, ownership, signature, replay, counters, and disabled capability, using memory persistence
auth-nest:test-published-adapter PostgreSQL persistence, restart, concurrent replay, uniqueness, migration upgrade/rollback, counter limits and user cascade
auth-angular:test SSR/unsupported API handling, scoped stores, duplicate commands, cancellation, malformed session/RBAC and stale restoration
angular-consumer-compatibility:test-22 Packs the libraries, asserts all four passkey exports, and compiles the example in an isolated npm consumer without workspace aliases
api-specs:lint and api-specs:verify Generated route/schema consistency

The browser suite uses Playwright CDP sessions and Chromium's virtual authenticator, not a physical device. It proves browser API serialization and server verification without mocking successful auth responses. Physical hardware, platform account sync, other browsers, and production proxy/TLS/cookie deployment remain host acceptance checks. The older auth-angular-example:contract-test Prism target checks password API schemas; Prism alone cannot validate passkey cryptography or session cookies.