Lucerna.Identity (Lucerna v0.0.1-alpha.0)

The shared "who is the user" primitive — one immutable value, built once per request, that feeds both products: gates reads take it positionally, Lucerna.identify/1 sends it to People. Mirrors GatesIdentity / identity.current() in the JS SDKs and Lucerna::Identity in the Ruby gem.

PII rules: user_id is a pseudonymous app id ("u_42"), never an email. email and name are first-class fields — never traits — and are only ever transmitted by identify, which routes them to People's encrypted columns. Traits are plaintext targeting data.

Summary

Functions

Projection for gates evaluation: user id + traits, with the first-class email merged in as a trait fallback so the engine's email→domain derivation makes domain overrides work. Purely local — gates evaluation transmits nothing; only identify sends PII.

Builds a validated identity. user_id must be a non-empty string; traits are stringified strictly (see Lucerna.Traits.normalize!/1). Bad input is a programming error and raises ArgumentError.

Wire payload for POST /sdk/v1/people/identify. Note the rename: local user_id -> wire appUserId. email/name keys are omitted entirely when absent; traits are always present.

Coerces the accepted identity forms — %Lucerna.Identity{}, map, keyword list, nil — into an identity (or nil for anonymous). Strict: bad input is a programming error. Gates reads have their own lenient path and never raise.

Types

t()

@type t() :: %Lucerna.Identity{
  email: String.t() | nil,
  name: String.t() | nil,
  traits: Lucerna.Traits.traits(),
  user_id: String.t()
}

Functions

for_gates(identity)

@spec for_gates(t()) :: %{user_id: String.t(), traits: Lucerna.Traits.traits()}

Projection for gates evaluation: user id + traits, with the first-class email merged in as a trait fallback so the engine's email→domain derivation makes domain overrides work. Purely local — gates evaluation transmits nothing; only identify sends PII.

new!(fields)

@spec new!(keyword() | map()) :: t()

Builds a validated identity. user_id must be a non-empty string; traits are stringified strictly (see Lucerna.Traits.normalize!/1). Bad input is a programming error and raises ArgumentError.

Examples

iex> identity = Lucerna.Identity.new!(user_id: "u_42", email: "sofia@acme.com", traits: %{plan: "pro"})
iex> {identity.user_id, identity.email, identity.traits}
{"u_42", "sofia@acme.com", %{"plan" => "pro"}}

iex> Lucerna.Identity.new!(user_id: "")
** (ArgumentError) user_id must be a non-empty string (got "")

to_payload(identity)

@spec to_payload(t()) :: map()

Wire payload for POST /sdk/v1/people/identify. Note the rename: local user_id -> wire appUserId. email/name keys are omitted entirely when absent; traits are always present.

Examples

iex> Lucerna.Identity.new!(user_id: "u_42", email: "sofia@acme.com") |> Lucerna.Identity.to_payload()
%{"appUserId" => "u_42", "email" => "sofia@acme.com", "traits" => %{}}

iex> Lucerna.Identity.new!(user_id: "u_42") |> Lucerna.Identity.to_payload()
%{"appUserId" => "u_42", "traits" => %{}}

wrap(identity)

@spec wrap(t() | map() | keyword() | nil) :: t() | nil

Coerces the accepted identity forms — %Lucerna.Identity{}, map, keyword list, nil — into an identity (or nil for anonymous). Strict: bad input is a programming error. Gates reads have their own lenient path and never raise.