Architecture
@idhub is built as three independently versioned layers instead of one
framework-specific or one vendor-specific package. Adapted from
ADR 0001: Portable identity resolution package architecture.
identity-core <- state machine. zero vendor deps, zero framework deps
├─ identity-provider-segment <- implements IdentityProvider for Segment
├─ identity-provider-launchdarkly <- implements IdentityProvider for LaunchDarkly
├─ identity-provider-fullstory <- implements IdentityProvider for FullStory
├─ identity-adapter-nextjs <- depends on identity-core only
└─ identity-adapter-angular <- depends on identity-core only
Layer 1: identity-core
Owns exactly three things, and nothing about any vendor or any framework:
- The canonical anonymous id — one first-party cookie, generated upstream of every vendor SDK, not one id minted per vendor.
- The identity state machine —
anonymous ⇄ identified(see below). - The provider registry + fan-out — a single
identify()/track()/page()/reset()call on the client reaches every registered provider.
identity-core has zero runtime dependencies — no React, no Angular, no
vendor SDK — and is designed to stay that way permanently. Any future feature
that needs a runtime dependency belongs in a provider or adapter package
instead.
Layer 2: identity-provider-*
One package per vendor, each implementing a single IdentityProvider
interface (onAnonymous / onIdentify / onReset, plus optional onTrack /
onPage). The vendor SDK is a peerDependency, so your app controls the SDK
version and loading strategy independently of this package's release cadence.
Adding a 4th vendor is one new Layer-2 package implementing IdentityProvider,
with zero changes to identity-core or either adapter.
Layer 3: identity-adapter-*
One package per framework, typed entirely against IdentityClient /
IdentityProvider from identity-core and containing zero vendor knowledge.
identity-adapter-nextjs— Edge middleware that guarantees the anonymous-id cookie exists before first render, plus a React context component and auseIdentity()hook.identity-adapter-angular— anIdentityModule/APP_INITIALIZER, an injectableIdentityServiceexposing state as an RxJSObservable, and a companion Express middleware for Angular Universal SSR, since Angular has no request-time middleware of its own.
Adding a 5th framework is one new Layer-3 package with the same zero-changes-elsewhere guarantee.
The identity state machine
anonymous -> identified (login / registration)
identified -> anonymous (logout)
Logout issues a new anonymous id — it never reverts to the pre-login one.
Reusing it would let a second person on a shared device inherit the previous
person's anonymous history. This mirrors FullStory's anonymize() semantics.
Why there's no alias()-shaped method
The old, pre-@idhub code in this codebase called each vendor's own
alias-style method directly on every anonymous→known transition
(analytics.alias(), ldClient.alias()) — and that logic was duplicated,
independently, per app. Vendor research (verified against current vendor docs)
found that all three vendors have since converged on not needing a
generic alias concept:
- Segment now frames
alias()as an advanced/exception call, not the default —identify(userId, traits)alone is sufficient because the anonymousId already travels with every call from the same client. - LaunchDarkly has deprecated/removed
alias()entirely in favor of multi-kind contexts: adevice/anonymous context pre-login, and a multi-kind context containing bothdeviceanduserkinds together on login — LaunchDarkly associates activity by context composition instead of a merge event. - FullStory's
setIdentityauto-merges every prior anonymous session on the device into the new identity — no separate alias call exists or is needed.
Because all three vendors converged on the same shape — carry one stable
anonymous id through the anonymous state, then fire one identify-style call
per vendor at the login moment — identity-core's IdentityProvider
interface deliberately has no alias()-shaped method at all. A provider
that still needs a vendor-specific exception handles it internally inside its
own onIdentify, never as a concept the core or your application code needs
to know about.