Build a sign-up with email verification
By the end of this recipe you'll have a sign-up flow that takes the user from a 3-field form through email verification to an active account — with real password rules, a terms checkbox you can hold up in court, and a verification poller that doesn't busy-wait the server.
Overview
A three-stage flow: form → verify → active. One reducer, one component, one polling effect that quietly waits for the user to click the email link.
Sign-up is the second-most-leveraged surface in the app. A confused user here doesn't bounce to a competitor — they bounce to never. The Huchu pattern keeps the form to three real fields (email, password, accept terms), uses the password strength meter from the forgot-password recipe so the rules feel learned rather than imposed, and never marks an account "active" until the user has proven the inbox is theirs.
The verify stage runs a 4-second poller against GET /api/auth/me. When the API flips emailVerified to true — because the user clicked the link in another tab or on their phone — the state machine advances to 'active' on its own. No reload needed, no "Refresh and check again" button. The variation at the bottom shows how to chain a 2FA-enrolment stage straight after verification, for workspaces that require it.
Skip this recipe if your app uses invite-only sign-up — start the user on the invitation link, set them straight to "create password", and skip the verification entirely (the invite link already proved the inbox).
What you'll build
Three screens. The verify screen polls the server so the user doesn't have to refresh.
Create your account
30 days free. No card required.
Verify your email
We sent a link to tendai@mukamba.co. Open it on any device — this page will know.
Checking… · Resend in 0:42
Account active
Welcome, Tendai. We'll set up your workspace.
You can see this live in
Recipe-only for now — no portal demo wires this sign-up flow yet. Coming soon when a self-service onboarding demo lands.
Required pieces
Everything this recipe pulls from @corelithzw/react. Click any to see its reference page.
@corelithzw/react exports used here: Stack, Form, Field, Input, Button, Alert, Checkbox, Meter, AuthShell. The score() helper is identical to Forgot password — extract it to a shared module once you have two recipes that use it.
Step-by-step build
Model the three stages and the verification flag
Same reducer shape as the 2FA and forgot-password recipes — string union for the stage, one error slot, dispatch actions for forward and back. The stages are 'form' → 'verify' → 'active'. We also carry email across the boundary so the verify screen can show it.
FAIL keeps stage, sets error.
BACK returns to form so the user can edit the email.
Switch to the Code tab to see the snippet.
Render the sign-up form with real password rules
Three fields, one strength meter, one required checkbox, one optional. The terms checkbox is the only thing that gates submit alongside the meter — block on the legal click, never on the marketing one. Use the same score() helper as the forgot-password recipe so the strength bar feels consistent across the cluster.
Create your account
30 days free. No card required.
Build the verify screen — and poll instead of asking the user to refresh
The verify screen runs a setInterval that pings /api/auth/me every 4 seconds while the tab is visible. When the response flips emailVerified to true, dispatch VERIFIED and the machine advances. Pause polling on tab-hidden — phones throttle aggressively and a poll storm on resume wastes everyone's battery.
Verify your email
We sent a link to tendai@mukamba.co. Open it on any device — this page will know.
Land on "account active" and hand off to onboarding
The active stage is a confirmation, not the destination. Use the same tick + status-live pattern as the 2FA recipe. Don't dump the user into the dashboard cold — pass them to a one-screen onboarding wizard (workspace name, vertical, time zone) before the real app opens.
Account active
Welcome, tendai@mukamba.co. Let's set up your workspace.
Final composition
The whole SignUpEmailVerify component. Reducer, three stages, strength meter, terms gate, visibility-aware poller, resend cooldown.
Create your account
30 days free. No card required.
Variations
Three forks. Each is a small diff from the final composition — the three-stage spine stays intact.
+ optional 2FA enrolment
The workspace requires 2FA. Slot an enrolment stage between 'verify' and 'active' so the user sets up their authenticator before the dashboard opens.
type Stage = 'form' | 'verify'
| 'enrol-2fa' | 'active';
case 'VERIFIED':
return { ...s, stage: 'enrol-2fa' };
case 'TFA_ENROLED':
return { ...s, stage: 'active' };
SSO sign-up
Workspace is SSO-only. Replace the form with a single SSO button and skip verification — the IdP already proved the inbox upstream.
// Drop 'form' and 'verify'
return (
<AuthShell>
<Button onClick={signUpWithSso}>
Continue with SSO
</Button>
</AuthShell>
);
Invite-only
The user lands on a tokenised invite URL. Skip the email field; pre-fill from the token; skip verification entirely.
// Read the invite on mount
useEffect(() => {
const t = new URLSearchParams(location.search).get('invite');
fetch(`/api/invites/${t}`)
.then(r => r.json())
.then(i => setEmail(i.email));
}, []);
Accessibility
What this recipe takes care of for you. Each item is something a screen-reader or keyboard user will notice if you skip it.
- The password field has a hint, not just a meter.
Fieldrenders the hint asaria-describedbyso SR users hear the rules before they type — not after a failed submit. - Meter announces its rating.
Meterrendersaria-valuetext("Strong") so the SR reads the strength word, not "3 of 4". - Terms checkbox is
required. The submit button is also disabled, so the failure mode is "button disabled, here's why" instead of "form submits, then errors". - Verify screen is a live region. When polling flips to verified,
aria-live="polite"means the SR hears "Account active" without the user having to refocus. - Poller pauses on hidden tabs. No network spam, no false "still polling" announcements, no battery burn on phones.
- Resend disables itself. Real
disabledwhile the countdown runs — keyboard skip, SR reads it as dimmed. - Whole flow is keyboard-only. Tab order matches reading order, Enter submits, Esc on verify returns to the form so the user can fix a typo.