Decouple identity_map from main app: username renames via event, not shared DB
Opened by stack72 · 4/9/2026· GitHub #211
Context
identity_map is a MongoDB collection in the telemetry service that maps usernames to their device identifiers (distinct_ids). It's maintained by two bounded contexts:
- Telemetry service — StatsConsumer creates/updates mappings when authenticated events arrive
- Main app —
chooseUsername()inlib/auth.tsdirectly modifies identity_map during username renames (pull distinct_ids from old entry, addToSet to new entry, delete old)
This is shared-database integration. The main app knows the identity_map schema, knows the merge logic, and breaks if the telemetry service changes its storage format. Two contexts, one collection, no single owner.
After #102 lands, the main app no longer reads identity_map (scores come from queue snapshots keyed by username). But it still writes to it on renames. This issue eliminates that last cross-context write.
Current rename flow (lib/auth.ts — chooseUsername())
1. Update BetterAuth user record (username field)
2. Directly modify telemetry DB:
a. identity_map: rename or merge old → new entry
b. user_scores: re-key old → new document
3. ReturnStep 2a is a bounded context violation — the main app reaches into telemetry's storage to fix up its internal index.
Step 2b goes away with #102 (user_scores is owned by the main app's UserScoreRepository, keyed by username — the main app re-keys its own data, which is fine).
Plan
1. Publish username_changed event
lib/auth.ts — after the BetterAuth username update succeeds, write a username_changed event to swamp_club_events (the same queue collection from #102, reverse direction: main app → telemetry service).
{
type: "username_changed",
oldUsername: string,
newUsername: string,
userId: string,
createdAt: Date,
status: "pending",
}Remove the direct identity_map and user_metrics modifications from chooseUsername(). Keep the user_scores re-keying (that's the main app's own collection).
2. Consume username_changed in telemetry service
services/telemetry/lib/consumers/identity-migration.ts (new) — registered consumer that handles username_changed events:
- identity_map: rename entry or merge old → new (same logic currently in
lib/auth.ts) - username_metrics: re-key old → new document (if it exists)
- user_metrics: no change needed (keyed by distinct_id, not username)
The telemetry service owns the migration logic for its own collections. The main app just says "username changed" and moves on.
3. Event queue direction
#102 establishes swamp_club_events as telemetry → main app. This issue needs main app → telemetry. Two options:
Option A: Shared bidirectional queue. Add a direction or target field to events. Each consumer filters to its own events. Simple, one collection, but muddies the contract.
Option B: Separate telemetry_commands collection. Main app writes, telemetry service polls. Clean separation. The telemetry service already has a watcher loop — add a second poll source or a dedicated watcher for this low-volume collection.
Recommend Option B — username renames are rare (single-digit per day at most). A dedicated collection keeps the high-volume telemetry event queue uncontaminated by low-volume command events. The telemetry watcher can poll it on a longer interval (30s).
4. Remove direct telemetry DB access from main app
lib/auth.ts — delete the identity_map and username_metrics migration code from chooseUsername(). The function becomes:
- Update BetterAuth username
- Re-key user_scores (own collection)
- Publish
username_changedevent totelemetry_commands
No more importing the telemetry database connection for renames.
5. Update tests
tests/auth/choose_username_fn_test.ts — update to verify event publication instead of direct identity_map modification. The test should assert a username_changed document appears in telemetry_commands, not that identity_map was updated.
New test for the telemetry consumer:
username_changedevent with existing mapping → entry re-keyedusername_changedwith mappings on both old and new → mergedusername_changedwith no existing mapping → no-op
Files
| Action | File | Part |
|---|---|---|
| MODIFY | lib/auth.ts |
1, 4 |
| CREATE | services/telemetry/lib/consumers/identity-migration.ts |
2 |
| MODIFY | services/telemetry/main.ts |
2 |
| MODIFY | tests/auth/choose_username_fn_test.ts |
5 |
| CREATE | tests/telemetry/identity-migration_test.ts |
5 |
Ordering
Deployable in one step — the consumer must exist before the old direct-write path is removed. If a phased rollout is preferred:
- Deploy telemetry consumer (processes events, no-ops if none exist)
- Deploy main app change (publishes events, stops direct writes)
Depends on
- #102 (establishes queue infrastructure and watcher patterns)
Verification
- Rename a user →
telemetry_commandsgets ausername_changedevent - Telemetry consumer processes it → identity_map updated correctly
lib/auth.tshas zero imports from telemetry DB- Leaderboard and stats still resolve renamed users correctly
deno task check+deno task testpass
Open
No activity in this phase yet.
Sign in to post a ripple.