Skip to main content

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:

  1. The canonical anonymous id — one first-party cookie, generated upstream of every vendor SDK, not one id minted per vendor.
  2. The identity state machineanonymous ⇄ identified (see below).
  3. 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 a useIdentity() hook.
  • identity-adapter-angular — an IdentityModule/APP_INITIALIZER, an injectable IdentityService exposing state as an RxJS Observable, 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: a device/anonymous context pre-login, and a multi-kind context containing both device and user kinds together on login — LaunchDarkly associates activity by context composition instead of a merge event.
  • FullStory's setIdentity auto-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.