DocsGuides

Guides

Implement identity stitching and account lifecycle

6 min readReviewed July 2026

GraphJSON stores the identifiers you send. It does not automatically merge anonymous visitors, users, devices, sessions, or customer accounts.

That makes identity behavior explicit, but your application must choose:

  • what entity a metric counts
  • when two identifiers represent the same person
  • whether a link rewrites interpretation of earlier activity
  • which account owned an event at occurrence time
  • how merges, transfers, and deletion requests behave

Keep the application database or identity service authoritative.

Use separate identifiers#

Recommended event envelope:

{
  "event": "report_exported",
  "event_id": "evt_01J...",
  "occurred_at": 1785081600,
  "anonymous_id": "anon_c2f...",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "session_id": "ses_91",
  "schema_version": 1
}

Each identifier answers a different question:

Field Meaning
anonymous_id Pseudonymous installation or browser identifier
user_id Authenticated application person
account_id Customer or tenant context for this action
session_id Application-defined activity session
event_id Unique occurrence used for delivery and deduplication

Do not copy an email address into user_id. Use stable, opaque internal IDs.

Choose the counting grain#

Before writing SQL, finish this sentence:

This metric counts unique __________.

Possible answers:

  • accounts
  • authenticated users
  • anonymous visitors
  • sessions
  • devices
  • events

“Active users” is not a complete definition when one person can use several accounts or several devices.

Create anonymous IDs safely#

For public clients:

  1. generate a cryptographically random UUID
  2. store it only after the applicable consent decision
  3. keep it scoped to your application
  4. rotate or remove it according to privacy policy
  5. send through a first-party endpoint

Do not:

  • fingerprint the device
  • derive the value from an email
  • reuse an advertising identifier without a lawful, documented reason
  • let a browser choose another customer’s account_id

The first-party endpoint should accept the public anonymous ID but derive authenticated IDs from the server session when one exists.

When an anonymous session becomes authenticated, emit a link fact:

{
  "event": "identity_linked",
  "event_id": "evt_link_01J...",
  "occurred_at": 1785081600,
  "anonymous_id": "anon_c2f...",
  "user_id": "usr_42",
  "link_reason": "signed_in",
  "schema_version": 1
}

Then emit future product events with both identifiers while the anonymous ID remains available:

{
  "event": "dashboard_created",
  "anonymous_id": "anon_c2f...",
  "user_id": "usr_42",
  "account_id": "acct_7"
}

The link event records evidence. It does not mutate old GraphJSON rows.

Choose retrospective or event-time identity#

Two policies are valid for different questions.

Retrospective person view#

Treat pre-login anonymous activity as belonging to the later user.

Useful for:

  • acquisition-to-activation funnels
  • first-touch analysis
  • onboarding research

Trade-off: a link learned today changes how a historical query interprets earlier activity.

Event-time identity view#

Count an event under only the identity known when it occurred.

Useful for:

  • security
  • access audits
  • billing
  • contractual usage

Trade-off: a person’s journey stays split across anonymous and authenticated identifiers.

Name the chosen policy in the metric definition. Do not switch silently.

Resolve anonymous history in SQL#

A retrospective mapping:

WITH links AS (
  SELECT
    JSONExtractString(json, 'anonymous_id') AS anonymous_id,
    argMax(
      JSONExtractString(json, 'user_id'),
      timestamp
    ) AS user_id
  FROM product_events
  WHERE JSONExtractString(json, 'event') = 'identity_linked'
  GROUP BY anonymous_id
),
activity AS (
  SELECT
    timestamp,
    JSONExtractString(json, 'event') AS event,
    JSONExtractString(json, 'anonymous_id') AS anonymous_id,
    JSONExtractString(json, 'user_id') AS direct_user_id
  FROM product_events
)
SELECT
  activity.event,
  uniqExact(
    if(
      activity.direct_user_id != '',
      activity.direct_user_id,
      links.user_id
    )
  ) AS identified_people
FROM activity
LEFT JOIN links USING anonymous_id
GROUP BY activity.event
ORDER BY identified_people DESC

Decide what to do when both identifiers are empty. Do not turn an empty string into one enormous “user.”

Handle shared devices#

An anonymous ID can be used by more than one authenticated person on a shared browser.

Choose a rule:

  • link only the current anonymous session
  • rotate anonymous_id after sign-out
  • do not retrospectively stitch shared-device history
  • exclude ambiguous links from person-level metrics

Never assume one browser ID represents one human forever.

Handle multiple devices#

One user can link several anonymous IDs:

anon_phone_1 ─┐
anon_laptop_2 ├─ user_42
anon_tablet_3 ┘

Deduplicate by user_id after resolving links. Counting linked anonymous IDs would measure devices, not people.

Keep the many-to-one mapping in your authoritative identity service if it must support privacy requests or security review.

Put account context on every event#

A user who belongs to two customer accounts can perform the same event under different tenant contexts.

Derive account_id from the active authorized server session:

