fix(onboarding): atomic DB append for step completion (deferred bug L3)

markOnboardingStepDone read space_members.onboarding_progress, appended the step
id in JS, and wrote the whole jsonb object back — a lost-update race where two
near-simultaneous completions (double-submit, or two steps ticked quickly) both
read the same base array and the second write clobbered the first, silently
dropping a step.

Replace it with mark_onboarding_step_done (migration 055): a single atomic
UPDATE that dedup-appends the step id into onboarding_progress.completed_step_ids
under the row lock — no update can be lost (READ COMMITTED re-evaluates the jsonb
expression against the latest tuple via EvalPlanQual), other keys are preserved,
and re-appending an existing id is a no-op. Ownership-guarded by auth.uid() so a
caller can only complete steps on their own membership row; the no-JWT service
path is trusted and scoped by p_member_id (mirrors class_signup_tx et al.).

integration/onboarding-progress.test.ts covers dedup/idempotency, key
preservation, the auth.uid ownership gate, and the point of the fix: 12
concurrent completions all survive.

Migration 055 folded into schema.sql; types/database.ts regenerated.
typecheck 0 · lint 0 errors · 645 unit · 58 integration · build ok
This commit is contained in:
Moheeb Zara
2026-08-04 22:36:12 -07:00
parent b2b463491a
commit 4aa6646499
5 changed files with 221 additions and 14 deletions
+4
View File
@@ -3462,6 +3462,10 @@ export type Database = {
}
expire_proposals: { Args: never; Returns: number }
get_user_space_ids: { Args: { uid: string }; Returns: string[] }
mark_onboarding_step_done: {
Args: { p_member_id: string; p_step_id: string }
Returns: Json
}
members_with_permission: {
Args: { perm: string; sid: string }
Returns: {