# Session missing in fetch requests to your API

> The browser is signed in, but your own API route or backend sees no user. Send the session in cookies or as a Bearer token, and build a per-request client on the server.

Source: https://supahost.app/kb/session-missing-in-fetch · Updated 2026-11-11

## What you see

The user is signed in on the page, but when the page calls your own API (a Next.js Route Handler, an Express server, a Python backend), the server finds no user:

```
AuthSessionMissingError: Auth session missing!
```

Or `supabase.auth.getUser()` on the server returns `user: null`, and queries return only rows that anonymous visitors can see.

## Why it happens

Your server only knows who the user is if the request carries the session. Common reasons it doesn't:

- **The session is in local storage.** A browser client from plain `@supabase/supabase-js` stores the session in `localStorage`, which is never sent to a server.
- **The request goes to another origin.** `fetch` doesn't send cookies to a different domain unless you set `credentials: "include"` and the server's CORS allows credentials.
- **Server code calls your own API.** A Server Component that does `fetch("/api/...")` doesn't forward the visitor's cookies.
- **The server client ignores the request.** It was created without reading the request's cookies or `Authorization` header.

## How to fix it

Pick the option that matches your setup.

**Option A: cookies, same site.** Use `@supabase/ssr` so the session lives in cookies: `createBrowserClient` in the browser and `createServerClient` (reading the request cookies) in your route. Same-origin `fetch` sends the cookies automatically. See [Server-side auth and SSR](/docs/auth/server-side).

**Option B: a Bearer token, any backend.** Send the access token in a header:

```ts
// Browser
const { data: { session } } = await supabase.auth.getSession();

const res = await fetch("https://api.invoices.example/invoices", {
  headers: { Authorization: `Bearer ${session?.access_token}` },
});
```

On the server, build a client for each request with that token, so Row Level Security applies as that user:

```ts
// Express
import express from "express";
import { createClient } from "@supabase/supabase-js";

const app = express();

app.get("/invoices", async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer /, "");
  if (!token) return res.status(401).json({ error: "Not signed in" });

  const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
    global: { headers: { Authorization: `Bearer ${token}` } },
    auth: { persistSession: false, autoRefreshToken: false },
  });

  const { data: { user }, error } = await supabase.auth.getUser(token);
  if (error || !user) return res.status(401).json({ error: "Not signed in" });

  const { data, error: queryError } = await supabase
    .from("invoices")
    .select("id, number, total, status");
  if (queryError) return res.status(500).json({ error: queryError.message });
  res.json(data);
});
```

Don't create one shared client for all requests and set the token on it. Concurrent requests would overwrite each other's user.

**Option C: skip the extra hop.** In a Server Component, query Supabase directly with the server client instead of calling your own API route.

## Checks

- The token you send is `session.access_token`, not the anon key. If you send the anon key, see [401 invalid claim: missing sub claim](/kb/invalid-claim-missing-sub).
- Get the session right before the request. A token saved earlier may have expired; see [JWT expired](/kb/jwt-expired).
- For cross-origin cookies, set `credentials: "include"` on `fetch`, and on the server allow your exact origin (not `*`) with credentials.

## Related

- [Server-side auth and SSR](/docs/auth/server-side)
- [Sessions, JWTs and refresh tokens](/docs/auth/sessions)
- [Use SupaHost from a Node.js server](/docs/getting-started/node-server)
- [JWT expired](/kb/jwt-expired)
