@idhub/identity-adapter-angular
v0.1.0
Published
Angular adapter for @idhub/identity-core: IdentityModule.forRoot(), an RxJS-backed IdentityService, an APP_INITIALIZER that establishes the anonymous id before the first route renders, an optional HttpInterceptor, and a companion Express middleware for An
Readme
@idhub/identity-adapter-angular
Angular adapter for @idhub/identity-core. Zero vendor knowledge — it
only knows the IdentityProvider / IdentityClient contract from the core package.
import { IdentityModule } from '@idhub/identity-adapter-angular'
@NgModule({
imports: [
IdentityModule.forRoot({
providers: [segmentProvider, launchDarklyProvider, fullStoryProvider],
cookie: { name: '_anon_id', domain: '.example.com' },
enableHttpInterceptor: true,
}),
],
})
export class AppModule {}@Component({ /* ... */ })
export class HeaderComponent {
readonly state$ = this.identity.state$ // Observable<IdentityState>
constructor(@Inject(IdentityService) private identity: IdentityService) {}
onLogin(userId: string) { this.identity.identify(userId, { plan: 'pro' }) }
onLogout() { this.identity.reset() }
}What it gives you
| Export | Purpose |
|---|---|
| IdentityModule.forRoot(config) | Registers IdentityService, the APP_INITIALIZER, and (opt-in) the interceptor. |
| IdentityService | state$: Observable<IdentityState>, snapshot, identify(), reset(), track(), page(). |
| IdentityHttpInterceptor | Adds X-Identity-Id to every HttpClient request. Registered only via enableHttpInterceptor: true. |
| identityExpressMiddleware() (/server) | Companion SSR middleware for Angular Universal. |
| IDENTITY_CONFIG, IDENTITY_ID_HEADER | DI token / header name, for tests and advanced wiring. |
The APP_INITIALIZER guarantee
Angular blocks bootstrap — and therefore the first route render — until every
APP_INITIALIZER settles. IdentityService is in the initializer's deps, so Angular
must construct it first, and construction runs identity-core's createIdentityClient(),
which resolves the canonical anonymous id and fans onAnonymous out to every provider
synchronously. This is Angular's equivalent of the Next.js adapter's middleware
guarantee, adapted to the bootstrap lifecycle because Angular has no request-time
middleware of its own.
The e2e suite asserts this as an ordering property, not a state check: the test-double
provider records every hook onto a window-global log, and
e2e/identity.spec.ts asserts the log reads exactly
bootstrap_start → onAnonymous → app_initializer_done → component_init.
Known limitation. identity-core does not surface the promises returned by async
onAnonymous implementations, so the initializer guarantees every provider has been
called before first render — not that a provider's own network round-trip finished.
Providers needing that should contribute a second APP_INITIALIZER.
SSR / Angular Universal
import { identityExpressMiddleware } from '@idhub/identity-adapter-angular/server'
server.use(identityExpressMiddleware({ cookie: { name: '_anon_id' } }))
// downstream: req.anonymousId is set, Set-Cookie is appendedSeparate entry point on purpose — browser bundles never pull it in.
It uses identity-core's cookie primitives (parseCookieHeader,
serializeAnonymousIdCookie, resolveCookieConfig) rather than the Web Standard
getOrCreateAnonymousIdFromRequest, because Express exposes a Node IncomingMessage,
not a Request. Wrapping every request in a synthetic Request would allocate on every
hit and still need unwrapping to append Set-Cookie. Cookie naming, defaults and
serialization still come from identity-core — only the transport differs, so there is no
duplicated cookie logic. Express's real Request/Response satisfy the structural
IdentityRequestLike / IdentityResponseLike shapes, so no @types/express dependency
is needed. Set-Cookie is appended, never replaced, so it composes with session cookies.
Build strategy (tsup, not ng-packagr)
Angular libraries are normally built with ng-packagr. This package uses tsup, matching
the repo-wide convention, which is viable because it ships an injectable service, a
providers-only NgModule and an interceptor — no component templates to compile.
Two constraints follow, and both are handled:
- No
emitDecoratorMetadata. esbuild cannot emitdesign:paramtypes. Every injection site is therefore explicit —@Inject(...)parameter decorators plususeFactory+depsinforRoot()— so nothing relies on reflected metadata. useDefineForClassFields: false, required for legacy decorator semantics at the ES2022 target.
AOT caveat. tsup emits plain __decorateClass calls rather than ng-packagr's
partial-Ivy output, so the decorated classes are compiled by Angular's JIT compiler.
That is what the test app exercises. An app doing a strict AOT build against these
classes would want an ng-packagr (partial-compilation) artifact instead. If a consumer
hits that, the fallback is a ng-packagr build step for this package only — the source
does not need to change.
Angular version support
Developed and tested against Angular 20.3.30 (JIT bootstrap via
platformBrowserDynamic). Peer range is >=18 <23. APP_INITIALIZER is deprecated from
v19 onward in favour of provideAppInitializer but still present through v22; it is used
here because it is the documented multi-provider contract for this package.
Local development
pnpm --filter @idhub/identity-adapter-angular build # tsup, dual ESM+CJS
pnpm --filter @idhub/identity-adapter-angular test # vitest unit tests
pnpm --filter @idhub/identity-adapter-angular test:e2e # playwright against test-app/test-app/ is a throwaway Angular app (one module, one component, a recording
test-double provider) bundled by a 20-line esbuild script and served by a 30-line static
server — deliberately not an Angular CLI workspace, which would add a second build system
to the repo for two files.
