Why a state machine
Swappr onboarding collects four distinct pieces of information — a tenancy document, a current-home listing, photos plus a description, and matching preferences — before a user is allowed into the matching surface. The flow is deliberately server-driven rather than client-driven so that:- A user can drop off mid-flow (kill the app, switch device, swap a SIM) and resume from exactly the right step.
- The frontend cannot accidentally submit step N+1 before step N has been persisted.
- The audit trail (who completed which step, when) is unambiguous on the server.
The four steps
| Step | Endpoint | Persistence target | Output |
|---|---|---|---|
| 1 | POST /onboarding/step-1-tenancy | tenancy_verifications | currentStep = 2 |
| 2 | POST /onboarding/step-2-current-home | current_homes | currentStep = 3 |
| 3a | POST /onboarding/step-3-photos | current_homes.photos[] (append) | currentStep stays 3 |
| 3b | POST /onboarding/step-3-description | current_homes.description | currentStep = 4 (once both 3a and 3b done) |
| 4 | POST /onboarding/step-4-desired-home | user_preferences | onboardingStatus = COMPLETE, currentStep = null |
currentStep = 3 until both have been recorded.
The resume oracle
GET /onboarding/state is the resume oracle. The mobile client calls it:
- On app launch, before deciding which screen to render.
- After every reconnect from background.
- After any 5xx or
STATE_CONFLICTit can’t otherwise explain.
onboardingStatus, currentStep, and tenancyStatus. The client uses currentStep to navigate to the right step screen; once onboardingStatus === "COMPLETE" the client drops the user into the main app and never calls the onboarding step endpoints again.
tenancyStatus is reported alongside because it can flip independently (the admin queue approves or rejects the document after step 1 has been submitted). tenancyStatus === "APPROVED" is one of the four publish eligibility gates.
Step-out-of-sequence enforcement
Each step endpoint asserts thatusers.currentStep equals the expected value for that step before it will write. If it doesn’t, the request is rejected with 409 STATE_CONFLICT:
- The client tried to skip ahead. A buggy app calling step 3 while the user is still on step 2. The fix is for the client to call
GET /onboarding/stateand re-render. - The user already completed this step on another device. A user installs the app on a new phone and the local state thinks they are on step 2, but the server has already advanced them to step 3 from the original device. Again:
GET /onboarding/stateis the cure.
onboardingStatus === "COMPLETE", all five step endpoints return 409 STATE_CONFLICT permanently. Edits from that point go through /current-home/me/* and /preferences/me.
Diagram
Why requireOnboarded middleware gates downstream features
Several Phase 3+ surfaces — match candidates, conversation creation, post-share — are wrapped in a requireOnboarded middleware that returns 403 FORBIDDEN when onboardingStatus !== "COMPLETE". The reasoning:
- A user without a current-home listing cannot meaningfully be matched against anyone.
- A user whose tenancy document has not been uploaded cannot prove they have a tenancy to swap.
- Letting a half-onboarded user into chat creates abuse vectors.
See also
- Onboarding state endpoint
- Step 1 — tenancy
- Step 2 — current home
- Step 3 — photos / Step 3 — description
- Step 4 — desired home
- Publish listing — the downstream gate that consumes
tenancyStatus.