fix(auth): isolate session cookies per app in local dev#712
Conversation
Two agent-native templates running side-by-side on localhost previously stomped on each other's `an_session` cookie (browsers scope cookies by host only, not host+port). In dev with no explicit APP_NAME, fall back to `npm_package_name` then `package.json:name` so each app gets its own `an_session_<slug>` cookie. Production is unchanged — switching the cookie name on a live deploy would invalidate existing sessions, and real deploys already set APP_NAME or COOKIE_DOMAIN. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Better Auth's cookiePrefix was hard-coded to "an", so two dev apps on localhost both wrote `an.session_token` / `an.session_data` / `an.csrf_token` and signed each other out. Use the BETTER_AUTH_COOKIE_PREFIX export from auth.ts so the prefix becomes `an_<slug>` when a dev slug is resolved, and stays `an` in production / workspace / cross-subdomain modes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five tests for COOKIE_NAME / BETTER_AUTH_COOKIE_PREFIX: - npm_package_name fallback in dev - production stays unsuffixed - explicit APP_NAME wins over the dev fallback - COOKIE_DOMAIN keeps the shared `an_session` cookie - workspace mode keeps `an_session_workspace` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for agent-native-scheduling ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for agent-native-meeting-notes ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for agent-native-voice ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for agent-native-images ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
✅ Deploy Preview for agent-native-design ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Review Agent skipped this PR — @zuchka doesn't have a Builder seat in this space. If you're @zuchka: you may already have a seat under a different GitHub account. Reconnect GitHub If you're an admin: Add @zuchka to this space |
steve8708
left a comment
There was a problem hiding this comment.
Thanks for chasing this down. I agree with the root cause here: localhost cookies are host-scoped, not port-scoped, so two standalone dev apps on localhost:* need distinct cookie names/prefixes.
I think we need one more pass before merging, though. The PR says production is unchanged, but BETTER_AUTH_COOKIE_PREFIX is derived from APP_NAME_SLUG whenever APP_NAME is set and neither workspace mode nor COOKIE_DOMAIN is set. Today Better Auth is hard-coded to cookiePrefix: "an", so any deployed standalone app with APP_NAME=mail / calendar / etc. would rename Better Auth cookies from an.session_token to an_mail.session_token. That invalidates existing Better Auth sessions and is a production behavior change.
I’d suggest separating the cookie/auth “realm” from APP_NAME, since APP_NAME is currently carrying too many meanings: DB env prefix, app identity, workspace child id, and local cookie namespace. The invariant should be explicit:
- standalone local dev with no shared realm: isolate cookies per app, e.g.
an_session_<app>and Better Auth prefixan_<app> - workspace mode: shared login, e.g.
an_session_workspaceand Better Auth prefixan COOKIE_DOMAINmode: shared cross-subdomain login, e.g.an_sessionand Better Auth prefixan- production standalone: keep Better Auth prefix
anunless we intentionally ship a session migration
Concretely, I’d put the namespace resolution in a small leaf helper used by both auth.ts and better-auth-instance.ts, so better-auth-instance.ts doesn’t need to import auth.ts:
const sharedRealm = hasCookieDomain || isWorkspaceMode;
const localIsolatedRealm = process.env.NODE_ENV !== "production" && !sharedRealm;
const slug = explicitAppSlug || (localIsolatedRealm ? packageNameSlug : "");
COOKIE_NAME = hasCookieDomain
? "an_session"
: isWorkspaceMode
? "an_session_workspace"
: slug
? `an_session_${slug}`
: "an_session";
BETTER_AUTH_COOKIE_PREFIX = localIsolatedRealm && slug ? `an_${slug}` : "an";Can we also add tests for the cases that previously regressed?
NODE_ENV=production,APP_NAME=mail, noCOOKIE_DOMAIN, no workspace:COOKIE_NAMEmay remain app-suffixed if that is intentional, but Better Auth prefix should stayanAGENT_NATIVE_WORKSPACE=1plusAPP_NAME=mail: sharedan_session_workspace, Better Auth prefixanCOOKIE_DOMAIN=.agent-native.complusAPP_NAME=mail: sharedan_session, Better Auth prefixan
One related thing to verify: workspace/shared-login only works if the shared realm also has the same Better Auth signing secret and DB. Workspace deploys/root .env appear designed for that, but local workspace dev can get weird if each app auto-generates its own .env.local BETTER_AUTH_SECRET. If that can still happen, we should either generate/read the secret at the workspace root in workspace mode or warn loudly when workspace mode has no shared BETTER_AUTH_SECRET.
Summary
localhost:<portA>andlocalhost:<portB>previously stomped on each other's session cookies, signing each other out. Browsers scope cookies by host only (RFC 6265 — not host+port), so both apps sharedan_sessionand Better Auth'san.*cookies on thelocalhostcookie jar.NODE_ENV !== "production"), the framework now derives a per-app cookie suffix fromnpm_package_namethenpackage.json:namewhenAPP_NAMEis unset. Templates running solo viapnpm devautomatically getan_session_<app>and Better Auth prefixan_<app>, so cookies no longer collide.COOKIE_NAME), so the fallback is gated behindNODE_ENV !== "production". Real deploys with explicitAPP_NAME,COOKIE_DOMAIN, or workspace mode behave exactly as before.Behavior matrix
COOKIE_NAMEAPP_NAMEunset,npm_package_name=mailan_session_mail(new)an_mail(new)APP_NAME=calendaran_session_calendaran_calendarCOOKIE_DOMAIN=.agent-native.coman_sessionanAGENT_NATIVE_WORKSPACE=1an_session_workspaceanNODE_ENV=production,APP_NAMEunsetan_sessionanTest plan
packages/core/src/server/auth.spec.tscover all five scenarios in the matrix (5/5 passing, full suite 100/100)pnpm --filter @agent-native/core typecheck— no errors inauth.tsorbetter-auth-instance.tscd templates/calendar && pnpm dev+cd templates/mail && pnpm devin separate terminals; sign in to both in the same browser; confirm distinct cookies (an_session_calendar,an_session_mail,an_calendar.session_token,an_mail.session_token) and that signing into one no longer kicks the other outan_session/an.*cookie names it did before (sanity check that no live sessions are invalidated)🤖 Generated with Claude Code