DocsCore concepts

Core concepts

Users, accounts, and identity

4 min readReviewed July 2026

GraphJSON stores the identifiers you send inside each event. It does not create a hidden user profile, automatically merge anonymous activity, or decide whether your product should measure people, accounts, devices, or another entity.

That explicit model is useful, but it makes identifier design an instrumentation decision you should make before building funnels, retention, or personalized dashboards.

Choose the entity behind the question#

Most SaaS products need at least two identifiers:

{
  "event": "report_exported",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "report_id": "rpt_93",
  "format": "csv"
}

Use user_id for questions about an individual:

  • How many people activated?
  • Which users returned the following week?
  • Where do people leave onboarding?

Use account_id for questions about a customer organization:

  • How many accounts used exports this month?
  • Which accounts reached their plan limit?
  • What is activation by company?
  • Which customer should see a personalized dashboard?

Do not substitute one for the other. Ten active people at one company are ten active users but one active account.

Use durable internal identifiers#

Choose identifiers from your application database that:

  • are unique within the intended namespace
  • do not change when a person edits a profile
  • do not contain sensitive information
  • can be resolved by trusted server code
  • use one consistent type and casing

Prefer:

usr_01J2Y7F8V5
acct_01J2Y7H3Q9

Avoid email addresses, display names, company names, array indexes, or a value such as "anonymous" shared by many people. Mutable or reused identifiers silently combine activity that belongs to different entities.

Keep the prefix when different ID types could otherwise collide. An explicit usr_42 and acct_42 are easier to diagnose than two unrelated bare values of 42.

Represent anonymous activity deliberately#

Before sign-in, your application may know only a browser- or device-scoped identifier:

{
  "event": "pricing_viewed",
  "anonymous_id": "anon_8f62...",
  "plan": "pro"
}

After the person signs in, emit a linking event:

{
  "event": "identity_linked",
  "anonymous_id": "anon_8f62...",
  "user_id": "usr_42"
}

Then include user_id on subsequent authenticated events. Keep the link in your application database if it is needed for authorization or account recovery; analytics data should not be your identity system of record.

GraphJSON does not rewrite old anonymous events. When analysis must include pre-sign-in behavior, join the anonymous activity to identity_linked events in SQL. When it does not, begin the user journey at the first authenticated event and document that definition.

Important: Never use one constant anonymous ID for unidentified traffic. That turns every anonymous visitor into one synthetic person.

Model account membership as an event#

A user can belong to more than one account, and membership can change. Put the account in the event that happened rather than assuming a permanent user-to-account relationship:

{
  "event": "dashboard_viewed",
  "user_id": "usr_42",
  "account_id": "acct_7",
  "role": "admin"
}

For membership changes, log explicit facts:

{
  "event": "account_member_added",
  "account_id": "acct_7",
  "member_user_id": "usr_91",
  "invited_by_user_id": "usr_42",
  "role": "member"
}

This supports questions about the account context at the time of an action. If you need current membership or authorization, continue to read it from your application database.

Decide how sessions work#

GraphJSON does not assign sessions automatically. You can either send a session_id or derive sessions in SQL.

Send a session ID when your product already has a meaningful server session:

{
  "event": "search_completed",
  "user_id": "usr_42",
  "session_id": "ses_18b4...",
  "result_count": 27
}

Derive sessions when “a period of activity” is the useful definition. A common rule begins a new session after 30 minutes without an event, but the correct gap depends on the product. A code editor, delivery marketplace, and monthly reporting tool should not inherit the same session definition without thought.

Document the gap and time zone beside every session metric. Changing the rule changes the metric even when the underlying events stay the same.

Query users and accounts separately#

Count unique users:

SELECT uniqExact(JSONExtractString(json, 'user_id')) AS active_users
FROM product_events
WHERE JSONExtractString(json, 'event') = 'report_exported'
  AND toDateTime(timestamp) >= subtractDays(now(), 30)

Count unique accounts:

SELECT uniqExact(JSONExtractString(json, 'account_id')) AS active_accounts
FROM product_events
WHERE JSONExtractString(json, 'event') = 'report_exported'
  AND toDateTime(timestamp) >= subtractDays(now(), 30)

Filter empty identifiers when older or anonymous events share the same collection:

WITH JSONExtractString(json, 'account_id') AS account_id
SELECT uniqExact(account_id)
FROM product_events
WHERE account_id != ''

Protect tenant boundaries in embeds#

For customer-facing analytics, derive account_id from the authenticated server session. Do not accept an arbitrary account ID from the browser and attach the workspace API key.

The safe flow is:

signed-in browser
      ↓
your server resolves the allowed account
      ↓
your server creates a GraphJSON request with account_id = allowed account
      ↓
browser receives only the iframe URL or sanitized result

Use personalized dashboards for the complete implementation and authorization checklist.

Keep identity data minimal#

Events usually need an opaque identifier, not a complete profile. Avoid copying names, emails, addresses, authentication claims, or billing records into every event.

When analysis needs a stable attribute such as plan or region, decide whether it should represent:

  • the value when the event happened
  • the account’s current value

Include event-time properties in the event. Resolve current state from your application database or a deliberately maintained state-change event model. Mixing the two definitions produces charts that change meaning without changing names.

Identity checklist#

Before production instrumentation:

  • choose durable user_id and account_id formats
  • define which metrics count users and which count accounts
  • decide whether anonymous activity is linked or intentionally excluded
  • define session behavior if sessions matter
  • include the account context on multi-tenant events
  • keep authorization in the application, not the event store
  • avoid mutable and personally identifiable identifiers
  • test unique counts against a known set of users and accounts
  • record the identity definition in the tracking plan
Need a hand?

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

Contact support