Server-driven KYC flows with a state machine
The first version of most verification flows is a wizard: step one, step two, step three, hard-coded in the client. It works until the second flow arrives — businesses need a registry document, supporting documents and a director's signature before the same ID and liveness steps everyone does. Now you have two wizards, and they start to disagree.
The server owns the plan
On Kwikpik's verification platform the client asks one question: what should I show now? The server answers from an explicit state machine.
type StepKind =
| "consent"
| "business_document"
| "supporting_documents"
| "signature"
| "document"
| "liveness"
| "address_proof";
function buildPlan(ctx: VerificationContext): Step[] {
const personal = [consent, document, liveness, addressProof];
if (!ctx.isBusiness) return personal;
return [consent, businessDocument, supportingDocuments, signature, ...personal.slice(1)];
}buildPlan() is the only place the flows branch. Below the signature step, the business plan reuses the same step objects as the personal one. Fix a bug in liveness and both flows get it — they can't drift because there's nothing to drift.
Steps return results, not navigation
A step handler validates its submission and returns a result: passed, failed (with whether an attempt was used), or needs review. It never says "go to step X". The machine decides.
That distinction matters for the boring edge cases:
- Exhausted attempts move the verification to
in_reviewinstead of a dead end. - A retryable failure (the provider was down) doesn't burn an attempt.
needsReviewis sticky — one step flagging it carries through to the final status.
Single-use tickets and optimistic concurrency
Captures upload against a signed, single-use ticket issued for that step. Replay a request and the ticket is spent. Submit from two tabs and the version check rejects the stale one. The UI can be as optimistic as it likes because the server is pessimistic.
No review step
We deleted the "review your details" screen at the end. Address proof is the last real check, and the widget shows the outcome directly. Asking people to confirm work they already submitted was friction pretending to be care.
If your client contains if (isBusiness) in more than one place, the plan has leaked out of the server.