Data model
Identity and aliases
How Sankofa resolves anonymous traffic to known users — the distinct ID, the alias transport, and the server-side stitching that rewrites history.
Every event in Sankofa belongs to a distinct ID. Until a user signs in or otherwise tells you who they are, that ID is an opaque anonymous string the SDK generated on first launch. After they identify, you want everything they did before that moment to retroactively belong to the new known ID — so funnels, cohorts, and retention all stitch correctly.
That stitching is what identify and alias exist to make happen.
Three IDs, three jobs
| ID | Where it comes from | Lifetime |
|---|---|---|
anon_id | Generated by the SDK at first launch. UUIDv4. | Until reset() or storage cleared. |
distinct_id | Either the anon_id (until identified) or the user's stable ID after identify(...). | The current session's identity. |
alias_id | A previous identifier you want to attach to the current distinct_id. | Stored permanently in the alias table. |
The SDK always sends distinct_id on every event. It also sends anon_id separately so the server can stitch correctly across the identity transition.
The lifecycle of a real user
First launch — anonymous
SDK generates
anon_a3b9ff…. Every event usesdistinct_id = anon_a3b9ff…. The user browses, adds items to cart, firescart_updatedevents.The user signs up
Your app calls
Sankofa.identify("user_123"). The SDK:- Posts an alias from
anon_a3b9ff…→user_123to/api/v1/alias. - Switches the active
distinct_idtouser_123for all subsequent events. - Forwards
anon_idon the next decision handshake so cohort-targeted flags / configs are continuous across the boundary.
- Posts an alias from
Server-side stitching
The engine writes a row to the alias table. Within a few minutes, the analytics pipeline rewrites the historical anonymous events to attribute them to
user_123. After this, every chart and cohort attributes the pre-signup activity to the same user.The user signs in on another device
SDK on device 2 generates a fresh
anon_4ef2cc…, thenidentify("user_123")aliases it onto the same canonical ID. Both devices now produce events underuser_123.The user signs out
Your app calls
Sankofa.reset(). The SDK rotates to a newanon_id, clears the activedistinct_id, and starts a new session. Future events on this device are anonymous again untilidentifyruns.
The alias transport
identify calls alias for you. If you need to wire identity across systems where you can't call identify (e.g. a backfill from your data warehouse), use the alias transport directly.
curl -X POST https://api.sankofa.dev/api/v1/alias \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_..." \
-d '{
"alias_id": "anon_a3b9ff",
"distinct_id": "user_123"
}'The semantics: "anything that ever appeared as anon_a3b9ff is the same person as user_123." Call this once per identity transition; calling it many times is idempotent.
People profiles vs. aliases
alias, identify, and setPerson (a.k.a. peopleSet) all touch identity, but they do different things:
| Method | What it does |
|---|---|
alias(old_id, new_id) | Connects two identifiers as the same person. Server-side stitching uses this to rewrite history. |
identify(user_id, traits) | Calls alias under the hood, then switches the SDK's active distinct_id to user_id, and (if traits provided) also calls setPerson. |
setPerson(traits) | Updates the user's profile with email, name, plan, etc. Does not affect identity stitching — only the People view. |
Use identify for the common case. Use alias when you need to bridge identifiers without changing the SDK's session. Use setPerson to enrich the profile without touching identity.
Multi-device merge
Sankofa merges identities by design — the same distinct_id (user_123) on phone, web, and desktop produces one user in the People view. There's no extra config; just call identify(user_123) on each device after sign-in.
What we don't automatically merge: two anonymous IDs across devices. If a user browses anonymously on web, then anonymously again on mobile without ever signing in, those are two separate people from Sankofa's perspective. (That's by design — we don't fingerprint.)
Pitfalls and edge cases
If you accidentally identify("user_456") from a session that should belong to user_123, every event in that session is attributed to user_456. There's no automatic detection. Be careful with multi-tenant flows where a single browser session can switch identities.
To recover from a misattribution, you can re-alias on the server: post alias(user_456, user_123). That tells Sankofa they're the same person — which may or may not be what you want.
After reset(), the SDK has a fresh anon_id. If the user signs back in as the same user_id, calling identify aliases the fresh anonymous ID onto the existing user — exactly the behaviour you want.
Server SDKs require distinct_id on every event. If you don't have a known user (e.g. a webhook from a third party), pass a synthetic ID that's stable for the entity (user_unknown_<email_hash>). Don't make up random IDs per call — that creates one Sankofa person per event.
The alias graph is a tree, not a graph. If you alias(a → b) then alias(b → a), the first one wins; the engine ignores the second. You cannot un-alias once written.