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

# Per-user agents with Supabase Auth

> Pass authenticated end-user IDs from Supabase to Polpo for session filtering, analytics, and metering.

## What you'll build

An authenticated chat where every Supabase user has filterable Polpo sessions and analytics. Your Next.js app verifies the Supabase session on the server, then forwards the user's ID to Polpo on every chat call.

You get:

* Per-user session history and filtering
* Per-user analytics (filter dashboard by `user`)
* Audit trail (every session, task, run carries the user id)
* A stable external identifier for downstream metering

## Prerequisites

<Steps>
  <Step title="Supabase project with auth enabled">
    Email/password, OAuth, magic links — any Supabase auth method works. You only need the server-side helper to read the current user.
  </Step>

  <Step title="Polpo project + API key">
    Follow the [quickstart](/docs/quickstart) to create a project and grab a service-role API key (`sk_live_...`). Server-side only — never ship it to the browser.
  </Step>

  <Step title="Next.js 14+ App Router">
    Examples below use Route Handlers and Client Components. The same pattern works in Hono, Express, or any server you control.
  </Step>
</Steps>

## How it fits together

```
Browser ──▶ Next.js Route Handler ──▶ Polpo
              │  verify Supabase session
              ▼  forward user.id
           supabase.auth.getUser()
```

Supabase Auth lives in your app. Polpo never sees the JWT. Your server is the trust boundary: it verifies the session, then calls Polpo with your API key and a `user` field.

## Step 1 — Install

```bash theme={null}
pnpm add @polpo-ai/sdk@0.15.35 @supabase/ssr
```

## Step 2 — Verify the session and call Polpo

Server-side route handler. Reads the Supabase session via cookies, rejects unauthenticated requests, and streams the chat completion back to the browser with `user: user.id` attached.

```typescript app/api/chat/route.ts theme={null}
import { PolpoClient } from "@polpo-ai/sdk";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

const polpo = new PolpoClient({
  baseUrl: "https://{project}.polpo.cloud",
  apiKey: process.env.POLPO_API_KEY!,
});

export async function POST(req: Request) {
  const cookieStore = await cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: () => { /* route handlers don't write cookies */ },
      },
    },
  );

  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response("Unauthorized", { status: 401 });

  const { messages, agent } = await req.json();

  const stream = polpo.chatCompletionsStream({
    agent,
    messages,
    user: user.id,
  });

  return new Response(
    new ReadableStream({
      async start(controller) {
        const encoder = new TextEncoder();
        for await (const chunk of stream) {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
        }
        controller.enqueue(encoder.encode("data: [DONE]\n\n"));
        controller.close();
      },
    }),
    { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } },
  );
}
```

<Tip>
  `user.id` is a UUID that's stable for the lifetime of the Supabase user. Polpo treats it as an opaque string — pass anything that uniquely identifies the end-user (email, tenant id, internal user id) if you prefer.
</Tip>

## Step 3 — Stream the response in your client

Plain `fetch` + the standard SSE parsing pattern. Replace this with `@polpo-ai/react`'s `useChat` hook once you wire `PolpoProvider` against `/api/chat`.

```tsx app/chat/page.tsx theme={null}
"use client";

import { useState } from "react";

export default function ChatPage() {
  const [input, setInput] = useState("");
  const [reply, setReply] = useState("");

  async function send() {
    setReply("");
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        agent: "assistant",
        messages: [{ role: "user", content: input }],
      }),
    });
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buf = "";
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      const lines = buf.split("\n");
      buf = lines.pop() ?? "";
      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        const data = line.slice(6).trim();
        if (data === "[DONE]") return;
        try {
          const json = JSON.parse(data);
          const delta = json.choices?.[0]?.delta?.content ?? "";
          if (delta) setReply((r) => r + delta);
        } catch { /* keep buffering */ }
      }
    }
  }

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={send}>Send</button>
      <pre>{reply}</pre>
    </div>
  );
}
```

## Step 4 (optional) — List a user's past sessions

Polpo auto-creates a session per chat and tags it with the `user` field. You can list a single user's sessions from your server.

<Note>
  The typed `polpo.getSessions()` helper doesn't currently take a `user` filter parameter. Until it does, hit the underlying endpoint directly — `GET /v1/chat/sessions?user=<id>` does support the filter server-side. We'll add a typed param to the SDK in a later release.
</Note>

```typescript app/api/sessions/route.ts theme={null}
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function GET() {
  const cookieStore = await cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: () => {},
      },
    },
  );

  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response("Unauthorized", { status: 401 });

  const url = new URL("https://{project}.polpo.cloud/v1/chat/sessions");
  url.searchParams.set("user", user.id);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.POLPO_API_KEY!}` },
  });
  return new Response(res.body, {
    status: res.status,
    headers: { "Content-Type": "application/json" },
  });
}
```

## What this gives you

* **Per-user session history.** Sessions can be filtered by the exact `user.id` value.
* **Per-user analytics.** Filter completions, tasks, and runs by `user` in the dashboard.
* **Audit trail.** Every session, task, and run row stores the user id — useful for support, abuse review, and compliance.
* **Metering identifier.** The same `user` value can be correlated with your own billing and usage records.

## What we deliberately don't do

<Note>
  * **Polpo never verifies the Supabase JWT.** Your API key is the trust anchor. Verify the session in your server, then forward `user.id` — Polpo trusts the caller because the caller has the API key.
  * **We don't compete with Supabase Auth.** Bring your auth — we run the agents.
</Note>

## Troubleshooting

### `401 Unauthorized` from Polpo

Check `POLPO_API_KEY` is set in the server environment and not prefixed with `NEXT_PUBLIC_`. The SDK sends it as `Authorization: Bearer <key>`.

### `user` field shows up as `null` in the dashboard

The `user` field must be passed in the request **body**, not as a header. With the SDK, that's `polpo.chatCompletionsStream({ ..., user: user.id })`. With raw fetch, include `"user": "<id>"` as a top-level body field.

### Sessions aren't being scoped per user

Confirm both `@polpo-ai/sdk` and the target runtime are on `0.15.35`. Run `pnpm why @polpo-ai/sdk` to verify.

Shared and per-agent memory endpoints remain project-scoped; the `user` field does not isolate `/v1/memory` content. Keep authorization and tenant isolation in your application server.
