1import * as Alchemy from "alchemy";
2import * as Neon from "alchemy/Neon";
3import * as Output from "alchemy/Output";
4import * as Config from "effect/Config";
5import * as Effect from "effect/Effect";
6
7export default Alchemy.Stack(
8 "NeonUploadPreview",
9 {
10 providers: Neon.providers(),
11 state: Alchemy.localState(),
12 },
13 Effect.gen(function* () {
14 const projectId = yield* Config.String("PARENT_PROJECT_ID");
15 const parentBranchId = yield* Config.String("PARENT_BRANCH_ID");
16 const bucketName = yield* Config.String("PARENT_BUCKET_NAME");
17 const appOrigin = yield* Effect.sync(
18 () => process.env.UPLOAD_APP_ORIGIN ?? "*",
19 );
20 const branch = yield* Neon.Branch("Preview", {
21 project: { projectId },
22 parentBranch: { branchId: parentBranchId },
23 initSource: "parent-data",
24 });
25 // Adoption is confined to the new child; the parent Auth remains independently owned.
26 const auth = yield* Neon.Auth("PreviewAuth", {
27 branch,
28 name: "Upload journal preview",
29 allowLocalhost: true,
30 }).pipe(Alchemy.AdoptPolicy.adopt());
31 const api = yield* Neon.Function("PreviewApi", {
32 branch,
33 main: "./src/native.ts",
34 env: {
35 APP_ORIGIN: appOrigin,
36 UPLOAD_BUCKET: bucketName,
37 AUTH_URL: auth.baseUrl,
38 AUTH_JWKS_URL: auth.jwksUrl,
39 TRIGGER_NAME: "PreviewUploads",
40 },
41 });
42 // Inherited triggers stay disabled. This is a new, explicitly enabled child trigger.
43 yield* Neon.FunctionTrigger("PreviewUploads", {
44 function: api,
45 name: "PreviewUploads",
46 type: "storage_object_created",
47 storageObjectCreated: {
48 bucket: { projectId, branchId: branch.branchId, bucketName },
49 prefix: "incoming/",
50 },
51 path: "/jobs/upload",
52 enabled: true,
53 });
54 const web = yield* Neon.Website.Vite("PreviewWeb", {
55 branch,
56 rootDir: "./web",
57 env: { VITE_API_URL: api.url, VITE_NEON_AUTH_URL: auth.baseUrl },
58 assets: { notFoundHandling: "single-page-application" },
59 });
60 if (!web.url)
61 return yield* Effect.die(new Error("Preview website URL is unavailable"));
62 const siteUrl =
63 typeof web.url === "string" ? Output.literal(web.url) : web.url;
64 const origin = Output.map(
65 siteUrl,
66 (url: string | undefined) => new URL(url ?? "").origin,
67 );
68 yield* Neon.AuthTrustedDomain("PreviewOrigin", { auth, domain: origin });
69 // No CustomDomain is declared: parent hostnames must never route to this preview.
70 return {
71 url: web.url,
72 apiUrl: api.url,
73 authUrl: auth.baseUrl,
74 projectId,
75 parentBranchId,
76 branchId: branch.branchId,
77 bucketName,
78 };
79 }),
80);