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 step | The 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.