Fixing CORS and Cookie Failures Between a Multi-Subdomain Frontend and a Shared API
Serving the same app from several regional subdomains against one shared API? The moment you add authentication, CORS errors appear or the session cookie silently vanishes. Here's the real cause and the definitive fix.
The Problem
When a site serves the same app from multiple subdomains (say eg.example.com, us.example.com, in.example.com) but all of them call one shared API (say api.example.com), two things break the moment you add authentication: the browser blocks the API response with a CORS error, or the request succeeds but the session cookie never gets set, so the user looks logged out on every request.
Both symptoms come from the same root cause: browsers treat eg.example.com and api.example.com as two different origins, no matter how closely related they look, and the default same-origin security rules for both CORS and cookies apply in full.
Why It Happens
CORS: the wildcard trap
A common first attempt is setting Access-Control-Allow-Origin: * on the API. That works for plain requests but silently breaks the moment the frontend needs to send credentials (cookies or an Authorization header with credentials: "include") — the CORS spec explicitly forbids a wildcard origin when credentials are involved, so the browser blocks the response even though the server sent one.
Cookies: the domain scope trap
Even once CORS is fixed, a cookie set by api.example.com with no explicit Domain attribute is scoped to that exact host only — it never reaches eg.example.com or any other subdomain. And any cookie meant to cross subdomains needs SameSite=None; Secure, or the browser drops it as a third-party cookie in a cross-subdomain request.
The Fix
1. Echo the specific origin, never a wildcard, when credentials are involved
const allowedOrigins = [
"https://eg.example.com",
"https://us.example.com",
"https://in.example.com",
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
}
next();
});
Match against an explicit allowlist and reflect back the exact requesting origin — this satisfies the spec's ban on wildcard + credentials while still supporting every subdomain.
2. Scope cookies to the parent domain, not the API host
Set-Cookie: session=abc123; Domain=.example.com; Path=/; Secure; SameSite=None; HttpOnly
The leading dot (or, in modern browsers, an explicit Domain=example.com) makes the cookie valid for the parent domain and every subdomain under it, so a cookie set from api.example.com is readable by eg.example.com as well. SameSite=None is required for the cookie to be sent on the cross-subdomain request at all, and it forces Secure (HTTPS-only) as a prerequisite.
3. Set credentials: "include" on every frontend request
fetch("https://api.example.com/me", { credentials: "include" });
Without this, the browser won't send the cookie even if the server would happily accept it — this is opt-in on both the request and response side.
4. Watch preflight caching during rollout
Browsers cache a successful OPTIONS preflight response for the duration of Access-Control-Max-Age. If you change the allowed-origins list while debugging, a stale cached preflight can make it look like your fix didn't work — hard-refresh or clear the preflight cache before re-testing.
Why This Works
None of this weakens the security model — it just aligns it with the real trust boundary. The allowlist keeps CORS credentialed access limited to your own subdomains, never a true wildcard. The parent-domain cookie scope reflects that all the subdomains and the API genuinely belong to the same organization, which is exactly what Domain=.example.com is for.
Conclusion
CORS errors and vanishing sessions across subdomains are almost always the same two misconfigurations: a wildcard origin that can't carry credentials, and a cookie scoped too narrowly to cross subdomains. Fix the origin allowlist and the cookie's Domain/SameSite attributes together, and a shared API can serve every regional subdomain from one authenticated session.
