Nov 05, 2025·7 min read

SameSite cookie settings: Secure, domain scope, and subdomains

SameSite cookie settings explained: fix Secure, HttpOnly, domain scoping, and subdomain behavior so logins work reliably in production.

SameSite cookie settings: Secure, domain scope, and subdomains

Why auth breaks in production when cookies are misconfigured

When login works on localhost but fails on your real domain, the problem is often not your password check or database. It's the cookie.

Local testing is forgiving: everything is on one host, you might be on plain HTTP, and redirects are simpler. In production, browsers enforce stricter rules. Small cookie details decide whether the browser will send your session back to the server.

The symptoms can look unrelated:

  • the login form submits, but you land back on the sign-in page
  • a redirect loop between /login and /app
  • sessions that disappear after a refresh
  • one browser works while another fails

Production environments add moving parts: HTTPS, CDNs or proxies, separate subdomains (like app.example.com and api.example.com), and redirects through third parties (OAuth, payment pages, email magic links). That's where SameSite, Secure, and cookie scoping start to matter.

A cookie isn't just "a session token." It's also a set of rules that tells the browser when to send that token back.

Misconfiguration usually breaks auth in a few predictable ways:

  • The browser never sends the cookie on the request that creates or checks the session (often SameSite or domain scope).
  • The browser rejects the cookie because Secure isn't set when it should be.
  • The cookie is scoped to the wrong Domain or Path, so it doesn't reach the routes that need it.
  • Two cookies with the same name exist (old and new, staging and prod), and the browser sends a different one than you expect.

Example: everything works on http://localhost:3000, but after deploying to app.yourdomain.com, your API sets a host-only session cookie for api.yourdomain.com. Your frontend never sends it, so every page load looks logged out.

Most "logged in" experiences still rely on a cookie that represents your session. After you enter your password, the server sends a cookie in the response and the browser stores it. On later requests, the browser attaches that cookie automatically so the server can recognize you.

A lot of confusion comes from mixing three related ideas:

  • Session cookie: an identifier the server looks up (often stored in a database or cache).
  • Access token: a short-lived credential (often used with APIs).
  • Refresh token: a longer-lived credential used to get a new access token.

Production auth bugs often show up when an app expects one approach but implements another. For example, the frontend expects a cookie-based session, but the backend issues tokens intended for an Authorization header.

For cookie-based auth, the attributes matter as much as the value:

  • SameSite: controls when cookies are sent in cross-site contexts.
  • Secure: cookie is only sent over HTTPS.
  • HttpOnly: blocks JavaScript from reading the cookie.
  • Domain: which hostnames can receive it.
  • Path: which URL paths can receive it.

Browsers don't always send cookies. They only send them when domain and path match, the connection satisfies Secure, and the request context passes SameSite rules.

A classic production-only failure: locally everything is localhost, but in production the frontend is on https://app.example.com and the API is on https://api.example.com. If Domain or SameSite is even slightly wrong, you get a login loop.

SameSite: what it does and when it blocks your login

SameSite is a cookie flag that tells the browser when it's allowed to send your session cookie. Many "works locally, breaks in production" issues come from SameSite settings that don't match the real sign-in flow.

Lax vs Strict vs None (plain terms)

SameSite=Strict is the most locked down. The cookie is only sent in a first-party context. If a user arrives from another site, the browser may withhold the cookie.

SameSite=Lax is the default that works for many apps. The cookie is typically sent on normal top-level navigations (like clicking a link), but not on most cross-site background requests.

SameSite=None means "allow this cookie in cross-site contexts." Modern browsers require Secure when you use None, or they'll drop the cookie.

What commonly triggers breakage

SameSite that's too strict often breaks:

  • OAuth logins (Google, GitHub) where the user is redirected back
  • embedded apps (your app inside an iframe on another domain)
  • flows where the browser treats your frontend and API as cross-site

Browser defaults also changed over time. Cookies without an explicit SameSite may be treated as Lax, and cookies set to None without Secure are commonly rejected.

When do you actually need SameSite=None? Use it only when the session cookie must be sent in a cross-site context (iframes, true cross-site setups, some redirect-based flows). If everything is same-site, Lax is often enough and reduces risk.

Secure cookies and HTTPS: the most common production mismatch

A cookie marked Secure is only sent over HTTPS. That sounds simple, but it's a top reason logins work locally and then fail after deployment.

