> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Federated with Clerk

> Clerk owns the account. Sign the Clerk userId on your server and UserKit trusts the claim.

Clerk is your source of truth. UserKit needs to know *which* of your users is on
the page, and it will not take the page's word for it — your server signs the
claim.

<Warning>
  **The identity secret never enters the bundle.** Never prefix it with
  `NEXT_PUBLIC_`, never render it into HTML, never send it to the browser to save
  a round trip. Whoever holds it can mint a **verified** session for any of your
  users.
</Warning>

The contract is the one on the
[federated identity](/en/customer-auth/federated) page and nothing here changes it:

```
hash = hex( HMAC-SHA256( identity_secret, external_id ) )
```

## What Clerk supplies

One value: `userId`, the `user_…` id Clerk's own webhooks name. That is the
`external_id`.

```ts lib/clerk.ts theme={null}
import "server-only";

import { auth } from "@clerk/nextjs/server";

export async function currentExternalId(): Promise<string | null> {
  const { userId } = await auth();
  return userId ?? null; // ← the external_id
}
```

`auth()` reads the session Clerk's middleware already verified. Decoding the
`__session` cookie yourself and signing its `sub` would be signing a value the
browser chose.

<Note>
  `auth()` needs `clerkMiddleware()` mounted, or it **throws** rather than
  answering with a null `userId`. That is the right failure: "I could not tell"
  must never be read as "signed out".
</Note>

## The server half

```ts app/api/userkit-boot/route.ts theme={null}
import { createHmac } from "node:crypto";
import { NextResponse } from "next/server";

import { currentExternalId } from "@/lib/clerk";

export const dynamic = "force-dynamic";

export async function GET() {
  const externalId = await currentExternalId();
  if (!externalId) {
    return NextResponse.json({ error: "not_signed_in" }, { status: 401, headers: NO_STORE });
  }

  const hash = createHmac("sha256", process.env.USERKIT_IDENTITY_SECRET!)
    .update(externalId)
    .digest("hex");

  return NextResponse.json({ external_id: externalId, hash }, { headers: NO_STORE });
}

// This body identifies one person. A CDN that kept it would hand the next
// visitor a proof of somebody else's identity.
const NO_STORE = { "Cache-Control": "no-store" };
```

## The client half

```tsx theme={null}
"use client";

import { createClient } from "@userkit/js";

const userkit = createClient({
  publishableKey: process.env.NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY!,
});

const response = await fetch("/api/userkit-boot", { cache: "no-store" });

if (response.status === 401) {
  // Still a visitor. An anonymous boot is where first-touch attribution lands.
  await userkit.boot();
} else {
  const { external_id, hash } = await response.json();
  await userkit.boot({ externalId: external_id, hash });
}
```

`getState().verified` is `true` only when the HMAC checked out. Signed in to
Clerk with `verified: false` means either the secret belongs to the other
environment or the message signed was not exactly the `external_id`.

## Configuration

| Variable                                                | Where it lives                                                                      |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `USERKIT_IDENTITY_SECRET`                               | **Server only.** `GET /v1/organization/environments/{id}/identity`, per environment |
| `NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY`                   | The browser. It identifies the environment and authenticates nothing                |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY` | Clerk's own pair — the second is server-only, exactly like UserKit's                |

## Clerk organizations are not the identity

`orgId` is a second dimension, and signing `userId + orgId` into one message
would make one person two contacts. UserKit's own team object is the
[customer](/en/concepts/customer-teams), resolved per request from
`X-Customer-Id` — map Clerk's organization onto that, not onto the
`external_id`.

## What does not travel

A Clerk user carries verified email addresses. They do not become identities in
UserKit: an email sent through `/v1/boot` is stored as an attribute, does not
resolve to an existing contact and does not become an identity edge. The HMAC
proves the `external_id` and only the `external_id`.

To prove an address, send a [magic link or an email
code](/en/customer-auth/email-proof) — the two flows that arrive in it.

<Card title="The full example" icon="github" href="https://github.com/userkithq/monorepo/tree/main/examples/federated-clerk">
  A runnable Next app with these files, an `.env.example` and the pieces this page
  leaves out.
</Card>
