afterbuild/ops
ERR-213/Auth · CORS
ERR-213
Set-Cookie header ignored — cookie not stored after login

appears when:When the frontend and API are on different registrable domains and the cookie is sent with SameSite=Lax (the default) or Secure=false

Cookies not set cross-domain in production

Modern browsers silently drop cross-site cookies that are not SameSite=None and Secure. The server thinks the cookie was set. The browser never stored it.

Last updated 17 April 2026 · 7 min read · By Hyder Shah
Direct answer
Cross-site cookies require three things together: Set-Cookie: ...; SameSite=None; Secure on the server response, credentials: "include" on every client fetch, and Access-Control-Allow-Credentials: true with an explicit (non-wildcard) origin on the CORS response. Missing any of the three and the browser drops the cookie silently.

Quick fix for cookies not set cross-domain

app/api/auth/login/route.ts
typescript
01// app/api/auth/login/route.ts — set cross-site cookie correctly02import { cookies } from "next/headers";03 04export async function POST(req: Request) {05  const { email, password } = await req.json();06  const session = await authenticate(email, password);07 08  const store = await cookies();09  store.set("session", session.token, {10    httpOnly: true,11    secure: true,         // required with SameSite=None12    sameSite: "none",     // allow cross-site sends13    path: "/",14    domain: ".example.com", // share across app.example.com + api.example.com15    maxAge: 60 * 60 * 24 * 7,16  });17 18  return Response.json({ ok: true });19}20 21// Client:22await fetch("https://api.example.com/auth/login", {23  method: "POST",24  credentials: "include", // required to send/receive cookies cross-site25  headers: { "Content-Type": "application/json" },26  body: JSON.stringify({ email, password }),27});
Correct cookie attributes for cross-site auth — SameSite=None, Secure, and credentials: include on the client

Deeper fixes when the quick fix fails

01 · Share a cookie across subdomains of the same registrable domain

lib/cookie.ts
typescript
01// Best option when you own both apex and subdomains02store.set("session", token, {03  httpOnly: true,04  secure: true,05  sameSite: "lax", // same-site — Lax is enough06  domain: ".example.com", // shared across app.example.com, api.example.com07  path: "/",08});09 10// Client fetch does not need credentials: include for same-site11// but including it is harmless.
Preferred architecture — keeps cookies first-party, avoids SameSite=None entirely

02 · CORS middleware with credentials

middleware.ts
typescript
01// middleware.ts — echo origin explicitly, never *02import { NextResponse, type NextRequest } from "next/server";03 04const ALLOWED = new Set([05  "https://app.example.com",06  "https://staging.example.com",07]);08 09export function middleware(req: NextRequest) {10  const origin = req.headers.get("origin") ?? "";11  const res = NextResponse.next();12 13  if (ALLOWED.has(origin)) {14    res.headers.set("Access-Control-Allow-Origin", origin);15    res.headers.set("Access-Control-Allow-Credentials", "true");16    res.headers.set("Vary", "Origin");17  }18 19  if (req.method === "OPTIONS") {20    res.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");21    res.headers.set("Access-Control-Allow-Headers", "content-type");22    return new NextResponse(null, { status: 204, headers: res.headers });23  }24 25  return res;26}27 28export const config = { matcher: "/api/:path*" };
Credentials require an explicit origin allowlist — wildcards are browser-rejected

03 · Playwright regression test

tests/cross-site-auth.spec.ts
typescript
01// tests/cross-site-auth.spec.ts02import { test, expect } from "@playwright/test";03 04test("cookie persists across cross-site pages", async ({ page, context }) => {05  await page.goto("https://app.example.com/login");06  await page.getByLabel("Email").fill("test@example.com");07  await page.getByLabel("Password").fill("test");08  await page.getByRole("button", { name: "Sign in" }).click();09 10  // Cookie should exist on the API domain11  const cookies = await context.cookies("https://api.example.com");12  const session = cookies.find((c) => c.name === "session");13  expect(session).toBeDefined();14  expect(session?.sameSite).toBe("None");15  expect(session?.secure).toBe(true);16});
Playwright in Chromium surfaces SameSite drops exactly like real Chrome

Why AI-built apps hit cookies not set cross-domain

The cookie spec evolved aggressively between 2020 and 2024. Chrome 80 (February 2020) flipped the default SameSite attribute from None to Lax. Before that change, any cookie set without an explicit SameSite was sent on every request, which created cross-site request forgery risks. After the change, cookies without an explicit attribute only go with same-site top-level navigations. Any cross-site fetch — including a login POST from app.example.com to api.example.com if those are separate registrable domains — drops the cookie.

AI-generated code trips on this because the scaffold was usually trained on examples where everything runs on localhost, which is same-origin. The scaffold sets sameSite: "lax" or omits the attribute entirely. In dev, the login works. In production on Vercel with a separate API domain, the server responds 200 OK with Set-Cookiein the header — but Chrome reads the header, evaluates the SameSite policy, and silently drops it. The app shows "logged in" briefly while the response is in memory, then every subsequent request is anon.

The second trap is the Secure attribute. Browsers refuse SameSite=None without Secure=true — the combination is treated as invalid and the cookie is dropped. Secure requires HTTPS. On Vercel preview deployments and production, everything is HTTPS, so this is fine. Some dev setups use HTTP for the API, which silently breaks the cookie even though the code looks correct.

The third trap is CORS. Setting the cookie attributes right is necessary but not sufficient. The browser also needs explicit permission to include credentials on the request and to accept credentials on the response. That means credentials: "include" on the fetch, Access-Control-Allow-Credentials: true on the server, and a specific origin in Access-Control-Allow-Origin — the wildcard * is rejected when credentials are involved.

cookies not set cross-domain by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × cookies not set cross-domain
BuilderFrequencyPattern
LovableEvery multi-domain deployDefault cookie attrs, no CORS credentials setup
Bolt.newCommonAccess-Control-Allow-Origin: * with credentials — browser rejects
CursorCommonsameSite: 'lax' hard-coded in template, never adjusted for cross-site
Base44SometimesSets Secure: false in dev and forgets to toggle it for prod
Replit AgentRareUses localStorage instead of cookies, opens XSS exfil surface

Related errors we fix

Stop cookies not set cross-domain recurring in AI-built apps

Still stuck with cookies not set cross-domain?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.
start the triage →

cookies not set cross-domain questions

Why do cookies work on localhost but break when deployed?+
On localhost, frontend and API usually run on the same host (both at 127.0.0.1). That is a same-origin request, and the default SameSite=Lax cookie attribute is fine. In production the frontend lives on app.example.com and the API on api.example.com or a totally different domain — that is cross-site. SameSite=Lax blocks cross-site cookies on fetch requests. You need SameSite=None plus Secure.
What is the difference between cross-site and cross-origin?+
Cross-origin means different scheme, host, or port: https://a.com and https://a.com:8080 are cross-origin. Cross-site means different registrable domain: a.com and b.com are cross-site, but app.a.com and api.a.com are same-site. Cookies care about site, not origin. SameSite=None is only needed for genuinely cross-site; subdomains of one registrable domain are same-site and work with SameSite=Lax.
Why do I need both SameSite=None and Secure?+
Browsers rejected SameSite=None without Secure starting in Chrome 80. The reasoning: SameSite=None opens the cookie to cross-site requests, which is only safe over HTTPS. If you set SameSite=None without Secure, the browser silently drops the Set-Cookie header and logs a warning in DevTools. You cannot have one without the other in any modern browser — no exceptions.
Do I still need CORS credentials settings?+
Yes. Cookie attributes are necessary but not sufficient. The browser also needs permission to send and receive credentials on cross-site fetch requests. Set fetch(url, { credentials: 'include' }) on the client, and on the server respond with Access-Control-Allow-Credentials: true plus an explicit Access-Control-Allow-Origin (not a wildcard — the browser rejects wildcard when credentials are involved).
How long does a cross-domain auth audit take?+
For a single frontend-API pair, diagnosing and fixing cookie and CORS setup is under 1 hour. Multi-client setups (web app, mobile webview, embedded dashboard) run 2-3 hours because each consumer has a different cookie policy. Our Auth, Database and Integrations service is fixed-fee and includes a regression test that posts login from a test origin and asserts the Set-Cookie header survives.
Next step

Ship the fix. Keep the fix.

Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

Sources