One rule catches people off guard: if you use SameSite=None, modern browsers require the cookie to also be Secure. Without it, the browser can silently drop the cookie.

Local development is where teams get misled. Many apps run on http://localhost, so Secure is skipped to make testing easy. Then production runs on HTTPS, someone flips SameSite=None for a redirect or embedded flow, and the cookie never sticks.

Proxies and CDNs can make this worse. Your browser is on HTTPS, but your app server might see the incoming request as HTTP because TLS ends at the proxy. If the app decides "I'm on HTTP," it may refuse to set Secure, or it might generate redirects that bounce between HTTP and HTTPS.

If cookies disappear only on the live site, check:

  • In DevTools, confirm the auth cookie shows Secure and the request URL is https://...
  • Look for warnings like "cookie was blocked because it had SameSite=None without Secure"
  • Verify proxy headers (often X-Forwarded-Proto: https) and that your framework is configured to trust the proxy
  • Make sure users can't hit an HTTP version of the site
  • Confirm the cookie is set on the final domain after redirects, not an intermediate hostname

If your session cookie is readable in JavaScript, any XSS bug can turn into a full account takeover. That's why HttpOnly matters. With HttpOnly, the browser still sends the cookie on requests, but document.cookie can't read it.

Storing auth in a JS-readable cookie often feels convenient for an SPA, but it usually adds risk without real benefit. If the frontend needs to know whether the user is signed in, call a lightweight endpoint like /me and let the server decide based on the cookie.

CSRF is the other half of this story. Cookies are sent automatically, so a malicious site can try to trigger requests from a logged-in browser. SameSite helps, but state-changing requests still need CSRF protection.

Practical rules that keep UX simple while raising the bar:

  • Keep the session token only in an HttpOnly cookie (not localStorage).
  • Use CSRF protection for state-changing requests.
  • Scope cookies tightly (host and path) so fewer requests carry your session.

One trade-off: if you use cookies for API auth, your SPA can't attach them manually. It relies on the browser, so CORS and credentials settings must be correct.

Domain and Path scoping: stop cookies from going to the wrong place

Fix SameSite the right way
We’ll check your redirects, OAuth callbacks, and cookie flags for production.

Cookie bugs often look like "random logouts" or "it works locally but not after deploy." Domain and Path decide where the browser will send your auth cookie. If they don't match your real URLs, your server never sees the cookie and treats the user as logged out.

Even perfect SameSite settings can't help if the cookie isn't sent at all.

Host-only vs Domain cookies

If you don't set the Domain attribute, you get a host-only cookie. It's locked to the exact host that set it, like app.example.com.

If you set Domain=.example.com, the cookie becomes available to subdomains like app.example.com and api.example.com. That can be useful, but it also widens where the cookie can be sent and increases the chance of one subdomain overwriting another cookie with the same name.

Path scoping in multi-app setups

Path is the URL "folder" where the cookie is valid. Path=/ sends the cookie to every path on that host. Path=/app sends it only to /app and below.

A common mistake is setting the cookie to Path=/api because it was created on an API route, then wondering why pages under /app can't stay logged in.

Quick checks that prevent many production-only auth issues:

  • Pick one canonical host (www or non-www) and stick to it.
  • If you need shared auth across subdomains, use a parent domain cookie. Otherwise, leave Domain unset.
  • Use Path=/ unless you have a clear reason to isolate cookies.
  • Avoid reusing the same cookie name across different apps with different scopes.
  • Verify which host actually sets the cookie after redirects.

A common setup is app.example.com for the frontend and api.example.com for the backend. It looks simple, but cookie rules can shift between local dev and production once HTTPS, proxies, and real domains are involved.

If a cookie is host-only, a cookie set by api.example.com will not be sent to app.example.com, and vice versa. If you truly need one cookie to work across both, you typically scope it to the parent domain.

Before sharing cookies across subdomains, confirm:

  • Do you actually need the same cookie on both subdomains, or can the API be the only place that sets and reads it?
  • Are you using HTTPS end to end, including behind a load balancer or CDN?
  • Do your SameSite settings match how the browser classifies your requests (same-site vs cross-site)?

Also remember: CORS and cookies must match. Even if the cookie is correctly scoped, the browser won't attach it to API requests unless your fetch/XHR uses credentials, and the API allows credentials with an explicit origin.

Sharing cookies across subdomains isn't always worth it. A broad domain cookie increases where that cookie can be sent. Keep auth cookies as tight as possible and widen scope only when there's a clear need.

Clean up flaky authentication
Replace messy session code with clear flows that don’t break on refresh.

A safe cookie setup starts with your real user journey, not a default framework setting. What works on localhost can fail after deployment because production adds HTTPS, redirects, and separate hosts.

A practical checklist

Map your login flow end to end, then make the cookie match it:

  • Map the real login flow (including redirects). Write down every hop: app, auth provider, callback URL, dashboard.
  • Decide where the session should live. Pick one place to set and read the session cookie consistently (often the API).
  • Choose SameSite based on that flow. Use None only if you truly need cross-site cookie sending. Otherwise, Lax is often enough.
  • Enforce HTTPS and set Secure and HttpOnly. If you use SameSite=None, you must use Secure.
  • Set Domain and Path intentionally. Widen scope only when needed. Keep Path=/ unless you have a clear reason to limit it.

Validate on the real production domain in an incognito window: log in, hard refresh, open a new tab, and confirm the cookie is present and sent on the requests that matter.

Common mistakes that cause hard-to-debug auth bugs

Cookie bugs feel random because they often appear only after deployment. Small differences (HTTPS, a real domain, a proxy, a separate API host) make browser rules matter.

The mistakes behind most login loops and "logged out after refresh" reports:

  • Setting SameSite=None but forgetting Secure (the browser may drop the cookie).
  • Setting Domain too broad and unintentionally sharing cookies with other subdomains.
  • Mixing HTTP and HTTPS behind a proxy, so the app sets cookies or redirects based on the wrong scheme.
  • Trusting localhost behavior, which doesn't match real domains.
  • Treating CORS as the only issue. CORS can allow a request, but cookies can still be blocked by scope or SameSite.

Example: you deploy app.example.com (frontend) and api.example.com (backend). You update SameSite to allow cross-site behavior but forget Secure. In production, the browser drops the session cookie and every refresh looks like a new user.

Quick checks before you ship (and when you get a bug report)

Most cookie bugs become obvious once you check three places: the browser's cookie storage, the network request, and the server's reason for rejecting the session.

In DevTools (Application/Storage), click the cookie and verify:

  • Is the cookie present after login, and does it persist after refresh?
  • Are SameSite, Secure, HttpOnly, Domain, and Path what you expect?
  • Does Expires or Max-Age look reasonable?
  • Are you accidentally setting two cookies with the same name on different domains?
  • Is the cookie being set by the host you think (app vs api)?

Then check the Network tab. Inspect an authenticated request (like GET /me). If the cookie exists in storage but isn't on the request, either the attributes are blocking it or the request is going to a different host than you think.

Finally, verify HTTPS end to end. If TLS ends at a load balancer but proxy headers are missing, the app may set non-Secure cookies or generate redirects that break the session.

When you can, log a clear server-side reason for session rejection: missing cookie, expired session, invalid signature, CSRF failure, and so on.

Example: login loop after deployment due to SameSite and domain

Ship with fewer surprises
Get a clear list of auth and cookie fixes before your next release.

A common story: OAuth login works on localhost, then breaks after you deploy to https://app.example.com. Users click "Continue with Google," get redirected back, briefly look logged in, then bounce back to the login page. Repeatedly.

What's happening is usually a cookie mismatch across the OAuth redirect and your subdomains.

Locally, your app and API might both be on localhost, so the browser sends the session cookie everywhere. After deploy, the flow often looks like this:

  1. The OAuth provider redirects the user to https://app.example.com/auth/callback.

  2. The callback sets a session cookie, but it's host-only for app.example.com (no Domain attribute).

  3. The frontend calls https://api.example.com/me to fetch the logged-in user.

  4. The browser doesn't send the cookie to api.example.com, so the API returns 401. The app interprets that as "not logged in" and restarts the login flow.

SameSite can make it worse. If the cookie used during the OAuth callback is SameSite=Strict, the browser may not send it on the cross-site redirect back from the provider, so your server can't match state and the flow fails.

A practical fix plan:

  • Set Secure=true in production (especially if you use SameSite=None).
  • Use SameSite=Lax for typical login sessions and OAuth state cookies (it usually survives top-level redirects).
  • If the API truly needs the same cookie, scope it to the parent domain and keep Path consistent.

To confirm the fix without guessing:

  • Check the cookie entry for Domain, Path, SameSite, and Secure.
  • On the failing API request, verify whether the Cookie header is present.
  • Look for "blocked" notes in the cookie panel (browsers often explain why a cookie was rejected).

Next steps: lock it in and get help if it's still flaky

Once you've found a cookie setup that works, write it down as a rule, not a guess: the exact values for SameSite, Secure, HttpOnly, Domain, and Path, plus what changes between local, staging, and production.

Keep a tiny test plan you can repeat before releases: login, refresh, new tab, incognito window, and a second browser (Chrome and Safari usually reveal differences fast).

If you're dealing with an AI-generated prototype that breaks once it's on real domains, it's often one of a few repeat offenders: missing Secure, mismatched Domain/Path, cookies set on the wrong host after redirects, or cross-site behavior the browser blocks.

If you want a second set of eyes, FixMyMess (fixmymess.ai) focuses on taking broken AI-generated apps and making them production-ready, including diagnosing cookie and auth flows end to end. A quick audit is often enough to pinpoint the exact attribute or redirect step that breaks the session.

FAQ

Why does login work on localhost but fail after I deploy?

Usually the browser isn’t sending back the session cookie on your production domain. On localhost everything shares the same host, but in production HTTPS, subdomains, and redirects activate stricter rules, so a slightly wrong SameSite, Secure, Domain, or Path can make the server think you’re not logged in.

What causes the “/login” ↔ “/app” redirect loop?

A redirect loop often means the app sets a cookie during login, but the next request that checks the session doesn’t include that cookie. The most common causes are a cookie scoped to the wrong host (app vs API subdomain), SameSite blocking it during a redirect-based flow, or the cookie being dropped because it uses SameSite=None without Secure.

What SameSite setting should I use for most auth cookies?

Start with SameSite=Lax for a typical web app login session because it usually survives normal top-level navigations and many OAuth redirects. Use SameSite=None only when you truly need cookies in cross-site contexts (like iframes or a genuine cross-site setup), and only with Secure enabled.

Why does SameSite=None require Secure?

Modern browsers reject SameSite=None cookies unless they also have Secure, meaning they must be sent only over HTTPS. When you forget Secure, the cookie may appear to be set by your server, but the browser silently discards it, so every refresh looks like a logged-out user.

Why do Secure cookies break behind a CDN or reverse proxy?

Secure means the cookie is only sent over HTTPS. In production that’s what you want, but proxies can confuse your app into thinking the request is HTTP, which can change how cookies and redirects are generated. The fix is usually to enforce HTTPS and ensure your backend correctly trusts proxy headers so it knows the original scheme was HTTPS.

What’s the difference between a host-only cookie and a Domain cookie?

A host-only cookie (no Domain attribute) is only sent to the exact host that set it, like api.example.com. A domain cookie (like .example.com) can be sent to multiple subdomains. If your frontend is on app.example.com and the cookie is host-only for api.example.com, the frontend won’t send it on requests to app.example.com, which commonly creates “logged out after refresh” behavior.

How can the cookie Path make me randomly “lose” my session?

The Path attribute restricts where the cookie is sent on the same host. If you accidentally set Path=/api, then pages under /app won’t receive the cookie even though they’re on the same domain, so session checks fail. For most auth cookies, Path=/ is the simplest default unless you have a clear reason to isolate it.

Should my session cookie be HttpOnly, and will that break my SPA?

Use HttpOnly so JavaScript can’t read the session cookie, which reduces the damage from XSS. If your frontend needs to know whether the user is logged in, rely on a server endpoint (like “who am I”) instead of reading a token from the browser. This keeps the login state accurate without exposing secrets to client-side code.

How do I quickly debug whether the cookie is being set and sent?

First, check whether the cookie exists in the browser after login and whether it persists after a hard refresh. Next, inspect an authenticated request in the Network tab and confirm the Cookie header is present. If the cookie is stored but not sent, it’s almost always SameSite, Secure, Domain, Path, or a request going to a different host than you think.

My AI-generated prototype’s auth is flaky in production—what should I do next?

If you inherited an AI-generated app, cookie and auth settings are often inconsistent between local and production, especially with subdomains, OAuth callbacks, and proxy setups. The fastest path is a focused audit of the real login flow on your live domain to pinpoint the exact attribute or redirect step that drops the session. If you want, FixMyMess can review the codebase and repair the auth and cookie setup so it works reliably in production, often within 48–72 hours.