# Browser companion

Lookalike sign-in and sign-up screens, organization switching, member management and signed session tokens, so a @clerk/nextjs app signs users in against the simulator.

The browser companion lets a `@clerk/nextjs` app sign users in, switch organizations and manage members against the simulator, on the same users, organizations and memberships the Backend API serves. Its lookalike sign-in and sign-up screens offer email sign-in and simulated **Continue with GitHub** and **Continue with Google** buttons. Its screens also create and select organizations, invite members, change roles and remove members. No provider, OAuth exchange or email is involved.

The companion needs three things at boot: an RSA private key of your own, your site's origin and the simulator's own origin as the browser reaches it (`CLERK_TWIN_ISSUER`). The simulator reads the key from a file on your machine, which the Docker runner does not mount, so the example runs it on the native runner. Clerk's libraries refuse the publishable key of a host with no dot, such as `localhost`, so the issuer uses `127.0.0.1`. The example uses port 8123: use any free port in both the flag and the issuer.

```bash
KEY="$(mktemp -d)/clerk-twin.pem"
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$KEY"
chmod 600 "$KEY"
export RYSTIC_URL=$(rystic run clerk-twin --runner native -d --port 8123 --scenario clerk_acme_tenant --env CLERK_TWIN_PRIVATE_KEY_FILE="$KEY" --env CLERK_TWIN_SITE_ORIGIN=http://localhost:3000 --env CLERK_TWIN_ISSUER=http://127.0.0.1:8123 --json | jq -r .data.url)
curl -s "$RYSTIC_URL/_twin/config" | jq -c '{issuer, proxy_url}'
# -> {"issuer":"http://127.0.0.1:8123","proxy_url":"http://127.0.0.1:8123"}
curl -s -o /dev/null -w '%{http_code}\n' "$RYSTIC_URL/clerk.js"
# -> 200
```

### Configure your site

`GET /_twin/config` returns the publishable key, the public PEM and the proxy URL. Configure the site from them and the simulator's own paths:

| Site variable | Value |
|---|---|
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | `publishable_key` from `/_twin/config` |
| `CLERK_JWT_KEY` | `jwt_key` from `/_twin/config` |
| `NEXT_PUBLIC_CLERK_PROXY_URL` | `proxy_url` from `/_twin/config` |
| `NEXT_PUBLIC_CLERK_JS_URL` | the simulator's `/clerk.js` |
| `NEXT_PUBLIC_CLERK_UI_URL` | the simulator's `/clerk-ui.js` |
| `CLERK_API_URL` | the simulator's origin |
| `CLERK_SECRET_KEY` | any `sk_` value |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL`, `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | your own sign-in and sign-up pages |

Without a key file, `/_twin/*` and the JWKS (`/.well-known/jwks.json`, `/jwks`, `/v1/jwks`) answer 404, while `clerk.js` and the Backend API serve either way.

### Sign-in

Any address signs in, and an unknown one creates a user. Signing up asks for consent. Signing in as an address with a pending invitation joins its organization, with no email sent. **Continue with GitHub** signs in as `github@example.test` and **Continue with Google** as `google@example.test`; the first use asks for the site's consent checkbox. No provider is contacted.

**Differs from Clerk:** sign-in is passwordless, and any address works. There is no password, MFA, OAuth, email or SMS delivery.

### Sessions

A session is an RS256 token carrying the user's current public metadata and active organization, so a role your backend changes shows on the next token refresh. Sessions last 24 hours of the simulator's clock, and a reset ends every session. A token's `iat`, `nbf` and `exp` are stamped on the wall clock your site verifies them against. `exp` is that wall time plus the session's remaining life on the simulator's clock. Tokens therefore verify however long the simulator sits idle and wherever its clock is pinned.

**Differs from Clerk:** Clerk's session tokens are short-lived, with `exp` set by the instance's token lifetime setting. The simulator's `exp` runs to the session's end.

### The handshake

When a token has expired, or a visit arrives from another origin, the SDK middleware sends the browser to the proxy URL's `/v1/client/handshake`. The simulator sends it back to `redirect_url`, which must be on the site origin, with the session renewed while it is live and signed out once it has ended. Without the sign-in and sign-up URLs above, the SDK sends a signed-out visitor to Clerk's hosted Account Portal instead.

### Origins and private metadata

The companion's JSON routes return 403 to a request whose `Origin` header names any origin but the configured site's. `clerk.js`, `clerk-ui.js` and `clerk.css` load from any origin. Private metadata never reaches the browser, while webhooks and the Backend API carry it.

### What it does not do

The companion is a test surface, not Clerk's Frontend API. It serves only the `@clerk/nextjs` surface a Next.js app uses, and its sign-in is passwordless ([Sign-in](#sign-in)). Organization profile custom pages mount through the site's own callbacks. Its roles are `org:admin` and `org:member`, and it refuses to remove an organization's last admin.

**Differs from Clerk:** other Clerk components render an unsupported message.

Session revocation is per process. A reset ends every session the process issued. A fresh process with the same signing key cannot tell an old token from a new one, so use a new key when you reuse seeded sessions across runs.

---

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