Dotyc
API Reference

IdentityProvider

The identity provider interface: emitter facet (get/subscribe) and ingest facet (resolve), plus the Identity model.

Identity providers are the third family of Dotyc building blocks — resolvers, distinct from adapters (consumers). One provider ideally exposes both facets in a single package: the emitter facet reads the current user from your auth SDK; the ingest facet verifies the session or JWT server-side.

Interface

interface IdentityProvider {
  emitter?: {
    get(): IdentityClaim | null
    subscribe(cb: (i: IdentityClaim | null) => void): () => void
  }
  ingest?: {
    resolve(request: Request): Promise<Identity | null> // session or JWT
  }
}
MemberDescription
emitter.getReturns the current identity claim, or null when signed out.
emitter.subscribeNotifies on identity changes (login, logout, refresh); returns an unsubscribe function. The emitter absorbs noisy notifications via its dedupe hash.
ingest.resolveResolves the incoming Request (session cookie or JWT) into a verified identity, or null.

The Identity model

interface Identity {
  anonymousId: string // lightweight per-device id — cookie (localStorage fallback)
  sessionId: string   // lightweight per-session id — volatile, rotates on inactivity
  userId?: string     // the reconciliation pivot (verified at ingest)
  accountId?: string  // the organization / account (B2B)
  traits?: Record<string, unknown>
  verified: boolean   // false = emitter claim, true = resolved at ingest
}

IdentityClaim is the same shape without verified — it is what the emitter sends inside the Envelope.

Trust model: client-declared identity is a claim; identity resolved at ingest is the truth. When both exist, the verified one wins and overrides the claim. For apps without a backend session, the provider verifies the JWT issued by your auth (Clerk, Supabase, Neon Auth, Better Auth all emit one) — verified identity with no server of your own.

Reconciliation: as soon as an ingested event carries both lightweight ids and a verified identity, the anonymousId/sessionId to userId link is signaled to adapters through their identify hook — without waiting for an explicit identify() call. Dedupe caches at both stages (persisted hash on the emitter, a TTL cache backed by the dispatcher's store) prevent hammering. The merge itself belongs to the destinations: Dotyc stores nothing.

Usage

import { clerkIdentity } from '@dotyc/clerk/client'
const dotyc = createEmitter({ catalog, transport, identity: clerkIdentity() })
import { clerkIdentity } from '@dotyc/clerk/server'
const dispatcher = createDispatcher({ catalog, wiring, identity: clerkIdentity() })

A custom ingest-side provider:

const myIdentity: IdentityProvider = {
  ingest: {
    async resolve(request) {
      const session = await auth.getSession(request)
      if (!session) return null
      return {
        anonymousId: readAnonymousCookie(request),
        sessionId: readSessionIdCookie(request),
        userId: session.userId,
        accountId: session.orgId,
        verified: true,
      }
    },
  },
}

Notes

  • Without a provider, identity is manual: identify() / reset() on the emitter.
  • identify() is not a catalog event; login business logic belongs to an explicit catalog event such as signed_in.
Draft — subject to change: how a claim/verified conflict is surfaced (silent override, warning, or configurable rejection), whether traits get a typed schema, and the exact sessionId rotation rule are still open.

See also

On this page