# Add Clerk authentication to a React app with the Clerk CLI - Part 2

> Part 2 of 2. Start with [Add Clerk authentication to a React app with the Clerk CLI](https://clerk.com/articles/add-clerk-authentication-to-a-react-app-with-the-clerk-cli.md).

_This is Part 2 of a two-part series on adding Clerk authentication to a React application using the Clerk CLI. [Part 1](https://clerk.com/articles/add-clerk-authentication-to-a-react-app-with-the-clerk-cli.md) covered the CLI installation, scaffolding the Vite React app, and getting the dev server running. This part covers React SPA-specific auth patterns and managing the Clerk instance as code._

## React-specific: protecting a route and understanding SPA auth considerations

React SPA + Clerk is a specific shape, and a few of its details only matter in this shape. This section covers them all in one place so you can decide up front whether a pure SPA is the right fit.

### Gating UI with `<Show>`

In Clerk Core 3, the React SDK unifies every conditional render behind one component. The legacy `<SignedIn>` / `<SignedOut>` components were removed from `@clerk/react` v6 — the current primitive is [`<Show>`](https://clerk.com/docs/react/reference/components/control/show.md) with a typed `when` prop:

```tsx
// src/App.tsx
import { Show, UserButton } from '@clerk/react'

function Dashboard() {
  return (
    <section>
      <h2>Dashboard</h2>
      <p>Signed-in-only content.</p>
    </section>
  )
}

export default function App() {
  return (
    <main>
      <header>
        <Show when="signed-in">
          <UserButton />
        </Show>
        <Show when="signed-out">
          <p>Sign in to see the dashboard.</p>
        </Show>
      </header>

      <Show when="signed-in">
        <Dashboard />
      </Show>
    </main>
  )
}
```

The `when` prop takes `"signed-in"`, `"signed-out"`, or an authorization object (role, permission, feature, plan) — one component for both authentication and authorization checks. `<Show>` is purely visual: it only controls what renders. Any data the signed-in UI depends on still needs to come from an API call that a backend has independently verified.

### Route gating without a router

The CLI starter does not ship a router. You have two options:

- **Conditional render only** — wrap the component (or the whole page) in `<Show when="signed-in">` as above. Fine for a single-page dashboard.
- **Add a router** — install `react-router-dom` (or TanStack Router), define routes, and gate them with `<Show>` or a custom guard. The pattern is the same either way: client-side guards are UX, not security.

For a custom guard, `useAuth()` from `@clerk/react` returns `{ isLoaded, isSignedIn, userId, getToken }`. Wrap any route or component in a conditional that waits for `isLoaded` (to avoid rendering during the initial auth resolve) and then branches on `isSignedIn`:

```tsx
import { useAuth } from '@clerk/react'
import { Navigate } from 'react-router-dom'

export function RequireAuth({ children }: { children: React.ReactNode }) {
  const { isLoaded, isSignedIn } = useAuth()
  if (!isLoaded) return null
  if (!isSignedIn) return <Navigate to="/sign-in" replace />
  return <>{children}</>
}
```

Use hooks when you need to run code conditionally (not just render), use `<Show>` when you only need to render conditionally.

### Where the publishable key lives in your bundle

The publishable key ships to the client, and that is deliberate. It identifies your Clerk Frontend API to the browser and cannot be used to call the Backend API or modify user records (see [How Clerk works](https://clerk.com/docs/guides/how-clerk-works/overview.md) for the underlying model). The authority surface of a publishable key is: tell Clerk "I am this instance, here is a session, please issue a JWT." That is the entire capability.

The secret key never ships to the client. In a pure SPA there is no place to use it safely — it belongs on a server. `clerk env pull` writes it to `.env.local` because your future backend will need it and keeping both keys in one file avoids drift. Until that backend exists, the secret key sits there unused.

### OAuth redirect URLs on production deploys

In development, OAuth flows (Google, GitHub, etc.) use Clerk's shared OAuth credentials, which are pre-allowlisted for `localhost`. In production, each origin you deploy to — `https://app.example.com`, `https://staging.example.com` — must be allowlisted in your Clerk instance before first sign-in will complete. Missing this step is the most common "it worked locally, broken in prod" OAuth failure.

Allowlist production origins via the Clerk Dashboard ([production deploy checklist](https://clerk.com/docs/guides/development/deployment/production.md), [customize redirect URLs](https://clerk.com/docs/guides/development/customize-redirect-urls.md)) or via the Backend API. If you enable your own OAuth providers (not Clerk's shared credentials), each provider also has its own authorized-redirect list inside that provider's console — Google, GitHub, and Apple all require the provider-side allowlist independently of Clerk's side. Plan both sides of that allowlist change into the same deploy.

### When React SPA + Clerk is not enough

A pure client-rendered React app using Clerk is the right shape when:

- Your app is fully client-rendered and talks to APIs you already secure elsewhere.
- The sensitive data your app shows is gated by its own API's authorization, not by what the React tree renders.
- You do not need server-side session reads or middleware-based route protection.

If any of those do not hold — if you need server-side rendering, protected API routes on the same origin, or server-only secrets in request handlers — reach for Next.js or TanStack Start. Both ship dedicated Clerk SDKs (`@clerk/nextjs` v6, `@clerk/tanstack-react-start` v1) with server-side auth helpers, middleware, and SSR-aware providers. Switching framework later is annoying but possible; getting the shape right up front is cheaper.

If you have a backend (Express, Hono, FastAPI, Go, whatever), Clerk issues a short-lived [JWT](https://clerk.com/glossary.md#json-web-token) for each signed-in user that your SPA attaches to outbound requests as a `Bearer` token via `useAuth().getToken()`. Your backend must verify that token — signature, issuer, expiry, and any required claims — on every protected request using Clerk's server SDK or a JWKS lookup. The "How do I verify Clerk tokens on my backend?" question in the FAQ points at the dedicated guide; that workflow is out of scope for this article.

## Configure Clerk as code: `clerk config`

The click-through dashboard is still there when you want it, but the CLI now exposes instance configuration as pull-able, patch-able JSON. Combined with git, that is the "config as code" workflow dashboards never supported: every sign-in option, passkey toggle, session policy, and attack-protection rule is a diff on a branch, reviewed and merged like any other change.

The core loop is four commands:

```bash
clerk config schema                          # what fields exist
clerk config pull --output config.before.json  # snapshot the current state
clerk config patch --dry-run --json '{...}'  # preview a change
clerk config patch --json '{...}' --yes      # apply it
```

Let's run four patches against the React instance. Start by snapshotting the baseline:

```bash
clerk config pull --output config.before.json
```

### Patch 1 — enable passkeys

Passkeys are [WebAuthn](https://clerk.com/glossary.md#webauthn)-backed credentials (biometric or device-bound) that replace passwords. The correct schema field is `used_for_sign_in`:

```bash
clerk config patch --dry-run --json '{"auth_passkey":{"used_for_sign_in":true}}'
clerk config patch --json '{"auth_passkey":{"used_for_sign_in":true}}' --yes
```

> `auth_passkey.enabled` is not a real schema field. Patches that include it are accepted by the API but silently stripped on apply, and the diff looks like a no-op even though the CLI reports success. Always snapshot with `clerk config pull` and diff against the result to confirm a patch actually persisted. `used_for_sign_in` is the field that toggles passkey sign-in on.

> Passkeys are a paid-plan feature. On the free plan, `config patch` returns a 403 when you try to enable them. Plan and pricing details live on the [Clerk Pricing page](https://clerk.com/pricing).

### Patch 2 — add username as a sign-in method

Enable username-based sign-in and sign-up:

```bash
clerk config patch --dry-run --json '{"auth_username":{"used_for_sign_in":true,"used_for_sign_up":true}}'
clerk config patch --json '{"auth_username":{"used_for_sign_in":true,"used_for_sign_up":true}}' --yes
```

Each identifier (email, phone, username, first name, last name) has the same two flags — `used_for_sign_in` and `used_for_sign_up` — which is how you mix and match the shape of your sign-in form as code.

### Patch 3 — set session lifetime

Session policy lives under `session`. The correct schema field is `lifetime` (seconds), not `inactivity_timeout_in_ms`:

```bash
clerk config patch --dry-run --json '{"session":{"lifetime":7200}}'
clerk config patch --json '{"session":{"lifetime":7200}}' --yes
```

`lifetime: 7200` sets a two-hour session token lifetime. The Clerk frontend auto-refreshes tokens before expiry, so `lifetime` sets the outer bound, not the frequency of a full sign-in.

> Custom session configuration is a paid-plan feature. On the free plan, the patch returns 403. Pricing on the [Clerk Pricing page](https://clerk.com/pricing).

### Patch 4 — harden attack protection

Block disposable-email providers and tighten the brute-force lockout threshold in one patch:

```bash
clerk config patch --dry-run --json '{"auth_access_control":{"block_disposable_email_domains":true},"auth_attack_protection":{"user_lockout":{"max_attempts":10}}}'
clerk config patch --json '{"auth_access_control":{"block_disposable_email_domains":true},"auth_attack_protection":{"user_lockout":{"max_attempts":10}}}' --yes
```

One JSON payload can touch unrelated subsystems; `config patch` merges into the existing configuration and only persists fields the schema recognizes.

### Confirm the diff

Snapshot again and diff:

```bash
clerk config pull --output config.after.json
diff config.before.json config.after.json
```

Expected output (abbreviated):

```
auth_access_control.block_disposable_email_domains: false → true
auth_attack_protection.user_lockout.max_attempts:   100 → 10
auth_passkey.used_for_sign_in:                       false → true
auth_username.used_for_sign_in:                      false → true
auth_username.used_for_sign_up:                      false → true
session:                                             null → { allowed_clock_skew: 5, claims: {}, lifetime: 7200 }
```

If your diff has a field listed in one of the patches but missing from the after snapshot, that field name is not in the current schema. Re-check `clerk config schema` and adjust.

> `clerk config put` replaces the entire configuration with the supplied JSON. Fields you omit are reset to defaults — which, for a live application, can silently log every user out or strip OAuth providers. Use `patch` for incremental changes and reserve `put` for scripted instance re-creation with a reviewed-to-completion payload.

Commit `config.before.json` and `config.after.json` (or their delta) to your repo. When the next engineer asks "when did we turn on passkeys?" the answer is in `git log`, not a dashboard's audit tab.

## Verify the config changes worked

Reload the dev server (`Ctrl-C`, `pnpm dev`). Visit `/` signed in, open the `<UserButton />` menu, and pick **Manage account**. The `<UserProfile />` component now exposes a **Passkeys** section with a **Register a passkey** button — that is patch 1 taking effect. The sign-in page now accepts a username field for new sign-ups (patch 2). Your session tokens now expire after two hours (patch 3 — open devtools and inspect the `__session` cookie's `exp` claim to confirm).

If any of these do not appear, run `clerk doctor` and re-diff the before/after snapshots. Silent schema strips are the most common reason a patch looks like it applied but did not. See the warning in Patch 1 for the canonical example.

## Inspect your instance with `clerk api`

`clerk api` is an HTTP client pre-authenticated to your linked instance. It covers both the Backend API (users, organizations, sessions, etc.) and the Platform API (accounts, applications, instances — for managing Clerk itself).

List available endpoints:

```bash
clerk api ls users
```

Output: the endpoints under `/users`, with HTTP method and path. The same `ls` works against any resource:

```bash
clerk api ls organizations
clerk api ls sessions
clerk api ls sign-in-tokens
```

Fetch an actual resource:

```bash
clerk api /users
clerk api /users/<user_id>
```

The first returns the user list as JSON (empty array until you sign up). The second returns one user record.

For the Platform API — the surface that manages your Clerk account itself rather than the users inside one instance — pass `--platform`:

```bash
clerk api --platform ls
clerk api --platform /platform/applications
```

> Platform API paths are prefixed with `/platform/...`, not `/accounts`. Calls like `clerk api --platform /accounts` return `clerk_key_invalid` because the routing table does not have an `/accounts` entry at the Platform level. Use `clerk api --platform ls` to browse the correct paths — `/platform/applications`, `/platform/instances`, and similar are the real endpoints.

Every `clerk api` call is a real API call against your real instance. Use `--instance dev` / `--instance prod` to target a specific instance if the app has both. Pipe the output through `jq` for quick filtering:

```bash
clerk api /users | jq '.[].email_addresses[0].email_address'
```

## CLI reference (quick skim)

The commands this article used, plus the handful you will reach for next:

| Command                                            | What it does                                                                     |
| -------------------------------------------------- | -------------------------------------------------------------------------------- |
| `clerk --version`                                  | Print CLI version.                                                               |
| `clerk update --yes`                               | Update to latest stable. `--channel canary` for pre-release.                     |
| `clerk auth login`                                 | Browser-based OAuth to Clerk. Stores tokens in OS keychain.                      |
| `clerk whoami`                                     | Print the authenticated user and currently-selected app.                         |
| `clerk apps list`                                  | List Clerk applications you have access to.                                      |
| `clerk apps create`                                | Create a new Clerk application without the dashboard.                            |
| `clerk init --starter --framework react --pm pnpm` | Scaffold a Vite + React + Clerk project.                                         |
| `clerk init`                                       | Same, but runs in an existing project directory (auto-detects framework).        |
| `clerk link --app app_xxx`                         | Associate the current project with a Clerk app.                                  |
| `clerk unlink`                                     | Clear the current project's application link.                                    |
| `clerk env pull`                                   | Write `.env.local` with the linked app's keys. `--instance prod` for production. |
| `clerk doctor`                                     | Pre-flight the local setup. `--spotlight` for bug-report formatting.             |
| `clerk config schema`                              | Print the configuration JSON schema.                                             |
| `clerk config pull --output <file>`                | Snapshot the instance configuration.                                             |
| `clerk config patch --dry-run --json '{...}'`      | Preview a config change.                                                         |
| `clerk config patch --json '{...}' --yes`          | Apply a config change.                                                           |
| `clerk config put`                                 | Replace the entire configuration (destructive — use sparingly).                  |
| `clerk api ls <resource>`                          | List endpoints under a resource.                                                 |
| `clerk api /<path>`                                | Call a Backend API endpoint.                                                     |
| `clerk api --platform /<path>`                     | Call a Platform API endpoint.                                                    |
| `clerk skill install -y`                           | Install Clerk-maintained agent skills globally.                                  |
| `clerk completion <shell>`                         | Generate shell completion for bash/zsh/fish.                                     |
| `clerk open`                                       | Open the Clerk Dashboard for the current app in your browser.                    |

Anything not listed here: run `clerk <subcommand> --help` or `clerk --help` for the full surface.

## Conclusion

You have now explored React SPA-specific authentication patterns and learned how to manage your Clerk instance configuration as code using the CLI. By treating your authentication setup as an auditable pipeline, you ensure a more robust and team-friendly development workflow.

## FAQ

## FAQ

### How do I verify Clerk tokens on my backend?

Call `useAuth().getToken()` on the client to get a short-lived JWT, send it as a `Bearer` header, and verify it on the backend with Clerk's server SDK or a JWKS lookup. Tokens default to a 60-second lifetime with auto-refresh. Client-side `useAuth()` is a rendering hint, not an authorization decision — only backend verification protects data.

## In this series

1. [Add Clerk authentication to a React app with the Clerk CLI](https://clerk.com/articles/add-clerk-authentication-to-a-react-app-with-the-clerk-cli.md)
2. **Add Clerk authentication to a React app with the Clerk CLI - Part 2** (you are here)
