1import * as Alchemy from "alchemy";
2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Command from "alchemy/Command";
4import * as Output from "alchemy/Output";
5import * as Prisma from "alchemy/Prisma";
6import * as Config from "effect/Config";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Redacted from "effect/Redacted";
10
11const tanstackMessageConfig = (fallback: string) =>
12 Config.String("TANSTACK_MESSAGE").pipe(Effect.orElseSucceed(() => fallback));
13
14export default Alchemy.Stack(
15 "PrismaTanstackStart",
16 {
17 providers: Layer.mergeAll(Prisma.providers(), Command.providers()),
18 state: Cloudflare.state(),
19 },
20 Effect.gen(function* () {
21 const stage = yield* Alchemy.Stage;
22 const appName = yield* Config.String("PRISMA_TANSTACK_APP").pipe(
23 Effect.orElseSucceed(() => `alchemy-prisma-tanstack-start-${stage}`),
24 );
25 const customDomainHostname = yield* Config.String(
26 "PRISMA_TANSTACK_DOMAIN",
27 ).pipe(Effect.orElseSucceed(() => undefined));
28 const devPort = yield* Config.Number("PRISMA_TANSTACK_DEV_PORT").pipe(
29 Effect.orElseSucceed(() => 3000),
30 );
31
32 // Project is the Prisma Console container for everything else in this
33 // stack: its default branch, databases, connections, env vars, and Apps.
34 // `createDatabase: false` keeps the example explicit so the database below
35 // is visible as its own Alchemy resource.
36 const project = yield* Prisma.Project("Project", {
37 name: yield* Config.String("PRISMA_PROJECT").pipe(
38 Effect.orElseSucceed(() => undefined),
39 ),
40 createDatabase: false,
41 region: "eu-west-3",
42 });
43
44 // Postgres is the product-shaped alias for Prisma.Database. In deploy it
45 // creates a real Prisma Postgres database and attaches it to the default
46 // `main` branch created transactionally with the project. In `alchemy dev`,
47 // the `dev` block starts a local Prisma Postgres via `@prisma/dev` and
48 // returns the same connection-string attributes.
49 const postgres = yield* Prisma.Postgres("Postgres", {
50 project,
51 region: "eu-west-3",
52 branchGitName: "main",
53 dev: {
54 provider: "@prisma/dev",
55 // Local dev should boot into a useful app, not an empty shell. This
56 // command runs after @prisma/dev is ready, with DATABASE_URL pointed at
57 // the direct local Postgres URL.
58 migrate: "bun run db:setup",
59 },
60 });
61
62 // Connection creates the app-facing database credentials. Its returned
63 // strings are redacted in Alchemy state. In dev mode it mirrors the local
64 // `@prisma/dev` URLs from Postgres, so the app keeps the same env names.
65 const connection = yield* Prisma.Connection("Connection", {
66 database: postgres,
67 });
68
69 // Apply checked-in, transactional Prisma Next migrations before building a
70 // deployment. Apply is idempotent and intentionally runs every deploy so a
71 // restored or drifted database is verified instead of trusting local state.
72 // Production data is never seeded here.
73 const databaseSchema = yield* Command.Exec("DatabaseSchema", {
74 command: "bun run db:migrate",
75 cwd: ".",
76 env: {
77 DATABASE_URL:
78 connection.directConnectionString.as<Redacted.Redacted<string>>(),
79 // Bound server-side lock waits and statement execution independently
80 // from the release-process deadline below.
81 PGOPTIONS: "-c lock_timeout=30s -c statement_timeout=5min",
82 // The release subprocess needs only the database credential. Shadow
83 // the IaC process's Management API credentials so schema tooling and
84 // any of its plugins cannot read the workspace-scoped service token.
85 PRISMA_SERVICE_TOKEN: "",
86 PRISMA_API_TOKEN: "",
87 },
88 memo: false,
89 timeout: "6 minutes",
90 });
91
92 // Compute builds the TanStack Start app, uploads the `.output` artifact
93 // to Prisma Compute, and promotes a Deployment. The env block is the runtime
94 // contract for deploy; the dev block runs Vite locally while reusing the
95 // same database/project IDs generated by the resources above.
96 const app = yield* Prisma.Compute("App", {
97 project,
98 appName,
99 regionId: "eu-west-3",
100 branchGitName: "main",
101 path: ".",
102 build: {
103 command: "bun run build",
104 outdir: ".output",
105 entrypoint: "server/index.mjs",
106 env: {
107 // Creates an explicit dependency on successful schema convergence
108 // without exposing the migration credential to the build.
109 ALCHEMY_SCHEMA_HASH: databaseSchema.hash.pipe(
110 Output.map((hash) => hash.input ?? "unmemoized"),
111 ),
112 },
113 },
114 port: 3000,
115 healthCheck: { path: "/api/health" },
116 env: {
117 // The deployed application receives only the pooled credential it
118 // uses. The direct credential is scoped to the schema command above.
119 DATABASE_URL: connection.databaseUrl,
120 TANSTACK_MESSAGE: yield* tanstackMessageConfig(
121 "hello from TanStack Start on Prisma Compute",
122 ),
123 // Keep values consumed by this deployment in Compute.env so they are
124 // reconciled before the deployment snapshots its branch environment.
125 TANSTACK_SHARED_FLAG: Redacted.make("project-level"),
126 },
127 dev: {
128 command: "bun run dev:start",
129 port: devPort,
130 env: {
131 TANSTACK_MESSAGE: yield* tanstackMessageConfig(
132 "hello from local alchemy dev",
133 ),
134 TANSTACK_SHARED_FLAG: "local-project-level",
135 PRISMA_TANSTACK_PG_POOL_MAX: "1",
136 },
137 },
138 destroyOldDeployment: true,
139 });
140
141 const customDomain = customDomainHostname
142 ? yield* Prisma.CustomDomain("CustomDomain", {
143 app: app.appId,
144 hostname: customDomainHostname,
145 })
146 : undefined;
147
148 return {
149 projectId: project.projectId,
150 branchId: postgres.branchId,
151 databaseId: postgres.databaseId,
152 connectionId: connection.connectionId,
153 appId: app.appId,
154 deploymentId: app.deploymentId,
155 url: app.url,
156 customDomainId: customDomain?.customDomainId,
157 customDomainHostname: customDomain?.hostname,
158 customDomainStatus: customDomain?.status,
159 customDomainDnsRecords: customDomain?.dnsRecords,
160 };
161 }),
162);