> ## 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.

# Federado com Supabase

> O Supabase Auth é dono da conta. Assine o id do usuário no seu servidor e o UserKit confia.

O Supabase Auth é a sua fonte da verdade. O UserKit precisa saber *qual* dos seus
usuários está na página, e não vai aceitar a palavra da página — o seu servidor
assina a afirmação.

<Warning>
  **O identity secret nunca entra no bundle.** Nunca prefixe com `NEXT_PUBLIC_`,
  nunca renderize no HTML, nunca mande para o navegador para economizar uma ida e
  volta. Quem tem esse segredo emite sessão **verificada** para qualquer um dos
  seus usuários.
</Warning>

O contrato é o da página de
[identidade federada](/pt-br/customer-auth/federated), e nada aqui o altera:

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

## O que o Supabase fornece

Um valor: `user.id`, o uuid em `auth.users`. É esse o `external_id`.

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

import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function currentExternalId(): Promise<string | null> {
  const store = await cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => store.getAll(),
        setAll: () => {},
      },
    },
  );

  const { data, error } = await supabase.auth.getUser();
  if (error || !data.user) return null;
  return data.user.id; // ← o external_id
}
```

<Note>
  `getUser()`, não `getSession()`. `getSession()` decodifica o cookie que o
  navegador mandou — assinar o conteúdo dele seria assinar um valor que o navegador
  escolheu. `getUser()` pergunta ao Supabase.
</Note>

## A metade do servidor

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

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

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 });
}

// Este corpo identifica uma pessoa. Um CDN que o guardasse entregaria ao próximo
// visitante a prova da identidade de outra pessoa.
const NO_STORE = { "Cache-Control": "no-store" };
```

Deslogado responde `401`, e não um par vazio: "nenhum usuário" e "um usuário cujo
hash eu não consegui calcular" são situações diferentes, e o próximo passo de quem
chamou também é.

## A metade do cliente

```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) {
  // Continua sendo um visitante. O boot anônimo é onde a atribuição de primeiro
  // toque cai.
  await userkit.boot();
} else {
  const { external_id, hash } = await response.json();
  await userkit.boot({ externalId: external_id, hash });
}
```

`getState().verified` só é `true` quando o HMAC conferiu. Logado no Supabase com
`verified: false` significa que o segredo é do outro ambiente ou que a mensagem
assinada não era exatamente o `external_id`.

## Configuração

| Variável                                                    | Onde vive                                                                           |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `USERKIT_IDENTITY_SECRET`                                   | **Só no servidor.** `GET /v1/organization/environments/{id}/identity`, por ambiente |
| `NEXT_PUBLIC_USERKIT_PUBLISHABLE_KEY`                       | No navegador. Identifica o ambiente e não autentica nada                            |
| `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY` | O par público do próprio Supabase                                                   |

## O que não viaja

Um usuário do Supabase carrega `email` e `email_confirmed_at`. Nenhum dos dois
vira identidade no UserKit: um e-mail enviado pelo `/v1/boot` é guardado como
atributo, não resolve para um contato existente e não vira aresta de identidade. O
HMAC prova o `external_id` e só ele.

Para provar um endereço, mande um [magic link ou um código por
e-mail](/pt-br/customer-auth/email-proof) — os dois fluxos que chegam nele.

<Card title="O exemplo completo" icon="github" href="https://github.com/userkithq/monorepo/tree/main/examples/federated-supabase">
  Um app Next executável com esses arquivos, um `.env.example` e as partes que esta
  página deixa de fora.
</Card>
