# Custom sign-up and sign-in forms

Sign-up and sign-in forms a site builds with useSignUp() and useSignIn() run on the simulator, by email with a password or with the code 424242.

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](https://www.rystic.ai/docs/simulators/clerk/features/browser-companion.md), so boot the simulator as that page does:

```bash
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:

```bash
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:

```bash
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:

```bash
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.

---

Source: https://www.rystic.ai/docs/simulators/clerk/features/custom-forms · Markdown: https://www.rystic.ai/docs/simulators/clerk/features/custom-forms.md
