# Clerk simulator

A stateful Clerk Backend API simulator — users and their metadata, organizations and memberships, webhooks, and a browser sign-in companion for Next.js apps.

`clerk-twin` is a local, stateful simulator of Clerk's Backend API — users and their metadata, organizations with their metadata and memberships, and the webhooks they emit — with a browser companion that signs a `@clerk/nextjs` app in. It never calls Clerk: no email is sent, no provider is contacted and no real instance changes.

## Quickstart

You need an API key from [Account → API keys](https://www.rystic.ai/account/api-keys) ([how](https://www.rystic.ai/docs/licensing.md#get-an-api-key)).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                      # your API key
rystic run clerk-twin -d --port 0       # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version
```

Expect `"product":"clerk-twin"`. The examples below use `localhost:8080`; use the port `rystic run` printed.

On Enterprise, `rystic login ./rystic-license.json` takes the place of the key ([license files](https://www.rystic.ai/docs/licensing.md#enterprise-license-files)).

Create a user, then an organization they administer:

```bash
curl -s localhost:8080/v1/users -H 'Authorization: Bearer sk_test_placeholder' \
  -H 'Content-Type: application/json' \
  -d '{"email_address":["Ada@Example.com"],"first_name":"Ada","skip_password_requirement":true,"legal_accepted_at":"2026-01-01T00:00:00Z"}'
# -> {...,"email_addresses":[{...,"email_address":"ada@example.com",...}],...,"id":"user_200000000000000000000000001",...,"object":"user",...}

curl -s localhost:8080/v1/organizations -H 'Authorization: Bearer sk_test_placeholder' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Acme","created_by":"user_200000000000000000000000001"}'
# -> {...,"id":"org_1",...,"object":"organization",...}

curl -s localhost:8080/v1/organizations/org_1/memberships -H 'Authorization: Bearer sk_test_placeholder'
# -> {"data":[{...,"role":"org:admin",...}],"total_count":1}
```

Point the SDK at the twin's origin; it appends `/v1` itself:

```ts
// @clerk/backend
const clerk = createClerkClient({ secretKey: "sk_test_placeholder", apiUrl: "http://localhost:8080" });
```

## The API version

The twin answers as Clerk's Backend API version `2026-05-12` unless a request's `Clerk-API-Version` header names another, and every answer carries the version in `clerk-api-version`.

1. It accepts `2021-02-05`, `2024-10-01`, `2025-04-10`, `2025-11-10` and `2026-05-12`; any other value answers 400 `api_version_invalid`.
2. From `2026-05-12`, `PATCH /v1/users/{user_id}` refuses `public_metadata`, `private_metadata` and `unsafe_metadata`, and `PATCH /v1/organizations/{organization_id}` refuses `public_metadata` and `private_metadata`, each with 422 `form_param_deprecated`: write metadata through the `/metadata` routes.

## Plans

Every plan ([plans](https://www.rystic.ai/docs/licensing.md#plans)), Free included, serves the whole Clerk surface, the browser companion and every control route below but `/_rystic/network` and `/_rystic/ws-drop`. Without `--scenario` the twin boots an **empty** instance, and `POST /_rystic/reset` wipes it back to empty.

**Free** runs 5 hours of simulator time a month. **Enterprise** runs on a license file instead of an API key.

Custom latency and fault injection are Pro and Enterprise: `/_rystic/network` and `/_rystic/ws-drop` need the `faults` feature, and on Free they answer 403 naming it.

## Auth

Clerk reads a secret key as `Authorization: Bearer sk_…`. The twin accepts any bearer value that starts with `sk_` and checks nothing else; no Clerk instance or key is involved.

1. A request with no bearer token, or one whose value does not start with `sk_`, is refused with the error its route's spec documents, so the status and code vary by route: 401 `authentication_invalid` on the user routes, the `/metadata` routes, listing organization memberships and deleting one; 401 `authorization_header_format_invalid` on `GET /v1/users/count`; 403 `authentication_invalid` on `GET /v1/organizations` and `GET /v1/organizations/{organization_id}`; 403 `authorization_invalid` on `POST /v1/organizations`, `PATCH /v1/organizations/{organization_id}`, adding a membership and `GET /v1/users/{user_id}/organization_memberships`.
2. `DELETE /v1/organizations/{organization_id}` and `PATCH /v1/organizations/{organization_id}/memberships/{user_id}` check no credential, since their spec declares no refusal; Clerk refuses them. None of these refusals is measured against Clerk.
3. Publishable keys (`pk_…`) belong to the browser companion below, never to the Backend API.

## Coverage

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs clerk; do not edit) -->

<!-- coverage-claim -->
40 routes across 5 resources, counted from the sim's literal registrations: 20 listed below, and 20 more that answer the same operations at other paths.

| Resource | Operations |
|---|---|
| Organization (8) | `GET, POST /v1/organizations`; `GET, PATCH, DELETE /v1/organizations/{organization_id}`; `GET /v1/organizations/{organization_id}/memberships`; `PUT, PATCH /v1/organizations/{organization_id}/metadata` |
| Organization Membership (3) | `POST /v1/organizations/{organization_id}/memberships`; `PATCH, DELETE /v1/organizations/{organization_id}/memberships/{user_id}` |
| User (9) | `GET, POST /v1/users`; `GET /v1/users/count`; `GET, PATCH, DELETE /v1/users/{user_id}`; `PUT, PATCH /v1/users/{user_id}/metadata`; `GET /v1/users/{user_id}/organization_memberships` |

<!-- END generated: served-operations -->

1. Bodies are JSON both ways, and timestamps are Unix milliseconds.
2. Email addresses are stored lowercase and matched in any case: `GET /v1/users?email_address=` finds a user by any letter case of their address, and a create whose address differs from a taken one only in case answers 422 `form_identifier_exists`. A malformed address answers 422 `form_param_format_invalid` on `email_address`.
3. `GET /v1/users` refuses an `order_by` that names no sortable field with 422 `form_param_value_invalid`.
4. `POST /v1/organizations` with `created_by` gives that user an `org:admin` membership: `GET /v1/organizations/{organization_id}/memberships` lists it right after the create, and `GET /v1/organizations/{organization_id}?include_members_count=true` counts it. Deleting a user removes their memberships, and each organization's count drops by one.
5. `PATCH /v1/organizations/{organization_id}/metadata` deep-merges `public_metadata` and `private_metadata`, removing a key set to `null`; `PUT` replaces each field it names whole. The user `/metadata` pair does the same for a user.
6. Every `image_url` and `profile_image_url` the twin hands out leads back to the twin, under `/preview/`, where `GET` or `HEAD` answers a PNG with no credential.

Not modelled, by decision: ban and unban, lock and unlock, sessions, invitations and session tokens, the Frontend API, and every other Backend API family. An unserved route answers 404 `{"message":"No route in twin for …"}`, not Clerk's error envelope.

## The browser companion

Beside the Backend API the twin serves lookalike sign-in and sign-up screens for `@clerk/nextjs`: email sign-in (any email signs in, and an unknown one creates a user after consent), simulated **Continue with GitHub** and **Continue with Google** buttons that sign in as `github@example.test` and `google@example.test`, organization selection and member management, and RS256 session tokens that carry the current public metadata and organization. It shares its users, organizations and memberships with the Backend API. No provider, OAuth exchange or email is involved.

It needs three things at boot: an RSA private key of your own, your site's origin, and the twin's own origin as the browser reaches it (`CLERK_TWIN_ISSUER`). The twin 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. The issuer's host needs a dot, because Clerk SDKs refuse the publishable key of a host such as `localhost`, so the example uses `127.0.0.1`:

```bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ./clerk-twin.pem && chmod 600 ./clerk-twin.pem
rystic run clerk-twin --runner native -d --port 8080 --env CLERK_TWIN_PRIVATE_KEY_FILE="$PWD/clerk-twin.pem" \
  --env CLERK_TWIN_SITE_ORIGIN=http://localhost:3000 --env CLERK_TWIN_ISSUER=http://127.0.0.1:8080
curl -s localhost:8080/_twin/config      # the publishable key, the public PEM and the proxy URL
```

Configure the site from `/_twin/config` and the twin's paths:

1. Set `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_JWT_KEY` and `NEXT_PUBLIC_CLERK_PROXY_URL` from `/_twin/config`.
2. Point `NEXT_PUBLIC_CLERK_JS_URL` and `NEXT_PUBLIC_CLERK_UI_URL` at the twin's `/clerk.js` and `/clerk-ui.js`.
3. Set `CLERK_API_URL` to the twin's origin, and use any `sk_` value as `CLERK_SECRET_KEY`.
4. Set `NEXT_PUBLIC_CLERK_SIGN_IN_URL` and `NEXT_PUBLIC_CLERK_SIGN_UP_URL` to your own pages.

When a session 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 twin answers there and sends the browser back to your site, with the session renewed if it is still live and signed out if it is not. Without the sign-in and sign-up URLs, the SDK sends a signed-out visitor to Clerk's hosted Account Portal. Without a key file the companion's JSON and JWKS routes are off; the Backend API serves either way.

## Control plane

Every route under `/_rystic/` is listed in [routes by plan](https://www.rystic.ai/docs/control-plane.md#routes-by-plan). The twin has no WebSocket feed.

## Scenarios

Five shipped worlds, each on `2026-02-02T00:00:00Z`.

| Scenario | What it holds |
|---|---|
| `clerk_acme_tenant` | five users with mixed credential and MFA state in one organization, two admins and three members, two pending invitations and three live browser sessions |
| `clerk_holdco_orgs` | one identity across three organizations as admin, billing manager and member, each with its own session context |
| `clerk_invite_backlog` | an organization at its ten-seat cap with two members and eight invitations pending across roles |
| `clerk_session_sweep` | six browser sessions across three users: two long lapsed, one expiring two hours after the pinned clock, three live |
| `clerk_solo_founder` | one user, the organization she created, the membership joining them, her one session and her one invitation |

## Seeding state

`POST /_rystic/seed` takes a register map — `state_user`, `state_organization`, `state_organization_membership` (keyed `<organization_id>/<user_id>`), `state_browser_session`, `state_browser_invitation` — and lands it directly; `GET /_rystic/registers` lists every field. `--seed <file>` or `RYSTIC_SEED=<file>` boots one.

## Determinism & reset

Ids are a sequence (`user_200000000000000000000000001`, then `…0002`; `org_1`, then `org_2`). The clock starts at the wall time the twin boots and moves one millisecond per request that stamps a time, never by wall time; every scenario starts at `2026-02-02T00:00:00Z`. `POST /_rystic/clock` pins it, so two identical runs from the same reset and the same pinned clock give byte-identical users and organizations. Sessions expire after 24 hours of twin time, and `POST /_rystic/reset` ends every session: a token issued before it no longer verifies. A session token's `iat`, `nbf` and `exp` are stamped on the wall clock instead, with `exp` at the session's remaining life, so your site accepts them however long the twin sits idle and wherever its clock is pinned.

## Egress

Each user write (its `/metadata` routes included), each organization create, update and delete, and each membership write records the webhook event Clerk would send: `user.created`, `user.updated`, `user.deleted`, `organization.created`, `organization.updated`, `organization.deleted`, and `organizationMembership.created`, `.updated` and `.deleted`. The organization `/metadata` routes record none. `GET /_rystic/egress` returns the events in order. Boot the twin with `--env CLERK_TWIN_WEBHOOK_URL=<your receiver>` and `--env CLERK_TWIN_WEBHOOK_SECRET=whsec_…` and each is also delivered, signed with Svix headers whose `svix-timestamp` is the wall clock at the write, so your receiver's five-minute window holds however long the twin sat idle; the envelope's own `timestamp` is twin time. A failed delivery never undoes the write. The event shapes follow Clerk's webhook documentation and are not measured against live deliveries.

## Limitations

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs clerk; do not edit) -->

**Known issues.**

- Typed parameters are not validated: `banned=maybe`, `created_at_after=notanumber` or a numeric `first_name` is accepted where Clerk answers 422 (RYS-1906)
- Responses carry no `x-clerk-trace-id` header to match the body's `clerk_trace_id` (RYS-1912)
- An operation the twin does not serve, such as ban, lock, sessions or invitations, answers a plain 404 instead of Clerk's `resource_not_found` error (RYS-1913)

**Not served.** 232 of the 252 operations in the published spec are not served.

- Organization Memberships, 4 of 6 served: `GET /v1/organization_memberships`, `PATCH /v1/organizations/{organization_id}/memberships/{user_id}/metadata`
- Organizations, 7 of 12 served: `GET /v1/organizations/{organization_id}/billing/credits`, `POST /v1/organizations/{organization_id}/billing/credits`, `GET /v1/organizations/{organization_id}/billing/subscription`, `PUT /v1/organizations/{organization_id}/logo`, `DELETE /v1/organizations/{organization_id}/logo`
- Users, 9 of 37 served: `POST /v1/users/ban`, `POST /v1/users/unban`, `DELETE /v1/users/{user_id}/backup_code`, `POST /v1/users/{user_id}/ban`, `GET /v1/users/{user_id}/billing/credits`, `POST /v1/users/{user_id}/billing/credits`, `GET /v1/users/{user_id}/billing/subscription`, `GET /v1/users/{user_id}/biometric_credentials`, … and 20 more
- Not served at all: Actor Tokens (2), Admin Portal Link Tokens (2), Agent Tasks (2), Allow-list / Block-list (6), API Keys (8), Beta Features (3), Billing (12), Clients (3), Directories (10), Domains (4), Email & SMS Templates (6), Email Addresses (7), Enterprise Connections (7), Instance Settings (11), Invitations (4), JWKS (1), JWT Templates (5), M2M Tokens (4), Machines (9), Miscellaneous (1), … and 18 more

**Required fields the model does not declare.** The spec requires these in a success response; the model's response shape leaves them out, so the twin may not return them:

- `PATCH /v1/organizations/{organization_id}/memberships/{user_id}` (200): `public_user_data/first_name`, `public_user_data/has_image`, `public_user_data/image_url`, `public_user_data/last_name`, `public_user_data/profile_image_url`, `public_user_data/user_id`
- `POST /v1/organizations/{organization_id}/memberships` (200): `organization/admin_delete_enabled`, `organization/created_at`, `organization/has_image`, `organization/id`, `organization/max_allowed_memberships`, `organization/name`, `organization/public_metadata`, `organization/slug`, … and 6 more
- `PUT /v1/users/{user_id}/metadata` (200): `backup_code_enabled`, `create_organization_enabled`, `delete_self_enabled`, `external_id`, `last_active_at`, `last_sign_in_at`, `legal_accepted_at`, `lockout_expires_in_seconds`, … and 9 more

**Not measured.** The fidelity suite runs 100 probes, and 19 documented branches have none. 13 need a credential the measuring account does not hold (401 ×6, 403 ×7). The other 6:

| Operation | Status | Why |
|---|---:|---|
| `DELETE /v1/organizations/{organization_id}/memberships/{user_id}` | 422 | the model describes no input that reaches it |
| `GET /v1/organizations/{organization_id}/memberships` | 422 | nothing the twin serves creates the record it needs |
| `PATCH /v1/organizations/{organization_id}` | 402 | the model describes no input that reaches it |
| `PATCH /v1/users/{user_id}` | 409 | the model describes no input that reaches it |
| `POST /v1/organizations` | 402 | the model describes no input that reaches it |
| `POST /v1/users` | 402 | the model describes no input that reaches it |

1 probe from earlier rounds is kept as unmeasured.

**Not modeled.** What the model's operations state they leave out:

- Credential-refusal outcomes (401/403) are not declared in this operation's responses and are not observable with the harness's valid secret key. (`DELETE /v1/organizations/{organization_id}`)
- Deletion of the organization's pending invitations (cascade is documented, but no invitation state family or operation is in scope). (`DELETE /v1/organizations/{organization_id}`)
- The documented side effect of clearing users' active sessions containing the deleted organization: no session state family exists in the model and no in-scope operation observes it. (`DELETE /v1/organizations/{organization_id}`)
- The 422 'Invalid request parameters' branch has no documented trigger condition or error code: the evidence names only the response, so no derivable request recipe (violating value, threshold or dedup key) can be declared and the branch carries no executable probe. (`DELETE /v1/organizations/{organization_id}/memberships/{user_id}`)
- Whether the organization_id path parameter also accepts the instance-unique slug here, as GET /organizations/{organization_id} documents; this operation's parameter description says only 'The ID of the organization'. (`DELETE /v1/organizations/{organization_id}/memberships/{user_id}`)
- Whether the service refuses removal of the last member holding elevated permissions (Organization.missing_member_with_elevated_permissions hints at such a rule) and, if so, at which status — not stated by any chunk in this bundle. (`DELETE /v1/organizations/{organization_id}/memberships/{user_id}`)
- The 400 ClerkErrors branch has no documented trigger for this operation (no request body, single string path parameter) and no captured wire observation; its condition and error code remain unresolved and no executable recipe is declared. (`DELETE /v1/users/{user_id}`)
- Whether deleting a user cascades to that user's organization memberships (state_organization_membership) is not stated in this operation's evidence. (`DELETE /v1/users/{user_id}`)
- Error codes carried in ClerkError.code for each rejection are not documented in the slice, so no branch discriminators are declared. (`GET /v1/organizations`)
- The exact effect of the +/- prefixes on user_id and organization_id filters, and of order_by on members_count, is described in prose only and is not modeled as separate branches. (`GET /v1/organizations`)
- The precise condition that selects 400 (ClerkErrors, 'Request was not successful') over 422 for this read is undocumented; the 400 guard carries no derivable request recipe. (`GET /v1/organizations`)
- Whether an unknown slug and an unknown id produce distinguishable error codes within the single documented 404 branch. (`GET /v1/organizations/{organization_id}`)
- Whether an unparseable/non-boolean value for include_members_count or include_missing_member_with_elevated_permissions is rejected (no 400/422 documented for this operation), so no violating query example is declared. (`GET /v1/organizations/{organization_id}`)
- Whether missing credentials (no Authorization header) answer 403 like an invalid key or some other status; the spec documents only AuthorizationInvalid, so no runtime missing_credential delegation is declared. (`GET /v1/organizations/{organization_id}`)
- Filter semantics of the array parameters (user_id, email_address, phone_number, username, web3_wallet, role): the +/- include/exclude syntax and the documented 100-value cap have no documented rejection status; values not found are ignored. (`GET /v1/organizations/{organization_id}/memberships`)
- Ordering semantics of order_by (+/- prefixes over phone_number, email_address, created_at, first_name, last_name, username) — the spec names the sortable fields but not tie-breaking or the default order. (`GET /v1/organizations/{organization_id}/memberships`)
- Whether the organization_id path parameter also accepts the organization slug on this path (documented only for GET /organizations/{organization_id}). (`GET /v1/organizations/{organization_id}/memberships`)
- Whether total_count reflects the filtered set or every membership of the organization. (`GET /v1/organizations/{organization_id}/memberships`)
- The service behavior when a multi-value filter (email_address, user_id, organization_id, ...) exceeds its documented cap of 100 values is not stated: it is modeled under the generic 400 guard with low confidence. (`GET /v1/users`)
- The specific ClerkError `code` values for each 422 cause (cursor-with-offset, cursor-with-non-created_at-ordering, limit out of range) are not documented and no wire capture is present, so the 422 branch carries no x_body_discriminator and the four causes are not split into separate branches. (`GET /v1/users`)
- … and 48 more

<!-- END generated: known-gaps -->

Fidelity is scored differentially against a live Clerk development instance, per response field: the last full-suite round matched **100 of 100 probes on 2026-10-07** (`convergence_log.json` `20261007T131900Z`), with 0 inconclusive, on a 100-probe suite of the Backend API. The browser companion, session signing and webhook delivery are held by the twin's own tests, not measured against Clerk. Each release states its own number.

## Release notes

Newest first.

### v0.0.1 — 2026-10-11

1. **A local Clerk for your app's users, organizations and sign-in.** The first release of the Clerk twin: the 20 Backend API operations on users and their metadata, organizations with their metadata and memberships, at `Clerk-API-Version: 2026-05-12`, served from a model validated against a live Clerk development instance, and a browser companion that serves a lookalike `clerk.js` so a `@clerk/nextjs` app signs users in against the same state. It runs on Pro and Enterprise. Point `@clerk/backend` at it with any `sk_` key (`HOWTO.md` shows how); no email is sent and no real instance changes. (RYS-1315, RYS-1234, RYS-2412)

2. **Users and their metadata, as Clerk keeps them.** `POST /v1/users` stores each email address in lowercase, `GET /v1/users?email_address=` finds a user by any letter case of their address, and an address that differs from a taken one only in case answers 422 `form_identifier_exists`; a malformed one answers 422 `form_param_format_invalid`. `GET /v1/users` refuses an `order_by` that names no sortable field with 422 `form_param_value_invalid`. `PATCH` and `PUT /v1/users/{user_id}/metadata` merge and replace metadata, and `DELETE /v1/users/{user_id}` removes the user's organization memberships. Every `image_url` and `profile_image_url` leads to a PNG the twin serves under `/preview/`. (RYS-1858, RYS-1899, RYS-1900)

3. **Organizations, their metadata and their members.** An organization created with `created_by` lists its creator as an `org:admin` member right after the create, and `include_members_count=true` counts them. `PATCH /v1/organizations/{organization_id}/metadata` deep-merges `public_metadata` and `private_metadata`, removing a key set to `null`, and `PUT` replaces each field it names whole. Writes and lists answer with the keys Clerk's answers carry. (RYS-1895, RYS-1899, RYS-1909)

4. **`Clerk-API-Version` is checked on every route and echoed.** `2021-02-05`, `2024-10-01`, `2025-04-10`, `2025-11-10` and `2026-05-12` (the default) are accepted and echoed in `clerk-api-version`, and any other value answers 400 `api_version_invalid`. At `2026-05-12`, metadata sent to `PATCH /v1/users/{user_id}` or `PATCH /v1/organizations/{organization_id}` answers 422 `form_param_deprecated`, as Clerk does: write it through the `/metadata` routes. (RYS-1914)

5. **A browser sign-in for `@clerk/nextjs`.** Point `NEXT_PUBLIC_CLERK_JS_URL` and `NEXT_PUBLIC_CLERK_UI_URL` at the twin's `/clerk.js` and `/clerk-ui.js` and your app gets email sign-in and sign-up with consent, **Continue with GitHub** and **Continue with Google** buttons that sign in local test identities, organization creation and selection, and member invitation, role changes and removal; signing in as an invited address joins its organization with no email sent. Sessions are RS256 tokens stamped on the wall clock your site verifies them against, and the SDK middleware's handshake is answered by the twin, so a sandboxed site never leaves it. Boot the twin with your own RSA key, your site's origin and its own origin; `GET /_twin/config` gives the publishable key, the public PEM and the proxy URL. (RYS-1234, RYS-1475, RYS-2413, RYS-2414)

6. **Webhooks Clerk would send, signed with Svix headers.** Every user write, organization create, update and delete, and membership write, from the Backend API or the browser, records `user.*`, `organization.*` or `organizationMembership.*` in `GET /_rystic/egress`; the organization `/metadata` routes record none. Boot with `CLERK_TWIN_WEBHOOK_URL` and a `whsec_` `CLERK_TWIN_WEBHOOK_SECRET` and each is also delivered, its `svix-timestamp` on the wall clock so your receiver's five-minute window holds however long the twin sat idle. A failed delivery never undoes the write. Event shapes follow Clerk's documentation and are not measured against live deliveries. (RYS-1317, RYS-2413)

7. **Five shipped worlds.** `--scenario clerk_acme_tenant` boots Acme Robotics: five users in one organization, two pending invitations and three live sessions. `clerk_holdco_orgs` holds one identity across three organizations, `clerk_invite_backlog` an organization at its ten-seat cap with eight invitations pending, `clerk_session_sweep` six sessions from lapsed to live, and `clerk_solo_founder` one user and her organization; `SCENARIOS.md` says what each is for. Without `--scenario` the twin boots an empty instance, `POST /_rystic/seed` lands users, organizations, memberships, sessions and invitations directly, and `POST /_rystic/reset` rewinds the id sequence, so two identical runs from a pinned clock give byte-identical users and organizations. (RYS-1315, RYS-2412)

8. **Measured against a live Clerk development instance.** The 2026-10-07 full-suite round matched 100 of 100 Backend API probes, with 0 inconclusive. `HOWTO.md` states what the suite leaves out and the differences this release ships with, each with its ticket: typed query parameters are not validated (RYS-1906), responses carry no `x-clerk-trace-id` header (RYS-1912), an unserved route answers a plain 404, not Clerk's `resource_not_found` (RYS-1913), and deleting an organization or changing a membership's role checks no secret key. (RYS-2412)

---

Source: https://www.rystic.ai/docs/simulators/clerk · Markdown: https://www.rystic.ai/docs/simulators/clerk.md
