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.
1 Reply
rhys
rhysOP7d ago
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',
}
},
}
})]
});

Did you find this page helpful?