Skip to content

Umbrella organization model: one customer org containing many brands #432

Description

@jrhizor

Part of #8.

Context

Organizations are currently 1:1 with brands — a shim kept by #419 and made explicit (but still 1:1) by #370's brands.organization_id FK. The billing plan (#345) attaches subscriptions to "the organization" via referenceId, and checkout-first onboarding (#346) needs an org to exist before any brand — both assume the org is the customer, not a per-brand shadow. Decided direction (see the discussion on #345): one umbrella customer org containing many brands, with membership and billing at the org level.

This must land before the first paying customer — re-anchoring live Stripe subscriptions to different orgs later is the migration to avoid. Migrating memberships now is cheap by comparison.

Scope

  • Provisioning: create the customer org at signup or checkout (named during onboarding); create-brand attaches the brand to the caller's existing org instead of minting an org per brand; org id/slug decoupled from brand names. Resolve Unify brand creation between API and UI (org provisioning; enable non-admin keys) #405's API/UI unification on these semantics ("attach to org"), not the old ones.
  • Access: replace requireOrgAccess(userId, brandId) call sites with a brand→org lookup through brands.organizationId (e.g. a requireBrandAccess helper). Nothing may rely on the brand.id === organization.id backfill artifact.
  • Session/UX: activeOrganizationId becomes the real org context; the brand switcher lists the active org's brands; the Team settings page from Self-serve signup, Google OAuth, and Resend transactional email #343 moves from per-brand settings to org level, and invitations grant org membership.
  • Data migration: collapse each existing cloud user's per-brand orgs into one umbrella org — brands keep their ids (URLs stay stable), member rows merge, in-flight invitations lapse naturally (48h expiry). Local/demo/whitelabel modes are untouched.

Out of scope

Acceptance

  • A new cloud user ends up with exactly one org; creating N brands adds N brands to that org and zero new orgs.
  • Team membership + invitations operate at the org level; every member sees all of the org's brands.
  • Existing cloud data migrated: one org per customer, all brand URLs unchanged, no orphaned member rows.
  • All access checks join through brands.organizationId; no new code depends on org/brand id equality.
  • Local/demo/whitelabel auth behavior unchanged (mode-compat smoke green).

Dependencies

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions