1import * as Alchemy from "alchemy";
2import * as Drizzle from "alchemy/Drizzle";
3import * as GCP from "alchemy/GCP";
4import * as Effect from "effect/Effect";
5
6export const database = Effect.gen(function* () {
7 // Shared-core, zonal, no backups: the cheapest Postgres Cloud SQL runs.
8 // No authorized networks — the only ways in are the Cloud SQL connector
9 // (Cloud Run's `/cloudsql` socket, IAM-gated) and the Data API used for
10 // migrations.
11 const instance = yield* GCP.SQL.Instance("Postgres", {
12 databaseVersion: "POSTGRES_17",
13 edition: "ENTERPRISE",
14 tier: "db-f1-micro",
15 availabilityType: "ZONAL",
16 backupEnabled: false,
17 dataApiAccess: true,
18 });
19 const db = yield* GCP.SQL.Database("App", {
20 instance: instance.instanceName,
21 });
22
23 // Generated once and kept in state; the regional secret below is where
24 // the Data API (migrations) and the service (runtime) read it from.
25 const password = yield* Alchemy.Random("AppPasswordValue", { bytes: 24 });
26 const user = yield* GCP.SQL.User("AppUser", {
27 instance: instance.instanceName,
28 password: password.text,
29 // The user owns the migrated tables, so Postgres refuses to drop it
30 // while the database still exists; deleting the instance removes it.
31 deletionPolicy: "ABANDON",
32 });
33 // The Data API only accepts regional secrets in the instance's region.
34 const passwordSecret = yield* GCP.SecretManager.LocationsSecret(
35 "AppPassword",
36 { location: instance.region },
37 );
38
39 // drizzle-kit regenerates ./migrations whenever src/schema.ts changes.
40 const schema = yield* Drizzle.Schema("Schema", {
41 schema: "./src/schema.ts",
42 out: "./migrations",
43 });
44
45 return { instance, db, user, password, passwordSecret, schema };
46});