feat(backend): track first organization onboarding in Bento

Merged
WC
3mo ago
+3441-59

Summary

  • mark future direct registrations that still have no active organization access with onboarding:awaiting_first_org and emit user:registered_without_org
  • atomically clear awaiting state and add permanent recovery suppression when active direct organization access appears, then emit user:joined_org
  • permanently suppress recovery for normalized email changes so a queued message cannot be sent to the wrong address
  • permanently suppress and unsubscribe deletion-scheduled users with abortable, ordered Bento requests while allowing Stripe, organization, and image cleanup to proceed independently
  • route organization-access changes through the existing pgmq trigger/consumer system and document the Bento workflow guard

Safety and rollout

  • Capgo's database remains the source of truth: registration and role-binding handlers reconcile deletion state after every terminal Bento tag/event, including ambiguous delivery failures, before propagating any retryable error.
  • Bento subscriber imports are asynchronous, so every terminal organization-access transition adds monotonic suppression in the same update that removes awaiting state; malformed or short provider acknowledgements fail closed and retry.
  • Default API-key provisioning is serialized with account deletion and rejects deletion-scheduled users inside the same database transaction.
  • Invited registrations and users who already have active direct organization access are not enrolled.
  • This is prospective only: there is no backfill and no existing subscriber is enrolled by this PR.
  • The database migration may deploy before the Cloudflare route. That ordering is safe for this prospective cohort because tag creation and the join handler activate together in the same worker deployment; role-binding messages from before that activation cannot strand a newly tagged subscriber.
  • This PR does not enable or edit a Bento workflow. Before enabling the non-transactional recovery email, its final pre-send branch must require onboarding:awaiting_first_org and reject both onboarding:first_org_recovery_suppressed and onboarding_disabled.

Verification

  • TZ=UTC bun test:unit — 1,323 passed
  • bun lint
  • bun lint:backend
  • bun typecheck:backend
  • bash scripts/check-supabase-migration-order.sh
  • clean bun run supabase:db:reset
  • bun scripts/supabase-worktree.ts test db — 816 passed
  • SQLFluff on the new migration and pgTAP test
  • bun scripts/supabase-worktree.ts db lint -s public --fail-on warning
  • production Cloudflare worker wrangler deploy --dry-run --env=prod