Design: Identity Mapping Schema and Token Convention

Date: 2026-04-02 Author: Pranav Narahari Ticket: EDE3-5 Status: Draft — pending engineer review before implementation (EDE3-7)


1. Purpose

Before any Slack message, Gmail thread, or Drive activity reaches the LLM or the COO’s UI, every real identity must be replaced with a stable, opaque token. This document defines:

  • The Firestore schema for the mapping table
  • The token naming convention (grounded in Eden’s actual org structure)
  • Edge case rules for non-person accounts
  • Storage location decision and rationale
  • Update policy for new hires, departures, and role changes

2. Background: Eden’s Org Structure

Sourced from Admin Directory API (2026-04-02, 181 total accounts). Eden uses two domains as one org:

DomainUsersPrimary use
@tryeden.com78Legacy / original domain; Executives, Operations, US-based staff
@eden.health103Newer domain; Pharmacy, BPO, Data/Eng, Marketing

Both domains must be treated as a single identity namespace. A person on @tryeden.com and a person on @eden.health are in the same org but are distinct identities unless confirmed otherwise.

Org Units (source of truth for token prefix)

Org Unit PathShort labelToken prefix
/ExecutivesExecExec
/Internal Operations/Data and DevelopmentEngineeringEng
/Internal Operations/Accounting and FinanceFinanceFinance
/Internal Operations/HR and RecruitingHRHR
/Internal Operations (catch-all)OperationsOps
/MarketingMarketingMktg
/Member Experience (non-BPO)Member ExperienceMX
/Member Experience/BPOBPOBPO
/PharmMedOpsPharmacy/Med OpsPharmOps
/PharmacyPharmacyPharmacy
/IT and SecurityITIT
/Team IndiaEngineering (India)Eng
/Legacy OperationsLegacy OpsLegacy
No dept / unassignedUnknownUnknown

Special account types (non-person):

TypeToken prefix
Shared / functional mailboxesSharedMailbox
Service accounts / integrationsServiceAcct
Distribution listsDL
External contacts (not in directory)External

3. Mapping Schema

Firestore Collection: identity_map

One document per identity. Document ID = URL-safe base64 of the primary email (e.g. ZGFuaWVsQHRyeWVkZW4uY29t for daniel@tryeden.com). Using a derived ID prevents duplicate documents for the same person and enables O(1) lookup without a query.

identity_map/{doc_id}

Document Fields

FieldTypeDescription
tokenstringStable anonymized token, e.g. Exec_1. Never changes.
primary_emailstringCanonical email address used as the primary key
alt_emailsstring[]Other known emails for the same person (e.g. both domains)
slack_user_idstring | nullSlack user ID (e.g. U05TX3GBC1F). Null if no Slack account.
slack_display_namestring | nullSlack display name at time of seeding (e.g. Daniel Dietz)
full_namestring | nullDisplay name from Google directory
org_unitstringGoogle Workspace OU path at time of seeding (e.g. /Executives)
token_prefixstringThe prefix component of the token (e.g. Exec)
token_counternumberThe numeric component of the token (e.g. 1)
account_typestringperson | shared_mailbox | service_account | distribution_list | external
statusstringactive | suspended | departed
created_attimestampWhen this mapping was first created
updated_attimestampLast metadata update (status changes, name changes). Token never changes.
notesstring | nullOptional: reason for edge case handling, e.g. "functional inbox, not a person"

Example Documents

// daniel@tryeden.com → Exec_1
{
  "token": "Exec_1",
  "primary_email": "daniel@tryeden.com",
  "alt_emails": [],
  "slack_user_id": "U05TX3GBC1F",
  "slack_display_name": "Daniel Dietz",
  "full_name": "Daniel Dietz",
  "org_unit": "/Executives",
  "token_prefix": "Exec",
  "token_counter": 1,
  "account_type": "person",
  "status": "active",
  "created_at": "2026-04-02T00:00:00Z",
  "updated_at": "2026-04-02T00:00:00Z",
  "notes": null
}
 
// care@tryeden.com → SharedMailbox_1
{
  "token": "SharedMailbox_1",
  "primary_email": "care@tryeden.com",
  "alt_emails": [],
  "slack_user_id": null,
  "slack_display_name": null,
  "full_name": null,
  "org_unit": "/Member Experience/Operational Functions",
  "token_prefix": "SharedMailbox",
  "token_counter": 1,
  "account_type": "shared_mailbox",
  "status": "active",
  "created_at": "2026-04-02T00:00:00Z",
  "updated_at": "2026-04-02T00:00:00Z",
  "notes": "Functional member experience inbox"
}

Lookup Indexes (Firestore composite indexes required)

IndexUse case
primary_emailRedact emails in Gmail/Drive/Calendar output
alt_emails (array-contains)Handle secondary domain emails
slack_user_idRedact Slack <@USERID> mentions
slack_display_nameRedact hydrated names in MCP search results
tokenReverse lookup for debugging (admin only, never exposed to COO)

4. Token Naming Convention

Format

{Prefix}_{Counter}
  • Prefix — short, role-based label derived from the Google Workspace OU (see table in §2)
  • Counter — monotonically increasing integer starting at 1, assigned in alphabetical order of primary_email within each prefix group at seeding time. Never reassigned, never reused.

Rules

  1. Tokens are permanent. Same person → same token forever, even if they change departments, change names, or leave.
  2. Counter is based on seeding order, not seniority or any meaningful attribute. This prevents inferring rank from the token.
  3. Prefix is based on OU at seeding time. If someone moves departments after seeding, their token prefix stays the same. Only new hires after that date get the new-department prefix.
  4. No collisions are possible because each prefix has its own counter namespace (Exec_1 and Eng_1 are different people).

Token assignments for known Executives (seeded from directory)

EmailNameToken
adam@tryeden.comAdam McBride (CEO)Exec_1
brad@eden.healthBradExec_2
daniel@tryeden.comDaniel Dietz (COO)Exec_3
jared@eden.healthJaredExec_4
jonah@tryeden.comJonahExec_5
josh@tryeden.comJoshExec_6
jowenne.duymayan@eden.healthJowenne DuymayanExec_7
mitesh@eden.healthMiteshExec_8
rebecca@tryeden.comRebeccaExec_9

Note: The COO (daniel@tryeden.com) will see Exec_3 in all redacted outputs. The full mapping is stored only in Firestore within Eden’s GCP project; the COO’s UI never surfaces it.


5. Edge Case Rules

ScenarioRuleToken prefix
Functional/shared mailboxes (care@, eden@, data@, disputes@, growth@, team@, etc.)Detected by: no fullName in directory, OU = /Member Experience/Operational Functions or no dept, or email is a common-noun word. Assign SharedMailbox_N.SharedMailbox
Service accounts / integrations (zapier.integration@, superadmin@, sebastian_villa@, emergency.admin@, user.archives@, devops1@)Detected by: OU = /IT and Security/2FA Bypass or /Legacy Operations with no person-name email, or contains integration/admin/bot in local part.ServiceAcct
Distribution lists (marketing_internal@, training.cohort@, collabs@, partners@, etc.)Detected by: plural-noun or compound-noun email with no directory person record.DL
Suspended users (cutter@, logan@, megan@, vanessa@, murali.prakash@)Seed at mapping time with status: "departed". Token is still assigned and stable — it may appear in historical data.Same prefix as their OU
Users with no OU / no dept (crislyn@, daelene@, denmark@, etc.)Assign Unknown_N. Revisit on next identity map refresh if OU is populated.Unknown
External contacts (people who appear in email threads / Slack DMs but are not in the directory)Detected by: email domain not in {tryeden.com, eden.health}. Assign External_N at runtime when first encountered by the PII redaction layer.External
Dual-domain same-person (e.g. adam.poma@eden.health and a hypothetical adam@tryeden.com for same person)Cross-reference by first name + last name match across domains. If confirmed same person, link via alt_emails. Use the @eden.health address as primary_email for newer accounts; @tryeden.com for legacy.Single token, two lookup entries
Bot / test accounts (test.user@eden.health, pharmacyintern@eden.health)OU = /IT and Security/Testing or obviously named. Assign ServiceAcct_N.ServiceAcct

6. Storage Decision: Firestore

Chosen: Google Cloud Firestore (in Eden’s GCP project, same project as the Command Center)

Rationale

RequirementFirestoreSecret ManagerEncrypted JSON in GCS
Indexed lookup by email in <10ms✗ (not a DB)✗ (full file load)
Indexed lookup by Slack user ID
Array-contains query for alt_emails
BAA compliance (stays in Eden GCP)
No extra GCP service needed✗ (Firestore is separate)
Append new entries at runtimeAwkwardFull file rewrite
Audit log of changes✓ (Firestore audit logs)Manual

Every Slack message, email, or Drive item that passes through the PII redaction layer triggers at least one identity lookup. At M1 scale (~50 messages/session), even GCS would be fast enough with caching — but Firestore’s indexing and atomic writes make the external-contact append pattern (§5) safe without locking.

Bootstrap: Seed Firestore from the Admin Directory API on first deploy. Script lives at src/scripts/seed-identity-map.ts.

Backup: Export the Firestore collection to GCS weekly. The export is also the source of truth for the alt_emails cross-domain mapping (maintained manually by the Brainforge eng team, not auto-derived).


7. Update Policy

New hires

When a new user appears in the Admin Directory API (detected by the weekly refresh script), create a new document with the next available counter for their OU prefix. The counter always increments — never fills gaps from departed users.

Departures

Set status: "departed" and updated_at to departure date. Do not delete the document. Historical data (emails, Slack messages) may reference their token. The token remains valid for redaction of past data.

Role / department changes

Update org_unit and updated_at. Do not change the token. The prefix reflects the original OU at seeding time. This is intentional: it prevents token instability and avoids re-processing historical data.

Name changes

Update full_name, slack_display_name, and updated_at. Add the old name to a former_names: string[] field (add this field when first needed). The PII redaction layer must check both current and former names.

New external contacts

When the PII redaction layer encounters an email domain not in {tryeden.com, eden.health}, it calls a createExternalIdentity() helper that:

  1. Checks if the email already exists in identity_map
  2. If not, creates a new document with token: "External_N" (N = next available external counter, read from a counters/external document in Firestore)
  3. Returns the token immediately for use in the current redaction pass

Slack display name drift

Slack display names change independently of Google directory names. The weekly refresh script re-fetches Slack user profiles and updates slack_display_name in Firestore. The old name is kept in former_names so historical MCP search results can still be redacted.


8. Counter Management

A separate Firestore document tracks the next available counter per prefix:

counters/{prefix}  →  { next: number }

Example: counters/Exec → { next: 10 } means the next Exec token will be Exec_10.

Increments are done as Firestore atomic transactions to prevent race conditions during concurrent seeding.


9. What This Design Does NOT Cover

  • Implementing the lookup function — EDE3-7
  • Building the PII redaction middleware — EDE3-8
  • The seeding script — part of EDE3-7 implementation
  • Key rotation or token re-mapping — explicitly out of scope; tokens are permanent by design
  • Exposing the reverse mapping to the COO — never; the mapping is admin-only

10. Review Checklist

  • Schema reviewed by AI tech lead (Sam)
  • Token prefix list confirmed against Eden org chart with CSO
  • Dual-domain cross-reference rules reviewed (edge case §5)
  • Firestore project location confirmed (Eden GCP project, not Brainforge)
  • BAA coverage for Firestore confirmed with Eden legal / Adam