1/**
2 * A Railway project that exercises every resource and binding:
3 *
4 * - `Site` — parent Project (`src/shared.ts`)
5 * - `Staging` — extra Environment (production is on the Project)
6 * - `Marker` — shared Variable the Api reads via `Config.String`
7 * - `Disk` — Volume the Worker mounts with `MountVolume`
8 * - `Db` — Postgres + `ConnectPostgres` / Drizzle
9 * - `Mysql` — MySQL (same shape as Postgres)
10 * - `DatabaseUrl` — `Railway.ref(Db, "DATABASE_URL")` template
11 * - `Cache` — Redis + `ReadWriteRedis`
12 * - `CacheProxy` — TcpProxy so Redis is reachable from a laptop
13 * - `Data` — Bucket + Put/Get/Head/List/Delete
14 * - `Echo` — image Service (`hashicorp/http-echo`, healthcheck)
15 * - `Ping` — Effect HTTP Service (`src/ping.ts`) that
16 * `ConnectPostgres`s the same Db (Docker, not a canvas Function)
17 * - `Cleanup` — canvas cron Function (`console.log("tick")`)
18 * - `Api` — Effect HTTP Service (`src/api.ts`, healthcheck)
19 * - `Worker` — Effect background Service that writes the volume
20 * (`src/worker.ts`)
21 * - `Backend` — canvas Group around the data plane
22 *
23 * `VolumeBackup` is Pro-plan gated (`RailwayForbidden` on Hobby) and
24 * omitted here; see `Railway.VolumeBackup` and the volumes hub page.
25 *
26 * Effect-native Services upload a generated Dockerfile; Railway
27 * builds the image. No GHCR.
28 *
29 * CustomDomain needs a hostname you control. Pass
30 * `RAILWAY_TEST_DOMAIN` to attach one to Api.
31 */
32import * as Alchemy from "alchemy";
33import * as Output from "alchemy/Output";
34import * as Railway from "alchemy/Railway";
35import * as Effect from "effect/Effect";
36import Api from "./src/api.ts";
37import { Echo } from "./src/echo.ts";
38import Ping from "./src/ping.ts";
39import {
40 Cache,
41 Cleanup,
42 Data,
43 DatabaseUrl,
44 Db,
45 Disk,
46 Marker,
47 Mysql,
48 Site,
49 Staging,
50} from "./src/shared.ts";
51import Worker from "./src/worker.ts";
52
53export default Alchemy.Stack(
54 "RailwayService",
55 {
56 providers: Railway.providers(),
57 state: Alchemy.localState(),
58 },
59 Effect.gen(function* () {
60 const site = yield* Site;
61 const staging = yield* Staging;
62 const marker = yield* Marker;
63 const disk = yield* Disk;
64 const db = yield* Db;
65 const mysql = yield* Mysql;
66 const databaseUrl = yield* DatabaseUrl;
67 const cache = yield* Cache;
68 const cacheProxy = yield* Railway.TcpProxy("CacheProxy", {
69 redis: cache,
70 environment: site,
71 applicationPort: 6379,
72 });
73 const data = yield* Data;
74 const echo = yield* Echo;
75 const ping = yield* Ping;
76 const cleanup = yield* Cleanup;
77 const worker = yield* Worker;
78 const api = yield* Api;
79 const backend = yield* Railway.Group("Backend", {
80 project: site,
81 resources: [db, mysql, cache, echo, api, worker],
82 });
83
84 const domain = process.env.RAILWAY_TEST_DOMAIN;
85 const www =
86 domain === undefined || domain.length === 0
87 ? undefined
88 : yield* Railway.CustomDomain("Www", {
89 service: api,
90 environment: site,
91 domain,
92 targetPort: 3000,
93 });
94
95 return {
96 projectId: site.projectId,
97 projectName: site.name,
98 projectUrl: site.url,
99 environmentId: site.environmentId,
100 stagingId: staging.environmentId,
101 stagingName: staging.name,
102 secretName: marker.name,
103 volumeId: disk.volumeId,
104 postgresServiceId: db.serviceId,
105 postgresName: db.name,
106 postgresPublic: db.publicConnectionUri,
107 mysqlServiceId: mysql.serviceId,
108 mysqlName: mysql.name,
109 databaseUrlName: databaseUrl.name,
110 pingUrl: ping.url,
111 cleanupId: cleanup.serviceId,
112 groupId: backend.groupId,
113 redisServiceId: cache.serviceId,
114 redisProxy: Output.interpolate`${cacheProxy.domain}:${cacheProxy.proxyPort}`,
115 bucketId: data.bucketId,
116 echoUrl: echo.url,
117 apiServiceId: api.serviceId,
118 apiName: api.name,
119 apiUrl: api.url,
120 workerServiceId: worker.serviceId,
121 customDomain: www?.domain,
122 workspaceId: site.workspaceId,
123 };
124 }),
125);