Skip to content

🎯 Decouple invitations from organizations (referral-ready platform invitations) #3808

Description

@PierreBrisorgueil

Epic β€” Decouple invitations from organizations (referral-ready)

Two invitation systems were colliding: the platform signup-gate (#3715, beta invite-only) and org membership email-invite. We fully decouple them.

Model: "invite a contact to the platform" (token + invitedBy + beta-gate eligibility) becomes a standalone optional invitations module depending only on auth. org becomes roster-of-existing-accounts only (add/roles/remove + join-requests). invitations βŠ₯ org β€” they communicate via userId. Referral (#5) is schema-ready (invitedBy + invitation.accepted event), credit-grant logic deferred.

Spec: infra/docs/superpowers/specs/2026-06-09-invitations-org-decouple-design.md
Plan: infra/docs/superpowers/plans/2026-06-09-invitations-org-decouple.md (audited on 4 axes + a Codex second-opinion pass folded in)

⚠️ Ordering constraint: Phase 1 (land #3715 byte-identical to the first consumer's deployed state) MUST merge before Phase 2 (module extraction), otherwise the next the first consumer /update-stack produces a 3-way conflict on auth.controller.js.

Locked decisions: no auto-verify of invite-created accounts Β· lowercase + case-insensitive unique index on users.email Β· cap TOCTOU overshoot accepted + documented Β· add-to-org β†’ PENDING with source:'owner_add' (E15 β€” never confused with a join-request) + persistent pending-list in Account + transient snackbar.

Phases (each = its own PR via /feature β†’ /verify β†’ /dev:verify-qa β†’ /dev:pull-request-finalize):

  • P1 β€” Land feat(auth): invite-only / capped signup gateΒ #3715 signup-gate on master (byte-identical) Β· Node
  • P2 β€” Extract invitations module + eligibility-hook inversion (E13 query token) Β· Node
  • P3 β€” Edge-case hardening (E2 two-phase claim, E3 CI email index, E6 no auto-verify, E8 soft-revoke, E9) Β· Node
  • P4 β€” Remove org email-invite (Node) + migration (org-owned) Β· Node
  • P5a β€” org.addMember(userId) + source discriminator + consent Β· Node
  • P5b β€” Org add-member UI + persistent pending-invitations list + snackbar Β· Vue
  • P6 β€” invitations Vue module + account/admin tabs + gap-fixes Β· Vue
  • P7 β€” Remove org email-invite (Vue) Β· Vue
  • P8a β€” Referral hook backend (referredBy schema + invitation.accepted) Β· Node
  • P8b β€” Referrals account view scaffold Β· Vue
  • P9 β€” Propagate downstream + run prod migrations (the first consumer) Β· downstream

Sub-issues are linked below and tracked by the Sub-issues progress bar.


πŸ›‘ Fable review corrections (2026-06-10, code-verified vs origin/master)

  • πŸ›‘ feat(auth): invite-only / capped signup gateΒ #3715 is ALREADY MERGED on origin/master (the spec/plan were authored from a local checkout 115 commits behind). f0e7a83c is an ancestor; all modules/auth/**/*invitation* + auth.signupCapacity.js + gate hunks are present; the first consumer_node master auth.controller.js is hash-identical. β†’ P1 (πŸ”§ Land #3715 signup-gate on master (byte-identical)Β #3809) collapses to a verify-no-op. The "Node backend not on master / front-ahead-of-back" premise is FALSE.
  • πŸ”΄ Casing bug across P5a: membership status is stored lowercase (PENDING:'pending'); any === 'PENDING' check (incl. the prescribed pre('validate') consent guard) is INERT β€” use the MEMBERSHIP_STATUSES.PENDING constant everywhere.
  • πŸ”΄ P2 must ship a /api/auth/invitations deprecation alias kept until P6 repoints the Vue store (else the live admin tab breaks for ~4 PRs).
  • Before executing ANY phase: git fetch && git checkout origin/master and re-derive file/line refs (local tree is stale; org signup-join flow was reworked β€” domain-match auto-join removed β†’ suggestedJoin).

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions