Identity Resolver
738 LOC — cross-device identity resolution with domainprint aliasing, parallel queries, and Upstash caching.
Resolution Strategy
Fast Path
Known user (userId exists, not anon_)
1 query: _get_user_identity_cached()
├── Cache hit: ~50ms
└── Cache miss: ~150ms
3 cases:
A) domainprint == primary → no_change
B) domainprint ∈ aliases → sync_to_primary
C) domainprint is NEW → merge + aliasParallel Path
Anonymous user — full search
3 queries in parallel (ThreadPoolExecutor): ├── _query_by_fingerprint (FP#hash) ├── _query_by_domainprint (DP#hash) └── _query_by_behavioral (BEH#hash) ~150ms total (vs ~450ms sequential) Priority: FP > DP > Behavioral > New
Decision Tree
resolve_identity(fingerprint, domainprint, behavioral_fp, user_id) │ ├── user_id exists && !anon_? │ │ YES → FAST PATH │ │ ├── get cached user (1 query) │ │ ├── user exists? │ │ │ NO → create_new_user() [confidence: 100] │ │ │ YES → check domainprint: │ │ │ ├── dp == primary → "existing_identity" [100] │ │ │ ├── dp ∈ aliases → "alias_detected" [100] │ │ │ └── dp is NEW → "new_domainprint_merged" [95] │ │ │ └── NO → PARALLEL PATH │ ├── FP match? → "identified_by_fingerprint" [90] │ ├── DP match? → "network_change" [95] │ ├── BEH match? → "identified_by_behavioral" [~85] │ └── No match → "new_anonymous_user" [60]
Domainprint Aliasing
A user's browser fingerprint can change (network change, VPN, update), but the domainprint — a first-party cookie unique to a domain — stays stable. When a new domainprint appears for a known user, it's merged as an alias of the primary.
User Profile (DynamoDB)
─────────────────────
PK: USER#user_2abc
SK: PROFILE
├── primaryDomainprint: "dp_A" ← master
├── domainprintAliases: [
│ {
│ domainprint: "dp_B",
│ isPrimary: false,
│ status: "merged",
│ mergedInto: "dp_A",
│ mergedAt: "2026-03-15T...",
│ sessionCount: 12,
│ mergeContext: {
│ country: "ES",
│ asn: "3352",
│ userAgent: "Chrome/125..."
│ }
│ }
│ ]
├── identities: [
│ { fingerprint: "fp_1", domainprint: "dp_A", confidence: 100 },
│ { fingerprint: "fp_2", domainprint: "dp_B", confidence: 90 }
│ ]
└── totalDevices: 2Reverse Indexes
DynamoDB doesn't support secondary lookups natively. We maintain 3 reverse indexes for O(1) lookups:
| Index | PK Pattern | Maps To | TTL |
|---|---|---|---|
| Fingerprint | FP#{hash} | userId + confidence (90) | 30 days |
| Domainprint | DP#{hash} | userId + isAlias flag | 365 days |
| Behavioral | BEH#{hash} | userId + similarity (85) | 30 days |
Confidence Scoring
User ID + DP
DP match / merge
FP match
New anonymous
Cache Strategy (Upstash Redis)
_get_user_identity_cached(user_id):
│
├── cache_get("user:{user_id}")
│ ├── HIT → parse JSON, validate
│ │ ├── Corruption check: primary ∈ aliases? → invalidate
│ │ └── Return cached user (~50ms)
│ │
│ └── MISS → _get_user_identity(user_id)
│ ├── DynamoDB GetItem (~150ms)
│ ├── cache_set(key, json, TTL=300s)
│ └── Return user
│
└── Cache invalidation:
└── On primaryDomainprint fix → cache_delete("user:{id}")Performance Optimizations
ThreadPoolExecutor global
Pool de 3 workers reutilizado entre invocaciones Lambda — ahorra ~15ms por request
Fire-and-forget timestamps
_update_identity_timestamp_async() — Thread daemon, no bloquea response (~100ms ahorro)
Identity eliminada de response
Los handlers no consumen identity completa — reducida payload en match responses (~80ms)