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

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

**How do I wire up Clerk UI components and configure settings in TanStack Start?**

This is part 2 of a two-part series on adding Clerk authentication to a TanStack Start app using the Clerk CLI. This part focuses on wiring up the landing page UI, starting the dev server, and performing operational tasks using the Clerk CLI such as configuring instance settings and querying the instance via `clerk api`.

## Wire up the landing page

`clerk init` deliberately leaves `src/routes/index.tsx` alone — it's your landing page, and the CLI doesn't make assumptions about your layout. That means out of the box the app has no sign-in affordance on the home page. Add one:

```tsx
import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/tanstack-react-start'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({ component: Home })

function Home() {
  return (
    <div className="p-8">
      <header className="mb-8 flex items-center justify-end gap-3">
        <Show when="signed-out">
          <SignInButton mode="modal" />
          <SignUpButton mode="modal" />
        </Show>
        <Show when="signed-in">
          <UserButton />
        </Show>
      </header>
      <h1 className="text-4xl font-bold">Welcome to TanStack Start</h1>
      <p className="mt-4 text-lg">
        Edit `src/routes/index.tsx` to get started.
      </p>
    </div>
  )
}
```

> Clerk Core 3 (released [2026-03-03](https://clerk.com/changelog/2026-03-03-core-3.md)) removed `<SignedIn>`, `<SignedOut>`, and `<Protect>` and replaced them with a single `<Show>` component. Any older TanStack Start tutorial you find on the internet still showing `<SignedIn>` / `<SignedOut>` is pre-Core-3 — port it. The [Core 3 upgrade guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md) has a `npx @clerk/upgrade` codemod that handles most call sites automatically.

This is the minimum viable landing page. The `mode="modal"` prop opens the sign-in/sign-up components in a modal rather than routing to `/sign-in` or `/sign-up`. Drop `mode="modal"` if you prefer full-page navigation to the catch-all routes `clerk init` generated.

## Start the dev server and sign up a test user

```bash
pnpm dev
```

Open `http://localhost:3000`. You should see the "Welcome to TanStack Start" header with **Sign in** / **Sign up** buttons in the top right. Click **Sign up**, run through the flow, and you'll land back on the home page with a `<UserButton />` in place of the sign-in/sign-up pair.

If the buttons don't render, two things to check before anything else:

1. **Restart the dev server.** Vite caches aggressively and new env vars don't always hot-reload.
2. **`clerk doctor`.** Missing publishable key is the single most common cause, and doctor catches it in one shot.

## Configure Clerk as code: `clerk config`

`clerk config` treats your Clerk instance configuration as data. You can pull the current state, diff it, patch fields, and audit the result. It's the same surface whether you're on dev or prod — add `--instance prod` when you're ready to push changes upstream.

Start by inspecting the schema:

```bash
clerk config schema
clerk config schema --keys auth_passkey session auth_attack_protection
```

`clerk config schema` prints the entire config surface. `--keys` scopes it — useful when you know the key names and just want the shape.

Pull the current config as a baseline:

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

Four patches follow. Run each `--dry-run` first; the dry run prints the exact shape that would be sent without making changes. Drop `--dry-run` and add `--yes` to commit.

**Patch 1: block disposable email domains** (no paid-plan gate):

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

**Patch 2: tighten lockout to 10 failed attempts** (no paid-plan gate — [the default is 100](https://clerk.com/docs/guides/secure/user-lockout.md)):

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

**Patch 3: enable passkeys as a sign-in factor** (paid plan required):

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

> [Passkeys](https://clerk.com/glossary.md#passkeys) can be **registered** on any plan; using them as a **sign-in factor** is a paid-plan feature. The `used_for_sign_in: true` flip is what unlocks the sign-in affordance in the `<SignIn />` component. See [pricing](https://clerk.com/pricing).

**Patch 4: tune session config** (paid plan required):

```bash
clerk config patch --dry-run --json '{"session":{"allowed_clock_skew":5,"claims":{},"lifetime":3600}}'
clerk config patch --json '{"session":{"allowed_clock_skew":5,"claims":{},"lifetime":3600}}' --yes
```

`allowed_clock_skew` tolerates small time drift between client and server (seconds). `claims` is where you inject custom [session](https://clerk.com/glossary.md#session) claims. `lifetime` is the session token lifetime in seconds (3600 = 1 hour).

> Session config changes are gated on a paid plan. If you hit a 403 on this patch, that's the plan gate — not a CLI bug.

Pull the new config and diff it:

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

You should see four blocks: `block_disposable_email_domains` flipped, `max_attempts` dropped from 100 to 10, `used_for_sign_in` flipped to `true`, and a full `session` object materialized (it was `null` before Patch 4).

> `clerk config put` replaces your entire config with the contents of a JSON file. It's destructive — use `patch` for day-to-day changes. `put` is for bootstrapping a new environment from a template, not for tuning.

## Verify the config changes worked

Reload the app and walk the sign-up flow again. A few things to confirm:

- **Disposable-email blocking.** Try signing up with a `@mailinator.com` address. The signup should be rejected at the email step.
- **Lockout.** Fail sign-in 10 times on purpose. The account should lock.
- **Passkey sign-in.** Sign in with your test user, open `<UserProfile />` (add a `/user` route rendering `<UserProfile />` if you don't have one yet), go to the **Security** tab, and register a passkey. Sign out, then sign in again — "Continue with passkey" should appear above the email field. Validated with platform authenticators (Touch ID, Windows Hello) and Bitwarden.
- **Session lifetime.** Signed-in sessions now expire after 1 hour instead of the default. Easy to verify in a long-running tab; easy to forget in dev unless you leave one open.

## Inspect your instance with `clerk api`

`clerk api` is a direct wrapper around the Clerk Backend API and (with `--platform`) the Clerk Platform API. It authenticates with your linked app's keys, so you don't need to craft curl commands or copy `CLERK_SECRET_KEY` into Postman.

List available endpoints and commands:

```bash
clerk api ls
clerk api ls users
```

List users:

```bash
clerk api /users
clerk api /users?limit=5
```

Fetch a single user:

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

The Platform API (cross-instance application management) lives behind `--platform`. The CLI auto-prepends `/v1` and points at the Platform API host (`api.clerk.com`). Platform resources are namespaced under `/platform/`, so the full path for listing applications is `/platform/applications` — the CLI resolves this to `api.clerk.com/v1/platform/applications`:

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

Backend API calls (no `--platform`) hit `api.clerk.dev` instead, and their paths do not need the `/platform/` prefix — `/users` resolves to `api.clerk.dev/v1/users`, `/organizations` to `api.clerk.dev/v1/organizations`.

> If a `--platform` call returns `clerk_key_invalid`, the usual cause is a missing auth token rather than a bad path — `clerk api --platform` uses the OAuth token from `clerk auth login`, not `CLERK_SECRET_KEY`. Re-run `clerk auth login` if the token has expired. If you get `404`, double-check the path against the Platform API spec: Platform resources need the `/platform/` segment, Backend resources do not.

`clerk api` isn't a replacement for the full Backend SDK in application code — it's for one-off inspection, ops, and scripts.

## Appendix: what `clerk init` wrote for you

If you ever need to reproduce `clerk init`'s output by hand — porting to a non-supported framework, or just curious — here's the exact surface. Validated against TanStack Start 1.167.42 + `@clerk/tanstack-react-start@1.1.5`.

**`package.json`** — one dependency added:

```json
{
  "dependencies": {
    "@clerk/tanstack-react-start": "^1.1.5"
  }
}
```

**`src/routes/__root.tsx`** — existing content wrapped in `<ClerkProvider>` (reformatted by `pnpm format`):

```tsx
import { ClerkProvider } from '@clerk/tanstack-react-start'
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'

import appCss from '../styles.css?url'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { title: 'TanStack Start Starter' },
    ],
    links: [{ rel: 'stylesheet', href: appCss }],
  }),
  shellComponent: RootDocument,
})

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <ClerkProvider>
          {children}
          <TanStackDevtools
            config={{ position: 'bottom-right' }}
            plugins={[
              {
                name: 'Tanstack Router',
                render: <TanStackRouterDevtoolsPanel />,
              },
            ]}
          />
          <Scripts />
        </ClerkProvider>
      </body>
    </html>
  )
}
```

**`src/routes/sign-in.$.tsx`** (new) — catch-all route so `<SignIn />` handles any sub-path (Clerk uses this for flow steps like `/sign-in/factor-two`):

```tsx
import { SignIn } from '@clerk/tanstack-react-start'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/sign-in/$')({
  component: Page,
})

function Page() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn />
    </div>
  )
}
```

**`src/routes/sign-up.$.tsx`** (new) — mirror of sign-in for sign-up:

```tsx
import { SignUp } from '@clerk/tanstack-react-start'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/sign-up/$')({
  component: Page,
})

function Page() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUp />
    </div>
  )
}
```

**`src/start.ts`** (modified) — Clerk's request middleware slotted into TanStack Start's server instance (TanStack's scaffold creates this file; `clerk init` adds the `clerkMiddleware()` call and the `@clerk/tanstack-react-start/server` import):

```ts
import { clerkMiddleware } from '@clerk/tanstack-react-start/server'
import { createStart } from '@tanstack/react-start'

export const startInstance = createStart(() => {
  return {
    requestMiddleware: [clerkMiddleware()],
  }
})
```

This is the TanStack Start equivalent of Next's `proxy.ts` / middleware — the hook-point where Clerk resolves the session for every server-side request. `createStart` takes [a callback that returns the config](https://github.com/TanStack/router/blob/main/packages/start-client-core/src/createStart.ts), not a plain object. `clerkMiddleware()` populates the request context that `auth()` from `@clerk/tanstack-react-start/server` reads inside server functions and loaders, so you can gate them with `beforeLoad` or by checking `userId` from `await auth()` before returning data.

**`.env.local`** — the four route URL vars come from the framework scaffold step, the two key vars come from `clerk init`'s built-in `env pull` (real values land whenever you pre-link an app with `clerk link --app <id>` or pick an app at the interactive prompt):

```bash
VITE_CLERK_SIGN_IN_URL=/sign-in
VITE_CLERK_SIGN_UP_URL=/sign-up
VITE_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
VITE_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/

# Clerk
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```

What `clerk init` does **not** touch: `src/routes/index.tsx` (landing page), `vite.config.ts`, `src/router.tsx`, `src/styles.css`, `README.md`. That's why the "wire up the landing page" step exists — you're filling in the piece the CLI intentionally left under your control.

## CLI reference (quick skim)

Commands the article touched:

| Command                                       | What it does                                                                                                                 |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `clerk auth login`                            | OAuth login, persists to the OS-standard CLI config directory (override with `CLERK_CONFIG_DIR`).                            |
| `clerk whoami`                                | Show current user + linked app.                                                                                              |
| `clerk apps list`                             | List apps in your org (add `--json` for machine-readable output).                                                            |
| `clerk apps create <name>`                    | Create a new app from the CLI.                                                                                               |
| `clerk init --framework tanstack-start`       | Install SDK + wire providers/routes/middleware. Auto-detects framework/package manager when the flag is omitted.             |
| `clerk link --app app_xxx`                    | Point the CLI at an existing app (run before `clerk init` to skip the app-picker).                                           |
| `clerk unlink`                                | Unlink the current app (add `--yes` to skip the confirmation prompt).                                                        |
| `clerk env pull`                              | Write env vars to `.env.local` (or `.env.production.local` with `--instance prod`; target a specific app with `--app <id>`). |
| `clerk doctor`                                | Validate auth + linked app + env vars.                                                                                       |
| `clerk config schema`                         | Print the full config schema.                                                                                                |
| `clerk config pull --output file.json`        | Snapshot current config.                                                                                                     |
| `clerk config patch --json '...' --dry-run`   | Preview a partial update.                                                                                                    |
| `clerk config patch --json '...' --yes`       | Commit the update.                                                                                                           |
| `clerk config put --file file.json`           | **Destructive** — replace config from file.                                                                                  |
| `clerk api /users`                            | Query the Clerk Backend API at `api.clerk.dev` (CLI auto-prepends `/v1`).                                                    |
| `clerk api --platform /platform/applications` | Query the Platform API at `api.clerk.com` (CLI auto-prepends `/v1`; Platform resources live under `/platform/`).             |
| `clerk completion zsh`                        | Print shell completion (pipe to your completion dir).                                                                        |
| `npx skills add clerk/skills`                 | Install/refresh the Clerk agent skills. Run inside `clerk init` or manually.                                                 |

Flags worth memorizing:

- `--yes` / `-y` — non-interactive; accept defaults.
- `--dry-run` — print the operation without executing (`config patch`, `config put`, `api`).
- `--instance prod` — target production instead of dev.
- `--app app_xxx` — scope the command to a specific app (valid on `link`, `env pull`, `config *`, `api` — **not** on `init`).
- `--no-skills` — skip the agent-skills prompt inside `clerk init`.

To upgrade the CLI itself, rerun the installer (`curl -fsSL https://clerk.com/install | sh`, `clerk update`, or `npm install -g clerk@latest`). There is no `clerk update` subcommand in 1.0.2.

Full reference: [Clerk CLI docs](https://clerk.com/docs/cli.md).

## Further reading

- [TanStack Start quickstart — Clerk docs](https://clerk.com/docs/tanstack-react-start/getting-started/quickstart.md)
- [Clerk CLI reference](https://clerk.com/docs/cli.md)
- [How Clerk works — overview](https://clerk.com/docs/guides/how-clerk-works/overview.md)
- [Core 3 upgrade guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md)
- [TanStack Start authentication guide](https://tanstack.com/start/latest/docs/framework/react/guide/authentication)
- [Clerk CLI source — GitHub](https://github.com/clerk/cli)

## Conclusion

In this series, we used the Clerk CLI to add authentication to a TanStack Start application. We covered scaffolding the app, running `clerk init`, pulling environment variables, verifying the setup, adding the UI components, and managing Clerk instance configurations as code.

## FAQ

## FAQ

### Do passkeys work in TanStack Start?

Yes — the passkey flow runs in Clerk's components, so it's identical across SDKs. [Register passkeys](https://clerk.com/glossary.md#passkeys) in `<UserProfile />` on any plan; using them as a sign-in factor (the `used_for_sign_in: true` flip) requires a paid plan.

### Can I edit the files clerk init generated, or keep the CLI in the loop?

Edit them freely — the output is regular TypeScript with no special markers and no ejection step. Rewrite the `<ClerkProvider>` placement, catch-all routes, or `start.ts` hook however you like; the full SDK surface is in [the TanStack Start quickstart](https://clerk.com/docs/tanstack-react-start/getting-started/quickstart.md) and on [GitHub](https://github.com/clerk/javascript). The CLI handles initial wire-up; the code afterward is yours.

## In this series

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