create the account first last

published
edited
oct 2, 2026 · v3
reading
2 min
tags
backend, state-machines, go

Most signup flows I've worked on create the user row on the first screen. You type an email, a row appears, and every screen after that patches it. It feels efficient. It is also how you end up with thousands of accounts that have an email, no phone, no store, and no way to finish.

I used to think the fix was cleanup jobs. I now think the fix is to clean up half-created accounts never create them.

the flow, as a state machine

The client doesn't decide what comes next. It asks the server, and the server answers with the next step.

stateDiagram-v2
  direction TB
  [*] --> identify
  identify --> verify_email: email or oauth
  verify_email --> details: otp ok
  verify_email --> verify_email: wrong code
  details --> verify_phone
  verify_phone --> create: otp ok
  create --> [*]: account + store

Nothing is written to the users table until the last arrow. Everything before it lives in a short-lived session.

step stored where survives a refresh creates rows
identify session (TTL) yes no
verify email session (TTL) yes no
details session (TTL) yes no
verify phone session (TTL) yes no
create database yes yes, in one transaction

the server owns the next step

signup/step.go
type Step string

const (
	Identify    Step = "identify"
	VerifyEmail Step = "verify_email"
	Details     Step = "details"
	VerifyPhone Step = "verify_phone"
	Done        Step = "done"
)

// Next is the only place that knows the order.
// The client renders whatever it is told to render.
func Next(s *Session) Step {
	switch {
	case s.Email == "":
		return Identify
	case !s.EmailVerified:
		return VerifyEmail
	case s.Name == "" || s.StoreName == "":
		return Details
	case !s.PhoneVerified:
		return VerifyPhone
	default:
		return Done
	}
}

Because the order lives in one function, adding a step is one case, not a change in three apps.

what it bought us

  • No half-created accounts. There is nothing to clean up because nothing was written.
  • The client got simpler. It stopped tracking progress and started rendering steps.
  • Retrying is safe. Refreshing the page asks the server where you are, and the answer doesn't change.

The trade-off is that the session store becomes important. If it goes down, signups pause. I'm fine with that. A paused signup is better than a broken account.

page history

  1. v3 · oct 2, 2026

    fixed the OTP retry arrow in the diagram

  2. v2 · sep 22, 2026

    added the state table

  3. v1 · oct 7, 2026

    published