1import * as Data from "effect/Data";
2import type * as Effect from "effect/Effect";
3import type * as Redacted from "effect/Redacted";
4import * as Binding from "../Binding.ts";
5import type { RuntimeContext } from "../RuntimeContext.ts";
6import type { Postgres } from "./Postgres.ts";
7
8/**
9 * Bind a {@link Postgres} database to a Railway {@link Service} or
10 * {@link Function} and obtain the Effect-native connection string for
11 * `Drizzle.Postgres` / `SQL.Postgres`.
12 *
13 * `ConnectPostgres` is the Context tag, the type, and the callable —
14 * `yield* Railway.ConnectPostgres(Db)`. Provide {@link ConnectPostgresHttp}.
15 *
16 * **Example:** Bind Postgres in a Function
17 * ```typescript
18 * import * as Drizzle from "alchemy/Drizzle/Postgres";
19 *
20 * export default class Query extends Railway.Function<Query>()(
21 * "Query",
22 * { project: Site, main: import.meta.url, build: { install: ["pg"] } },
23 * Effect.gen(function* () {
24 * const conn = yield* Railway.ConnectPostgres(Db);
25 * const db = yield* Drizzle.Postgres(conn.connectionString);
26 * return {
27 * fetch: db.execute("select 1 as ok").pipe(
28 * Effect.flatMap(HttpServerResponse.json),
29 * ),
30 * };
31 * }).pipe(Effect.provide(Railway.ConnectPostgresHttp)),
32 * ) {}
33 * ```
34 *
35 * ### Variable references
36 * `ConnectPostgres` packs a typed private URI onto the host. To store
37 * Railway's template instead of a resolved URI (IaC
38 * `db.env.DATABASE_URL`), pass `Railway.ref(Db, "DATABASE_URL")` as a
39 * {@link Variable} `value` or `Service.env` entry. Railway interpolates
40 * `${{Db.DATABASE_URL}}` at build/runtime.
41 *
42 * **Example:** Railway.ref
43 * ```typescript
44 * const db = yield* Railway.Postgres("Db", { project: site });
45 * yield* Railway.Variable("DatabaseUrl", {
46 * project: site,
47 * service: api,
48 * name: "DATABASE_URL",
49 * value: Railway.ref(db, "DATABASE_URL"),
50 * });
51 * ```
52 *
53 * @binding
54 * @product Postgres
55 * @category Storage & Databases
56 */
57export interface ConnectPostgres extends Binding.Service<
58 ConnectPostgres,
59 "Railway.ConnectPostgres",
60 (postgres: Postgres) => Effect.Effect<ConnectPostgresClient>
61> {}
62
63export const ConnectPostgres = Binding.Service<ConnectPostgres>(
64 "Railway.ConnectPostgres",
65);
66
67export const DATABASE_URL_SECRET = "DATABASE_URL";
68export const DATABASE_PUBLIC_URL_SECRET = "DATABASE_PUBLIC_URL";
69
70export const connectEnvKeys = (postgres: Pick<Postgres, "LogicalId">) => {
71 const id = postgres.LogicalId.replaceAll(/[^a-zA-Z0-9]/g, "_").toUpperCase();
72 return {
73 pooled: `RAILWAY_POSTGRES_${id}_POOLED`,
74 direct: `RAILWAY_POSTGRES_${id}_DIRECT`,
75 };
76};
77
78export class PostgresUrlMissing extends Data.TaggedError(
79 "Railway.PostgresUrlMissing",
80)<{
81 name: string;
82}> {}
83
84export interface ConnectPostgresClient {
85 /**
86 * Private (`{name}.railway.internal`) connection string. Pass this to
87 * {@link Drizzle.Postgres} or `SQL.Postgres` from a {@link Service}.
88 */
89 connectionString: Effect.Effect<
90 Redacted.Redacted<string>,
91 PostgresUrlMissing,
92 RuntimeContext
93 >;
94 /**
95 * Same private URI — Railway Postgres has no PgBouncer split. Kept so
96 * callers matching the Fly `ConnectPostgres` shape keep working.
97 */
98 directConnectionString: Effect.Effect<
99 Redacted.Redacted<string>,
100 PostgresUrlMissing,
101 RuntimeContext
102 >;
103}