Cookies amongst multiple tenants, subdomains, and custom domains for a SaaS

Hey folks, I'm working on what is ultimately Squarespace for a niche, where users will get a unique subdomain, and can also add their own custom domain (so we have mycoolapp.com where i'll be serving the marketing, john.mycoolapp.com, and maryscoolapp.com). Looking for guidance on how to get it working with BetterAuth; my stack is SvelteKit, with Postgres + Drizzle (using the direct Postgres connector, though), with Directus as the CMS for end users. Currently I'm having an issue where the cookies are being set on the TLD and not working on the subdomains. I'm aware that there's an organisation plugin, but that doesn't look to have anything specific to sharing cookies between sites.
3 Replies
rhys
rhysOP17h ago
req headers for sign-up via email:
POST /api/auth/sign-up/email HTTP/2
Host: peggy.localhost:5173
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0
Accept: */*
Accept-Language: en-AU,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br, zstd
Referer: https://peggy.localhost:5173/signin
content-type: application/json
Content-Length: 186
Origin: https://peggy.localhost:5173
DNT: 1
Connection: keep-alive
Cookie: __Host-__Secure-directus.session_token=Penk9Datqsju9A8dOYso7hGjXgOoqKkf.oc7keEUnUPZ0LTOj2lp%2FrvPe4J5YhbApI2Uhf4heQkw%3D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Pragma: no-cache
Cache-Control: no-cache
TE: trailers
POST /api/auth/sign-up/email HTTP/2
Host: peggy.localhost:5173
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0
Accept: */*
Accept-Language: en-AU,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br, zstd
Referer: https://peggy.localhost:5173/signin
content-type: application/json
Content-Length: 186
Origin: https://peggy.localhost:5173
DNT: 1
Connection: keep-alive
Cookie: __Host-__Secure-directus.session_token=Penk9Datqsju9A8dOYso7hGjXgOoqKkf.oc7keEUnUPZ0LTOj2lp%2FrvPe4J5YhbApI2Uhf4heQkw%3D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Priority: u=0
Pragma: no-cache
Cache-Control: no-cache
TE: trailers
response headers:
HTTP/2 200
access-control-allow-origin: *
vary: Origin
content-type: application/json
set-cookie: __Secure-directus.session_token=onz6fOWLpPbJhf2yCeh57tqIjOsTe6xM.jeemRnvxeWtSbTmnA1o%2B%2F95yTDLGrjiqbjAO6iMjRs8%3D; Max-Age=604800; Domain=.localhost:5173; Path=/; HttpOnly; Secure; SameSite=None; Partitioned
date: Mon, 21 Apr 2025 11:20:09 GMT
X-Firefox-Spdy: h2
HTTP/2 200
access-control-allow-origin: *
vary: Origin
content-type: application/json
set-cookie: __Secure-directus.session_token=onz6fOWLpPbJhf2yCeh57tqIjOsTe6xM.jeemRnvxeWtSbTmnA1o%2B%2F95yTDLGrjiqbjAO6iMjRs8%3D; Max-Age=604800; Domain=.localhost:5173; Path=/; HttpOnly; Secure; SameSite=None; Partitioned
date: Mon, 21 Apr 2025 11:20:09 GMT
X-Firefox-Spdy: h2
PUBLIC_DOMAIN is https://localhost:5173 and PUBLIC_COOKIE_DOMAIN is localhost:5173
export const auth = betterAuth({
appName: PUBLIC_APP_NAME,
baseURL: PUBLIC_DOMAIN,
secret: DIRECTUS_SECRET,
emailAndPassword: {
enabled: true,
password: {
hash(password) {
const directus = getDirectusInstance();
return directus.request(generateHash(password))
},
verify({ hash, password}) {
const directus = getDirectusInstance();
return directus.request(verifyHash(password, hash))
},
}
},
advanced: {
database: {
generateId () { return crypto.randomUUID() },
},
crossSubDomainCookies: {
enabled: true,
domain: `.${PUBLIC_COOKIE_DOMAIN}`,
},

cookiePrefix: 'directus',
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: 'none',
partitioned: true,
},
},
database: new Pool({
connectionString: DB_CONNECTION_STRING,
}),
// database: drizzleAdapter(db, {
// provider: 'pg'
// }),
user: {
modelName: 'directus_users',
fields: {
name: 'first_name',
email: 'email',
image: 'avatar',
createdAt: 'date_created',
updatedAt: 'date_updated',
emailVerified: 'email_verified',
}
},
session: {
modelName: 'directus_sessions',
fields: {
// id has to be initialised with gen_random_uuid() in the database as the default
userId: 'user',
expiresAt: 'expires',
ipAddress: 'ip',
userAgent: 'user_agent',
token: 'token',
createdAt: 'date_created',
updatedAt: 'date_updated',
}
},
verification: {
modelName: 'verification',
fields: {
createdAt: 'date_created',
updatedAt: 'date_updated',
expiresAt: 'expires_at',
value: 'value',
identifier: 'identifier',
},
},
account: {
modelName: 'account',
fields: {
accountId: 'account_id',
userId: 'user_id',
accessToken: 'access_token',
refreshToken: 'refresh_token',
idToken: 'id_token',
providerId: 'provider_id',
accessTokenExpiresAt: "access_token_expires_at",
refreshTokenExpiresAt: "refresh_token_expires_at",
scope: 'scope',
password: 'password',
createdAt: 'date_created',
updatedAt: 'date_updated',
}
},
socialProviders: {
google: {
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
mapProfileToUser: (profile) => {
return {
firstName: profile.given_name,
lastName: profile.family_name,
image: profile.picture
};
},
}
},
export const auth = betterAuth({
appName: PUBLIC_APP_NAME,
baseURL: PUBLIC_DOMAIN,
secret: DIRECTUS_SECRET,
emailAndPassword: {
enabled: true,
password: {
hash(password) {
const directus = getDirectusInstance();
return directus.request(generateHash(password))
},
verify({ hash, password}) {
const directus = getDirectusInstance();
return directus.request(verifyHash(password, hash))
},
}
},
advanced: {
database: {
generateId () { return crypto.randomUUID() },
},
crossSubDomainCookies: {
enabled: true,
domain: `.${PUBLIC_COOKIE_DOMAIN}`,
},

cookiePrefix: 'directus',
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: 'none',
partitioned: true,
},
},
database: new Pool({
connectionString: DB_CONNECTION_STRING,
}),
// database: drizzleAdapter(db, {
// provider: 'pg'
// }),
user: {
modelName: 'directus_users',
fields: {
name: 'first_name',
email: 'email',
image: 'avatar',
createdAt: 'date_created',
updatedAt: 'date_updated',
emailVerified: 'email_verified',
}
},
session: {
modelName: 'directus_sessions',
fields: {
// id has to be initialised with gen_random_uuid() in the database as the default
userId: 'user',
expiresAt: 'expires',
ipAddress: 'ip',
userAgent: 'user_agent',
token: 'token',
createdAt: 'date_created',
updatedAt: 'date_updated',
}
},
verification: {
modelName: 'verification',
fields: {
createdAt: 'date_created',
updatedAt: 'date_updated',
expiresAt: 'expires_at',
value: 'value',
identifier: 'identifier',
},
},
account: {
modelName: 'account',
fields: {
accountId: 'account_id',
userId: 'user_id',
accessToken: 'access_token',
refreshToken: 'refresh_token',
idToken: 'id_token',
providerId: 'provider_id',
accessTokenExpiresAt: "access_token_expires_at",
refreshTokenExpiresAt: "refresh_token_expires_at",
scope: 'scope',
password: 'password',
createdAt: 'date_created',
updatedAt: 'date_updated',
}
},
socialProviders: {
google: {
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
mapProfileToUser: (profile) => {
return {
firstName: profile.given_name,
lastName: profile.family_name,
image: profile.picture
};
},
}
},
async trustedOrigins (request) {
const directusAdmin = getDirectusInstance(undefined, DIRECTUS_ADMIN_TOKEN);
const allTenants = await directusAdmin.request(readItems('tenants', {
fields: ['url', 'subdomain'],
}))
const origins: string[] = [
PUBLIC_DOMAIN,
]
if (process.env.NODE_ENV === 'development') origins.push(
`https://www.${PUBLIC_COOKIE_DOMAIN}`)
const allOrigins = allTenants.reduce((acc, tenant) => {
// Handle custom domains stored in the 'url' field
if (typeof tenant.url === 'string' && tenant.url) {
// Ensure the URL is a valid origin format (https://domain.com)
try {
const parsedUrl = new URL(tenant.url);
if (parsedUrl.protocol === 'https:') {
acc.push(parsedUrl.origin);
}
} catch (e) {
console.error(`Invalid tenant URL format: ${tenant.url}`);
}
}
if (tenant.subdomain) {
acc.push(`https://${tenant.subdomain}.${PUBLIC_COOKIE_DOMAIN}`);
}
return acc;
}, origins);
if (process.env.NODE_ENV === 'development') {
console.log('Trusted Origins:', allOrigins)
}
return allOrigins;
},
plugins: [organization({

schema: {
organization: {
modelName: 'tenants',
fields: {
id: 'id',
name: 'title',
slug: 'subdomain',
logo: 'logo',
metadata: 'metadata',
createdAt: 'date_created',
},
},
invitation: {
fields: {
expiresAt: 'expires_at',
inviterId: 'inviter_id',
organizationId: 'organization_id',
teamId: 'team_id',
}
},
member: {
fields: {
organizationId: 'organization_id',
userId: 'user_id',
role: 'role',
status: 'status',
},
},
session: {
fields: {
activeOrganizationId: 'active_organization_id',
}
},
user: {
fields: {
organizationId: 'organization_id',
userId: 'user_id',
}
},
}
})]
});
async trustedOrigins (request) {
const directusAdmin = getDirectusInstance(undefined, DIRECTUS_ADMIN_TOKEN);
const allTenants = await directusAdmin.request(readItems('tenants', {
fields: ['url', 'subdomain'],
}))
const origins: string[] = [
PUBLIC_DOMAIN,
]
if (process.env.NODE_ENV === 'development') origins.push(
`https://www.${PUBLIC_COOKIE_DOMAIN}`)
const allOrigins = allTenants.reduce((acc, tenant) => {
// Handle custom domains stored in the 'url' field
if (typeof tenant.url === 'string' && tenant.url) {
// Ensure the URL is a valid origin format (https://domain.com)
try {
const parsedUrl = new URL(tenant.url);
if (parsedUrl.protocol === 'https:') {
acc.push(parsedUrl.origin);
}
} catch (e) {
console.error(`Invalid tenant URL format: ${tenant.url}`);
}
}
if (tenant.subdomain) {
acc.push(`https://${tenant.subdomain}.${PUBLIC_COOKIE_DOMAIN}`);
}
return acc;
}, origins);
if (process.env.NODE_ENV === 'development') {
console.log('Trusted Origins:', allOrigins)
}
return allOrigins;
},
plugins: [organization({

schema: {
organization: {
modelName: 'tenants',
fields: {
id: 'id',
name: 'title',
slug: 'subdomain',
logo: 'logo',
metadata: 'metadata',
createdAt: 'date_created',
},
},
invitation: {
fields: {
expiresAt: 'expires_at',
inviterId: 'inviter_id',
organizationId: 'organization_id',
teamId: 'team_id',
}
},
member: {
fields: {
organizationId: 'organization_id',
userId: 'user_id',
role: 'role',
status: 'status',
},
},
session: {
fields: {
activeOrganizationId: 'active_organization_id',
}
},
user: {
fields: {
organizationId: 'organization_id',
userId: 'user_id',
}
},
}
})]
});
Rakesh Kumar
Rakesh Kumar15h ago
I have been working on a similar project..it's in next js...to implement tenant specific authentication..I took inspiration form vercel platform starter kit ..however I am stuck on how to do authentication for tenants customers...just asked for help here also some times back..anyway you could do something similar for your app...you can implement auth directly on subdomain.maindomain.com....I could not figure out either how to make crossSubdomainCookies work properly..
rhys
rhysOP3h ago
Good to know that we have the same problem.

Did you find this page helpful?