Home/ Cookbook/ Auth/ Sign-up + email verify

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.

Auth 5 primitives · 0 blocks · 1 pattern ~25 min React · @corelithzw/react

Overview

A three-stage flow: formverifyactive. 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.

Huchu

Create your account

30 days free. No card required.

I agree to the Terms and Privacy Policy.
Send me product updates.
Create account
01 Form — email, password, terms

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

02 Verify — poller in the background

Account active

Welcome, Tendai. We'll set up your workspace.

Get started
03 Active — confirm + handoff

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

01

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.

Code-only step

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.

Step 1 · State machine@corelithzw/react
02

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.

Work email
tendai@mukamba.co
Password
••••••••••••
Create account
Step 2 · Sign-up form
03

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.

Checking… · Resend in 0:42
Wrong email?
Step 3 · Verify + poll
04

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.

Get started
Step 4 · Active

Final composition

The whole SignUpEmailVerify component. Reducer, three stages, strength meter, terms gate, visibility-aware poller, resend cooldown.

Huchu

Create your account

30 days free. No card required.

Work email
tendai@mukamba.co
Password
••••••••••••
Create account
SignUpEmailVerify.tsx@corelithzw/react

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. Field renders the hint as aria-describedby so SR users hear the rules before they type — not after a failed submit.
  • Meter announces its rating. Meter renders aria-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 disabled while 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.