-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathalchemy.run.ts
More file actions
120 lines (109 loc) · 3.98 KB
/
Copy pathalchemy.run.ts
File metadata and controls
120 lines (109 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import {
loadDeployEnv,
resolveDeployStage,
selectStateBackend,
} from "@stackpanel/infra/lib/deploy";
import { NeonProject, neonProviders } from "@stackpanel/infra/resources/neon";
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Output from "alchemy/Output";
import * as Workers from "@distilled.cloud/cloudflare/workers";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
const PROJECT = "stackpanel";
const SERVICE = "web";
// `appEnv` is our SOPS namespace (`prod` | `staging` | `dev`); `stage` is what
// alchemy itself sees and mirrors into `Stage`. Both are derived from a single
// source of truth so the secrets we decrypt match the resources we provision.
const { appEnv } = resolveDeployStage();
// Decrypts the per-app SOPS payload (CLOUDFLARE_*, NEON_API_KEY, …) and injects
// it into process.env so downstream Cloudflare/Neon providers see it. Hard-fails
// with a copy-pasteable message listing every missing required env var.
await loadDeployEnv(SERVICE, appEnv);
// stackpanel.com
const STACKPANEL_ZONE = "d34628a3ab639230ff1f6dc1eb640eec";
const program = Effect.gen(function* () {
const stage = yield* Alchemy.Stage;
const db = yield* NeonProject("postgres", {
name: `${PROJECT}-${stage}`,
regionId: "aws-us-east-1",
pgVersion: 17,
databaseName: `${SERVICE}_${stage}`,
roleName: `${PROJECT}-${SERVICE}-owner`,
});
const website = yield* Cloudflare.Vite("TanstackStart", {
compatibility: {
flags: ["nodejs_compat"],
},
env: {
DATABASE_URL: db.connectionUri,
},
});
let url: Output.Output<string | undefined> = website.url;
if (stage !== "dev") {
// Production binds two hostnames to the same worker:
// - apex stackpanel.com → marketing/landing (`/`, `/login`, …)
// - local.stackpanel.com → studio (mirrors local.drizzle.studio: the
// `/studio/*` routes talk to the user's machine via
// http://127.0.0.1:9876).
// Both ship the same bundle today; auth cookies are scoped to
// `.stackpanel.com` so a session from the apex carries into the studio.
// Non-prod stages only get the studio hostname — there's no marketing
// preview to host on the apex.
const hostnames =
stage === "production"
? ["local.stackpanel.com", "stackpanel.com"]
: [`local.${stage}.stackpanel.com`];
const primary = hostnames[0]!;
url = Output.all(website.accountId, website.workerName).pipe(
Output.mapEffect(([accountId, workerName]) =>
Effect.gen(function* () {
for (const hostname of hostnames) {
const existing = yield* Workers.listDomains({
accountId,
hostname,
});
const stale = existing.result.filter(
(d) => d.hostname === hostname && d.id,
);
if (stale.length > 0) {
yield* Effect.log(
`[alchemy] purging ${stale.length} existing binding(s) at ${hostname}: ${stale
.map((d) => `${d.service ?? "?"}#${d.id}`)
.join(", ")}`,
);
}
for (const d of stale) {
yield* Workers.deleteDomain({ accountId, domainId: d.id! });
}
yield* Workers.putDomain({
accountId,
hostname,
service: workerName,
zoneId: STACKPANEL_ZONE,
});
}
return `https://${primary}` as string | undefined;
}).pipe(Effect.orDie),
),
);
}
return {
url,
databaseUrl: db.connectionUri,
};
});
const providers = Layer.mergeAll(
Cloudflare.providers(),
neonProviders(),
) as Layer.Layer<any, never, any>;
export default Alchemy.Stack(
`${PROJECT}-${SERVICE}`,
{
providers,
// dev/PR previews → filesystem state (cached across CI runs);
// staging/prod → shared Cloudflare-hosted state store.
state: selectStateBackend(appEnv),
},
program,
);