Skip to content

Mirror writes DJ legal names into tubafrenzy's public DJ_HANDLE for handle-less DJs #2291

Description

@jakebromberg

Problem

mapShowToTubafrenzy in the legacy mirror computes the value written to tubafrenzy's public DJ_HANDLE field as:

// shared/legacy-mirror/src/http-mirror.ts:512 (pre-fix)
const djHandle = override && override.length > 0 ? override : dj.djName || dj.name;

The terminal dj.name is auth_user.name. That column is a hidden second copy of the DJ's legal name: dj-site's admin provisioning sends name: realName || username, the auth provision route stores it verbatim, and nothing ever maintains it afterwards (roster edits go straight to better-auth's admin.updateUser and touch real_name/dj_name only). The history is written up at shared/database/src/dj-name.ts:1-13 and :33-40.

So for every DJ who has not set a stage handle, starting a show writes their legal name into DJ_HANDLE.

This is the fifth dj_name-family writer. Commit 2a37bbc6 removed the auth_user.name fallback from what it described as "all 4 dj_name writers"; the mirror's outbound djHandle was not among them, and it has been reading name ever since.

Why it matters

Two callers reach it: apps/backend/middleware/legacy/flowsheet.mirror.ts:168 (live show-start path) and jobs/legacy-mirror-reconcile/orchestrate.ts:285 (reconcile sweep 1). Both select() the full auth_user row, so no query change is needed.

Fix

djHandle resolves as: per-show override -> resolveDjDisplayName(djName) (the canonical helper in @wxyc/database, which trims and treats the literal "Anonymous" as absent) -> auth_user.username -> ''. It never reads name, and it does not re-derive the trim/Anonymous logic locally.

username is the terminal fallback rather than name: it is the least-surprising non-PII stable identifier and it matches the auth_user.name := handle, else username policy chosen for the broader safeguards work. MirrorDJ gains username?: string | null.

The all-absent case is pinned to '' (mirroring the sibling showName ?? '') rather than left as emergent undefined on a field tubafrenzy treats as required. It is unreachable in practice — every provisioned user has a username, and anonymous users cannot own shows.

Deliberately not changed: http-mirror.ts:514 (djName: dj.realName || dj.name). That targets tubafrenzy's separate DJ_NAME legal-name field, where name is still the correct fallback until the auth_user.name backfill makes real_name reliably populated. It is not the leak, and folding it in here would change a field this issue is not about.

Also corrects the shows.legacy_dj_name docblock at shared/database/src/schema.ts:2497-2510, which described the outbound chain this change removes — a stale doctrine comment on exactly this column is how the last incident propagated.

Scope

This is Track 0 of plans/dj-name-pii-safeguards.md — the "stop the live writer" slice, sized to ship alone and immediately. The plan file itself lands in a sibling PR; the rest of it (PII field registry, COMMENT ON COLUMN migration, the databaseHooks.user derivation choke point, the auth_user.name backfill, the wxyc/restricted-real-name lint rule, and the sentinel wire spec) is out of scope here.

Acceptance criteria

  • mapShowToTubafrenzy's djHandle provably never reads name or realName, proven by a sentinel unit test across the override/handle/username/all-absent matrix.
  • The all-absent case yields ''.
  • http-mirror.ts:514 (DJ_NAME) is unchanged.
  • Both callers' unit fixtures carry username.
  • The shows.legacy_dj_name docblock no longer describes the removed chain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdeadline:tubafrenzy-turndownHard clock: must land before the 2026-08-31 tubafrenzy turndowntubafrenzyTouches WXYC/tubafrenzy (webhook, legacy mirror, flowsheet ETL, reconcile)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions