Skip to content

feat(registry): expose membership tier on public operator lookups#4513

Merged
bokelley merged 3 commits into
mainfrom
EmmaLouise2018/expose-membership-tier
May 14, 2026
Merged

feat(registry): expose membership tier on public operator lookups#4513
bokelley merged 3 commits into
mainfrom
EmmaLouise2018/expose-membership-tier

Conversation

@EmmaLouise2018
Copy link
Copy Markdown
Contributor

@EmmaLouise2018 EmmaLouise2018 commented May 14, 2026

Summary

/api/registry/operator?domain=X now surfaces the queried member's AAO public-card info when the profile owner has opted their member card into public visibility (is_public=true). The member object grows three optional fields:

  • is_founding_member — boolean, present whenever the profile is public (true or false; absent for private profiles)
  • membership_tier — raw enum (e.g. individual_professional, company_leader) for programmatic gating, present only when the org also has a resolvable tier
  • membership_tier_label — human-readable label (e.g. Professional, Partner, Leader) matching the AAO pricing page; presence mirrors membership_tier

Private profiles still return only { slug, display_name }. Tier and Founding Member reflect billing/cohort state, so we follow the existing profile-card visibility toggle (is_public) rather than introducing a second one. Fields are absent (not null) when not applicable, so existing consumers see no shape change.

Also fixes the company_icl label in tierLabel() from Member to Partner so it matches the public pricing page and the dashboard (which already used Partner). Founding Member is orthogonal to tier — founding orgs typically display both badges (e.g. Scope3 shows Partner + Founding Member).

Also in this PR

Migration 477 hot-fix. 477_broadcast_delivery_criteria.sql called _append_criterion() without redefining it. The helper was created and dropped in migration 407, so later migrations using it must redefine it inline. 477 has been failing CI on every PR for ~2 days; bundling the fix here unblocks this branch and everything else in flight. Same create/use/drop structure as 407; no criterion text or IDs change.

Visibility rules

Profile state slug / display_name is_founding_member membership_tier / _label
is_public=true AND org has a resolvable tier
is_public=true BUT no resolvable tier absent
Private profile (is_public=false) absent absent
No profile for domain member: null

Test plan

  • npm run typecheck clean
  • npm run build clean
  • tests/unit/operator-publisher-lookup.test.ts — schema validation for all four visibility regions (public+tier, public-no-tier, private, no-profile)
  • tests/unit/membership-tiers.test.ts — updated company_icl → Partner assertion
  • OpenAPI spec regenerated (static/openapi/registry.yaml)
  • Manual: hit /api/registry/operator?domain=<public-member-domain> post-deploy, confirm tier fields land in member
  • Manual: hit same endpoint for a private profile, confirm tier fields absent

Notes on the OpenAPI diff

Most of the yaml changes will disappear after #4515 (the separate catchup PR) lands — that one absorbs the unrelated drift (Publisher.hosting.*, MemberAgentTypeInput split, new endpoints, etc.) so this PR's diff shrinks to just the three new member.* fields plus the operator-endpoint description block. Rebase order: wait for #4515, then rebase this branch and the yaml diff cleans up.

@bokelley
Copy link
Copy Markdown
Contributor

Reviewed — implementation is solid, ship-ready after one mechanical step.

What's good

  • Auth gating (if (profile?.is_public && profile.workos_organization_id)) is correct; tier resolution reuses resolveMembershipTier + tierLabel, single source of truth.
  • The company_icl → Partner rename aligns the server with what dashboard-organization.html and admin-account-detail.html already display.
  • Migration 477 fix mirrors 407's create/use/drop pattern verbatim. Smoke test green.
  • Schema tests cover all three visibility regions (public+tier, public-no-tier, private).

One mechanical step before merge
The yaml diff in this PR bundles ~340 lines of unrelated drift catchup (Publisher.hosting.*, MemberAgentTypeInput split, verdict_source, tools_count/tools[], new /refresh, /monitoring/requeue, /manager-revalidation-request endpoints, tracks_silent removal). None of that is yours — static/openapi/registry.yaml had drifted behind the Zod source because npm run test:openapi isn't wired into build-check.yml (only the umbrella npm run test runs it).

I opened #4515 to land that catchup as a clean no-op-against-code PR. After it merges, please rebase this branch — the yaml diff here will shrink to just the three new member.* fields and the operator endpoint description block, making the actual change obvious to future readers.

(I also updated the PR body to call out the three fields and the bundled 477 fix — original text said "two".)

Optional follow-up (non-blocking)
A handler-level test that mocks memberDb.getProfileByDomain + orgDb.getOrganization and asserts response shape for is_public=true vs false would harden against future regressions. The current schema-only tests confirm the shape parses but don't exercise the route logic that decides whether to emit those fields. Fine to land without — visibility is well-isolated.

Nice work. 🚢

EmmaLouise2018 and others added 3 commits May 14, 2026 10:00
/api/registry/operator?domain=X now surfaces the queried member's AAO
membership tier when the profile owner has opted their member card into
public visibility (is_public=true). The member object grows two optional
fields: membership_tier (raw enum, e.g. company_leader) and
membership_tier_label (human-readable, e.g. Leader).

Private profiles still return only { slug, display_name } — tier reflects
billing state, so we follow the existing profile-card visibility toggle
rather than introducing a second one. The fields are absent (not null)
for private profiles and for orgs without a resolvable tier, so existing
consumers see no shape change.
Public operator lookup now exposes is_founding_member alongside the
membership tier when the profile is public. Founding Member is orthogonal
to tier — founding orgs like Scope3 display both (Partner + Founding
Member) on their member card, so a single tier field doesn't capture the
intent.

Also fixes the company_icl label in services/membership-tiers.ts from
"Member" to "Partner" so it matches the AAO pricing page and the
dashboard's local tier map (which already used "Partner"). The label was
internal-only previously; surfacing it on the operator endpoint forced
the naming inconsistency to surface.

Schema, OpenAPI spec, and unit tests updated. Private profiles still
return only { slug, display_name } — no shape change for existing
consumers.
Re-regen captures Emma's three new member.* fields plus a small post-#4515
drift sweep that landed today (verification_mode/verified query params on
agent listing; 400 response shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley force-pushed the EmmaLouise2018/expose-membership-tier branch from 31ec08b to 995f9cb Compare May 14, 2026 14:10
@bokelley bokelley merged commit 9aac785 into main May 14, 2026
13 checks passed
@bokelley bokelley deleted the EmmaLouise2018/expose-membership-tier branch May 14, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants