One login at app.fuyad.dev carries to admin.fuyad.dev through a cookie scoped to the parent domain.
A user who logs in at app.fuyad.dev should already be authenticated when they open admin.fuyad.dev. The whole trick is a session cookie scoped to the parent domain, marked httpOnly so no script can read it. BetterAuth handles the session, but the cookie attributes are where single sign-on across subdomains is won or lost.
Scope the cookie to the parent
A cookie set without a domain is bound to the exact host that set it, so app and admin would each get their own session. Setting the domain to .fuyad.dev makes the browser send it to every subdomain.
export const auth = betterAuth({
advanced: {
crossSubDomainCookies: { enabled: true, domain: ".fuyad.dev" },
cookies: { sessionToken: { attributes: { httpOnly: true, secure: true, sameSite: "lax" } } },
},
});httpOnly keeps the token out of JavaScript entirely, which closes the door on a whole class of token theft through XSS. secure means it only travels over HTTPS. sameSite: lax is the sweet spot here, since all the subdomains share a site.
One source of truth for sessions
Every subdomain validates against the same session store. The session id in the cookie is looked up server side on each request, so there is no token to forge and no per-app session to drift out of sync. Logging out anywhere deletes the session row, and the next request on any subdomain finds nothing and redirects to login.
const session = await auth.api.getSession({ headers: req.headers });
if (!session) return redirect("/login");SameSite and the cross-site case
Lax works because app and admin are the same site under one registrable domain. If I ever needed the session to ride along on a genuinely cross-site request, that would mean sameSite: none plus secure, and a careful look at CSRF. I avoid that until something actually requires it, because none is a much wider door.
Local development without HTTPS pain
secure cookies do not set over plain http, which makes local multi-subdomain testing annoying. I map app.localhost and admin.localhost in hosts and run a local TLS proxy so the cookie behaves exactly as it will in production. Testing SSO against localhost:3000 and localhost:3001 would prove nothing, because those are different origins on the same host, not subdomains.
The payoff is the experience users expect without thinking about it: log in once, move between the public app and the admin tools freely, and have a single logout end everything. The cookie attributes are four lines, but getting those four lines right is the entire feature.