const analyticsEvent = {
  event: "report_exported",
  user_id: session.user.id,
  account_id: session.activeAccount.id,
  report_id: validatedReportId
};

Do not infer the active account later from the user’s current membership. The user may switch accounts, leave an account, or join another one.

Record membership changes#

Emit completed lifecycle facts:

{
  "event": "account_membership_changed",
  "membership_id": "mem_19",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "previous_role": "member",
  "new_role": "admin",
  "change_type": "role_changed",
  "occurred_at": 1785081600
}

Other change_type values might include:

invited
joined
left
removed
role_changed

Do not use analytics as the access-control decision. The application’s authorization system remains authoritative.

Query event-time membership#

For many product metrics, putting account_id directly on the product event is enough and safer than reconstructing membership.

Reconstruct membership only when the question is specifically about lifecycle state:

SELECT
  JSONExtractString(json, 'account_id') AS account_id,
  JSONExtractString(json, 'user_id') AS user_id,
  argMax(
    JSONExtractString(json, 'new_role'),
    timestamp
  ) AS latest_role,
  argMax(
    JSONExtractString(json, 'change_type'),
    timestamp
  ) AS latest_change
FROM account_events
WHERE JSONExtractString(json, 'event') =
  'account_membership_changed'
GROUP BY account_id, user_id

A latest-state query should not be used to relabel historical product events.

Handle account switching#

Record the active account on the outcome event. An optional active_account_changed event can help analyze navigation:

{
  "event": "active_account_changed",
  "user_id": "usr_42",
  "previous_account_id": "acct_7",
  "account_id": "acct_12",
  "session_id": "ses_91"
}

Do not treat a switch event as proof that the user had authorization. Log it only after the application has completed the authorized switch.

Model user merges#

Sometimes the application merges duplicate user records.

Record an immutable fact:

{
  "event": "user_identity_merged",
  "source_user_id": "usr_duplicate",
  "destination_user_id": "usr_canonical",
  "merge_id": "merge_81",
  "merge_reason": "verified_duplicate",
  "occurred_at": 1785081600
}

Choose:

  • historical reporting remains under original IDs
  • approved analytical queries resolve source to destination retrospectively
  • a controlled backfill creates a new normalized dataset

The second choice changes historical interpretation. The third changes stored data and must be reconciled. Record the metric policy.

Do not emit an identity merge based only on matching names or unverified email text.

Model account merges and transfers#

An account merge is not merely a user merge. Revenue, entitlements, and historical ownership may follow different rules.

Preserve:

  • source account
  • destination account
  • effective time
  • merge or transfer reason
  • authoritative transaction ID
  • whether historical metrics should be restated

Prefer event-time account IDs for auditability. If executive reporting needs a retrospective parent account, maintain a separate approved account hierarchy mapping and version it.

Handle sign-out#

After sign-out:

  • stop attaching user_id
  • clear server session state
  • choose whether to retain or rotate anonymous_id
  • never retain the previous account_id as an authorized default
  • start a new session according to the session policy

This prevents one person’s identity from leaking into the next user on a shared device.

Identity and embedded analytics#

For a personalized dashboard:

  1. authenticate the viewer
  2. resolve the authorized account on the server
  3. construct a fixed tenant filter
  4. request GraphJSON data or an embed URL server-side
  5. return only the authorized result

Never accept a browser-provided account_id without an authorization check. Identity stitching is an analytical concern; tenant authorization is a product security concern.

Privacy and deletion#

Opaque identifiers remain personal data when your application can resolve them.

Maintain a restricted identity map that can answer:

verified person
  → user IDs
  → anonymous IDs
  → account memberships
  → relevant collections

For a deletion request:

  1. verify the request in the system of record
  2. resolve all relevant identifiers
  3. stop new events
  4. pause replay sources
  5. follow the tested GraphJSON deletion procedure
  6. remove identity mappings according to policy
  7. verify that retries cannot reintroduce data

Do not paste an identity map into a support email.

Test matrix#

Test:

  • anonymous activity without consent
  • anonymous activity with consent
  • first sign-in
  • sign-out
  • second person on the same device
  • one person on several devices
  • account switching
  • membership removal
  • invited user who never joins
  • duplicate-user merge
  • account transfer
  • deletion request
  • delayed and out-of-order identity events

Verify both event payloads and the SQL metrics they support.

Production checklist#

  • Every metric names its counting grain.
  • IDs are stable, opaque, and application-owned.
  • Anonymous IDs follow consent and retention policy.
  • Identity links are explicit completed facts.
  • Retrospective and event-time policies are documented.
  • Active account_id is derived server-side.
  • Historical events keep occurrence-time tenant context.
  • Shared-device behavior is tested.
  • Merge behavior is approved and versioned.
  • Embeds enforce authorization independently.
  • Privacy requests can resolve all intended identifiers.

Continue with Users, accounts, and identity, Web analytics and attribution, and Production embedded analytics.

Need a hand?

Tell us what you’re building and we’ll point you in the right direction.

Contact support