1/**
2 * A Hetzner Cloud app that exercises the full module-scope graph:
3 *
4 * - `Net` — private Network (`src/shared.ts`)
5 * - `Box` — Server attached to the Network
6 * - `Data` — Volume both Services mount via `Hetzner.MountVolume`
7 * - `Wall` — Firewall (SSH + API) applied to the Server
8 * - `Edge` — Load Balancer targeting the Server over the private IP
9 * - `DnsZone` / `AppRecord` — optional DNS (`HETZNER_ZONE` for a real apex)
10 * - `Api` — HTTP Service on port 3000 (`src/api.ts`)
11 * - `Worker` — background Service that writes the volume marker (`src/worker.ts`)
12 */
13import * as Alchemy from "alchemy";
14import * as Hetzner from "alchemy/Hetzner";
15import * as Output from "alchemy/Output";
16import * as Effect from "effect/Effect";
17import Api from "./src/api.ts";
18import {
19 API_PORT,
20 AppRecord,
21 Box,
22 Data,
23 DnsZone,
24 Edge,
25 Net,
26 Wall,
27} from "./src/shared.ts";
28import Worker from "./src/worker.ts";
29
30export default Alchemy.Stack(
31 "HetznerApp",
32 {
33 providers: Hetzner.providers(),
34 state: Alchemy.localState(),
35 },
36 Effect.gen(function* () {
37 const network = yield* Net;
38 const server = yield* Box;
39 const volume = yield* Data;
40 const firewall = yield* Wall;
41 const lb = yield* Edge;
42 const worker = yield* Worker;
43 const api = yield* Api;
44 const dns =
45 process.env.HETZNER_ZONE !== undefined &&
46 process.env.HETZNER_ZONE.length > 0
47 ? yield* Effect.all({ zone: DnsZone, records: AppRecord })
48 : undefined;
49
50 return {
51 networkId: network.networkId,
52 serverId: server.serverId,
53 serverIpv4: server.ipv4,
54 volumeId: volume.id,
55 firewallId: firewall.id,
56 loadBalancerId: lb.id,
57 loadBalancerIpv4: lb.ipv4,
58 loadBalancerUrl: Output.interpolate`http://${lb.ipv4}`,
59 zoneId: dns?.zone.zoneId,
60 zoneName: dns?.zone.name,
61 recordName: dns?.records.name,
62 apiUrl: api.url,
63 apiPort: api.port,
64 workerUnit: worker.unitName,
65 enqueueExample: Output.interpolate`http://${server.ipv4}:${API_PORT}`,
66 };
67 }),
68);