Skip to Content
SimulatorsClerkCustom sign-up and sign-in forms

Simulators

Custom sign-up and sign-in forms

A site that builds its own forms with @clerk/nextjs 7’s useSignUp() and useSignIn() signs users up and in against the simulator, on the same users the Backend API serves. The forms run on the browser companion, so boot the simulator as that page does:

PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')
export RYSTIC_URL=$(rystic run clerk-twin --runner native -d --port "$PORT" --scenario clerk_acme_tenant --env CLERK_TWIN_SITE_ORIGIN=http://localhost:3000 --env CLERK_TWIN_ISSUER="http://127.0.0.1:$PORT" --json | jq -r .data.url)
curl -s -o /dev/null -w '%{http_code}\n' "$RYSTIC_URL/_twin/config"
# -> 200

Sign a user up and in from your own form

Each form step runs on the simulator:

Form stepThe simulator
signUp.password({emailAddress, password}) or signUp.create({emailAddress})checks the address and password, and leaves the sign-up waiting on the email code
signUp.verifications.sendEmailCode()sends no email
signUp.verifications.verifyEmailCode({code: '424242'})creates the user, with the password when one was given, and opens a session
signIn.password({emailAddress, password})signs in a user who has a password
signIn.emailCode.sendCode({emailAddress}), then signIn.emailCode.verifyCode({code: '424242'})signs in any user
signIn.finalize() or signUp.finalize()makes the session active, then calls your navigate callback

424242 is the only code the simulator accepts, and it accepts it for every address. A refusal reaches the form’s errors on the field it names, such as errors.fields.code for a wrong code. The older SignUp and SignIn methods on clerk.client run the same flows. Among them are attemptEmailAddressVerification and attemptFirstFactor, and setActive({session}) activates the session a flow created.

The form steps call routes under /_twin/, which a test can also call. These calls check a sign-up, reject a wrong code, complete the sign-up and list the user’s sign-in methods:

curl -s -X POST "$RYSTIC_URL/_twin/sign-up/check" -H 'Content-Type: application/json' -d '{"email":"lena@example.test","password":"correct-horse"}' | jq -c .
# -> {"email_address":"lena@example.test","has_password":true}
curl -s -X POST "$RYSTIC_URL/_twin/sign-up/verify" -H 'Content-Type: application/json' -d '{"email":"lena@example.test","password":"correct-horse","code":"000000"}' | jq -c '.errors[0] | {code, param: .meta.param_name}'
# -> {"code":"form_code_incorrect","param":"code"}
curl -s -X POST "$RYSTIC_URL/_twin/sign-up/verify" -H 'Content-Type: application/json' -d '{"email":"lena@example.test","password":"correct-horse","code":"424242"}' | jq -c '{email: .user.email_addresses[0].email_address, signed_in: (.token | length > 0)}'
# -> {"email":"lena@example.test","signed_in":true}
curl -s -X POST "$RYSTIC_URL/_twin/sign-in/check" -H 'Content-Type: application/json' -d '{"email":"lena@example.test"}' | jq -c '[.supported_first_factors[].strategy]'
# -> ["password","email_code"]

A password sign-in and a code sign-in each open a session. Its token is the one finalize() makes active in the browser, and GET /_twin/session takes it as a bearer:

TOKEN=$(curl -s -X POST "$RYSTIC_URL/_twin/sign-in/verify" -H 'Content-Type: application/json' -d '{"email":"lena@example.test","password":"correct-horse"}' | jq -r .token)
curl -s "$RYSTIC_URL/_twin/session" -H "Authorization: Bearer $TOKEN" | jq -c '{email: .user.email_addresses[0].email_address}'
# -> {"email":"lena@example.test"}
curl -s -X POST "$RYSTIC_URL/_twin/sign-in/verify" -H 'Content-Type: application/json' -d '{"email":"lena@example.test","code":"424242"}' | jq -c '{email: .user.email_addresses[0].email_address, signed_in: (.token | length > 0)}'
# -> {"email":"lena@example.test","signed_in":true}

Differs from Clerk: the simulator keeps no password to compare, so any password signs in a user who has one. A user created without a password signs in only with the code:

curl -s -X POST "$RYSTIC_URL/_twin/sign-up/verify" -H 'Content-Type: application/json' -d '{"email":"omar@example.test","code":"424242"}' | jq -c '{has_password: .user.password_enabled}'
# -> {"has_password":false}
curl -s -X POST "$RYSTIC_URL/_twin/sign-in/verify" -H 'Content-Type: application/json' -d '{"email":"omar@example.test","password":"any-password"}' | jq -c '.errors[0] | {code, param: .meta.param_name}'
# -> {"code":"form_password_incorrect","param":"password"}

What the forms do not do

The forms support an email address with a password or a code, nothing else.

Differs from Clerk: single sign-on, phone numbers, usernames, passkeys, web3 wallets, email links, multi-factor steps, tickets, password reset and the waitlist return a feature_not_enabled error in the browser. The form sends the simulator no request. The method returns the error as {error} and the form’s errors shows it, while the older SignIn and SignUp methods throw it.

Last updated on