Context
We currently have four columns expressing different facets of "domain" with overlapping but not identical semantics:
| Column |
Set by |
Read by |
| `organization_domains.is_primary` + row |
WorkOS webhook (auto-promote on first verified) + admin Set Primary |
auto-membership inference, seat-cap |
| `organizations.email_domain` |
WorkOS webhook + admin Set Primary |
`find_paying_org_for_domain`, brand hierarchy joins |
| `member_profiles.primary_brand_domain` |
brand-claim verify flow + (after PR #4157) WorkOS auto-populate |
publish-agent gate, brand registry resolve |
| `brands.domain` (+ `workos_organization_id`) |
brand-claim verify flow + WorkOS sync (`markBrandDomainVerified`) |
public brand catalog |
Each has its own write path. Drift is real — see Media.net escalation #321 where members verified their WorkOS domain but couldn't publish agents because `member_profiles.primary_brand_domain` lived in a separate world.
Options to consider
(a) Keep both "primary" columns, single canonical write path. Org-membership-primary and brand-identity-primary are genuinely different concerns (one drives auto-link inference, one drives brand identity). Keep both columns but eliminate the multi-write surface — when a user takes any "set primary domain" action, all four sites update coherently.
(b) Collapse `member_profiles.primary_brand_domain` into a derived view. Replace the column with `SELECT domain FROM organization_domains WHERE workos_organization_id = $1 AND is_brand_primary = true` (new flag). Eliminates the drift surface entirely but is a more invasive migration.
(c) Audit consumer-by-consumer. Build a usage matrix of every code path that reads each of the four columns; collapse duplicated reads behind a single resolver function. Doesn't change the schema but reduces blast radius for future changes.
Why this matters
- Members can't reason about which "primary" the UI is showing them.
- Operators can't reason about which write path to use to fix drift.
- We have a class of bugs (Media.net) that only exist because the two "primary" columns drift independently.
Driver
Media.net escalation #321. The full thread / staging plan: Stage 3 of the cleanup discussed there. PR #4157 is Stage 1 (auto-populate from WorkOS), #4158 is Stage 2 (member self-service UI).
Decision needed
Which option (or hybrid). Open question for design — not picking one yet.
Context
We currently have four columns expressing different facets of "domain" with overlapping but not identical semantics:
Each has its own write path. Drift is real — see Media.net escalation #321 where members verified their WorkOS domain but couldn't publish agents because `member_profiles.primary_brand_domain` lived in a separate world.
Options to consider
(a) Keep both "primary" columns, single canonical write path. Org-membership-primary and brand-identity-primary are genuinely different concerns (one drives auto-link inference, one drives brand identity). Keep both columns but eliminate the multi-write surface — when a user takes any "set primary domain" action, all four sites update coherently.
(b) Collapse `member_profiles.primary_brand_domain` into a derived view. Replace the column with `SELECT domain FROM organization_domains WHERE workos_organization_id = $1 AND is_brand_primary = true` (new flag). Eliminates the drift surface entirely but is a more invasive migration.
(c) Audit consumer-by-consumer. Build a usage matrix of every code path that reads each of the four columns; collapse duplicated reads behind a single resolver function. Doesn't change the schema but reduces blast radius for future changes.
Why this matters
Driver
Media.net escalation #321. The full thread / staging plan: Stage 3 of the cleanup discussed there. PR #4157 is Stage 1 (auto-populate from WorkOS), #4158 is Stage 2 (member self-service UI).
Decision needed
Which option (or hybrid). Open question for design — not picking one yet.