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.

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.

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

// 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:

// 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.
  • Get the session right before the request. A token saved earlier may have expired; see JWT expired.
  • For cross-origin cookies, set credentials: "include" on fetch, and on the server allow your exact origin (not *) with credentials.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us