Skip to content

Commit 0093d5c

Browse files
czxtmclaude
andcommitted
web: bind apex stackpanel.com alongside local.stackpanel.com
Production now serves both hostnames from the same Worker: - stackpanel.com → marketing/landing - local.stackpanel.com → studio (`/studio/*` routes talking to the user's local agent at 127.0.0.1:9876) Both ship the same bundle today; better-auth's crossSubDomainCookies is scoped to `.stackpanel.com` in production so a sign-in from the apex carries into the studio subdomain. Outside production cookies stay host-only — preview stages live on isolated per-PR subdomains and dev runs on localhost where domain attributes are ignored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ed52573 commit 0093d5c

2 files changed

Lines changed: 50 additions & 28 deletions

File tree

apps/web/alchemy.run.ts

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,40 +44,49 @@ const program = Effect.gen(function* () {
4444
let url: Output.Output<string | undefined> = website.url;
4545

4646
if (stage !== "dev") {
47-
// Studio is local-agent first (mirrors local.drizzle.studio): the
48-
// browser app at local.stackpanel.com talks to the user's machine via
49-
// http://127.0.0.1:9876. Apex stackpanel.com is reserved for marketing.
50-
const hostname =
47+
// Production binds two hostnames to the same worker:
48+
// - apex stackpanel.com → marketing/landing (`/`, `/login`, …)
49+
// - local.stackpanel.com → studio (mirrors local.drizzle.studio: the
50+
// `/studio/*` routes talk to the user's machine via
51+
// http://127.0.0.1:9876).
52+
// Both ship the same bundle today; auth cookies are scoped to
53+
// `.stackpanel.com` so a session from the apex carries into the studio.
54+
// Non-prod stages only get the studio hostname — there's no marketing
55+
// preview to host on the apex.
56+
const hostnames =
5157
stage === "production"
52-
? "local.stackpanel.com"
53-
: `local.${stage}.stackpanel.com`;
58+
? ["local.stackpanel.com", "stackpanel.com"]
59+
: [`local.${stage}.stackpanel.com`];
60+
const primary = hostnames[0]!;
5461
url = Output.all(website.accountId, website.workerName).pipe(
5562
Output.mapEffect(([accountId, workerName]) =>
5663
Effect.gen(function* () {
57-
const existing = yield* Workers.listDomains({
58-
accountId,
59-
hostname,
60-
});
61-
const stale = existing.result.filter(
62-
(d) => d.hostname === hostname && d.id,
63-
);
64-
if (stale.length > 0) {
65-
yield* Effect.log(
66-
`[alchemy] purging ${stale.length} existing binding(s) at ${hostname}: ${stale
67-
.map((d) => `${d.service ?? "?"}#${d.id}`)
68-
.join(", ")}`,
64+
for (const hostname of hostnames) {
65+
const existing = yield* Workers.listDomains({
66+
accountId,
67+
hostname,
68+
});
69+
const stale = existing.result.filter(
70+
(d) => d.hostname === hostname && d.id,
6971
);
72+
if (stale.length > 0) {
73+
yield* Effect.log(
74+
`[alchemy] purging ${stale.length} existing binding(s) at ${hostname}: ${stale
75+
.map((d) => `${d.service ?? "?"}#${d.id}`)
76+
.join(", ")}`,
77+
);
78+
}
79+
for (const d of stale) {
80+
yield* Workers.deleteDomain({ accountId, domainId: d.id! });
81+
}
82+
yield* Workers.putDomain({
83+
accountId,
84+
hostname,
85+
service: workerName,
86+
zoneId: STACKPANEL_ZONE,
87+
});
7088
}
71-
for (const d of stale) {
72-
yield* Workers.deleteDomain({ accountId, domainId: d.id! });
73-
}
74-
yield* Workers.putDomain({
75-
accountId,
76-
hostname,
77-
service: workerName,
78-
zoneId: STACKPANEL_ZONE,
79-
});
80-
return `https://${hostname}` as string | undefined;
89+
return `https://${primary}` as string | undefined;
8190
}).pipe(Effect.orDie),
8291
),
8392
);

packages/auth/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ if (polarClient) {
5555
);
5656
}
5757

58+
// In production both stackpanel.com and local.stackpanel.com serve the same
59+
// app, and the API at api.stackpanel.com sets the session cookie. Scoping
60+
// the cookie to `.stackpanel.com` lets a sign-in from the apex carry into
61+
// the studio subdomain. Outside production we leave it host-only — preview
62+
// stages live on per-PR subdomains that share nothing with each other, and
63+
// local dev runs on localhost where a domain attribute would be ignored.
64+
const deployEnv = process.env.STACKPANEL_DEPLOY_ENV;
65+
const crossSubDomainCookies =
66+
deployEnv === "production"
67+
? { enabled: true as const, domain: ".stackpanel.com" }
68+
: undefined;
69+
5870
export const auth = betterAuth({
5971
database: drizzleAdapter(db, {
6072
provider: "pg",
@@ -69,6 +81,7 @@ export const auth = betterAuth({
6981
secure: true,
7082
httpOnly: true,
7183
},
84+
...(crossSubDomainCookies ? { crossSubDomainCookies } : {}),
7285
},
7386
plugins,
7487
});

0 commit comments

Comments
 (0)