From 9c5e8bf2411c7d67461ae98a08141fad2d61495d Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:30:36 -0700 Subject: [PATCH 1/5] feat(cloud): use @executor/env for env access --- apps/cloud/package.json | 1 + apps/cloud/src/api.ts | 5 +++-- apps/cloud/src/auth/handlers.ts | 3 ++- apps/cloud/src/auth/workos.ts | 7 ++++--- apps/cloud/src/env.ts | 36 +++++++++++++++++++++++++++++++++ apps/cloud/src/services/db.ts | 7 ++++--- apps/cloud/src/web/auth.tsx | 14 +++++++++---- apps/cloud/vite.config.ts | 17 +++++++++++++++- bun.lock | 1 + 9 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 apps/cloud/src/env.ts diff --git a/apps/cloud/package.json b/apps/cloud/package.json index 12a52a6537..9546af8988 100644 --- a/apps/cloud/package.json +++ b/apps/cloud/package.json @@ -8,6 +8,7 @@ "@effect-atom/atom-react": "^0.5.0", "@effect/platform": "catalog:", "@executor/api": "workspace:*", + "@executor/env": "workspace:*", "@executor/execution": "workspace:*", "@executor/plugin-google-discovery": "workspace:*", "@executor/plugin-graphql": "workspace:*", diff --git a/apps/cloud/src/api.ts b/apps/cloud/src/api.ts index 1fc1182f74..0b619dbb8d 100644 --- a/apps/cloud/src/api.ts +++ b/apps/cloud/src/api.ts @@ -31,6 +31,7 @@ import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers"; import { WorkOSAuth } from "./auth/workos"; import { DbService } from "./services/db"; import { createTeamExecutor } from "./services/executor"; +import { env } from "./env"; const ProtectedCloudApi = addGroup(OpenApiGroup) .add(McpGroup) @@ -104,7 +105,7 @@ const COOKIE_OPTIONS = { httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: process.env.NODE_ENV === "production", + secure: env.NODE_ENV === "production", }; const resolveAuth = (request: Request) => @@ -151,7 +152,7 @@ const resolveExecutor = (teamId: string) => const users = yield* UserStoreService; const team = yield* users.use((store) => store.getTeam(teamId)); const teamName = team?.name ?? "Unknown Team"; - const encryptionKey = process.env.ENCRYPTION_KEY ?? "local-dev-encryption-key"; + const encryptionKey = env.ENCRYPTION_KEY; return yield* createTeamExecutor(teamId, teamName, encryptionKey); }); diff --git a/apps/cloud/src/auth/handlers.ts b/apps/cloud/src/auth/handlers.ts index ca1e40c4cb..f0a2798c5a 100644 --- a/apps/cloud/src/auth/handlers.ts +++ b/apps/cloud/src/auth/handlers.ts @@ -6,13 +6,14 @@ import { addGroup } from "@executor/api"; import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api"; import { AuthContext, UserStoreService } from "./context"; import { WorkOSAuth } from "./workos"; +import { env } from "../env"; const COOKIE_OPTIONS = { path: "/", httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: process.env.NODE_ENV === "production", + secure: env.NODE_ENV === "production", }; // --------------------------------------------------------------------------- diff --git a/apps/cloud/src/auth/workos.ts b/apps/cloud/src/auth/workos.ts index 0572edb6eb..fd9d724d80 100644 --- a/apps/cloud/src/auth/workos.ts +++ b/apps/cloud/src/auth/workos.ts @@ -5,6 +5,7 @@ import { Context, Effect, Layer } from "effect"; import { WorkOS } from "@workos-inc/node"; import { WorkOSError } from "./errors"; +import { env } from "../env"; const COOKIE_NAME = "wos-session"; @@ -14,9 +15,9 @@ const COOKIE_NAME = "wos-session"; const make = Effect.gen(function* () { - const apiKey = process.env.WORKOS_API_KEY!; - const clientId = process.env.WORKOS_CLIENT_ID!; - const cookiePassword = process.env.WORKOS_COOKIE_PASSWORD!; + const apiKey = env.WORKOS_API_KEY; + const clientId = env.WORKOS_CLIENT_ID; + const cookiePassword = env.WORKOS_COOKIE_PASSWORD; if (!cookiePassword || cookiePassword.length < 32) { return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters")); diff --git a/apps/cloud/src/env.ts b/apps/cloud/src/env.ts new file mode 100644 index 0000000000..3cefbe52cf --- /dev/null +++ b/apps/cloud/src/env.ts @@ -0,0 +1,36 @@ +import { createEnv, Env } from "@executor/env"; + +const server = { + NODE_ENV: Env.literalOr( + "NODE_ENV", + "development", + "development", + "test", + "production", + ), + DATABASE_URL: Env.stringOr("DATABASE_URL", ""), + PGLITE_DATA_DIR: Env.stringOr("PGLITE_DATA_DIR", ".pglite"), + ENCRYPTION_KEY: Env.stringOr( + "ENCRYPTION_KEY", + "local-dev-encryption-key", + ), + WORKOS_API_KEY: Env.string("WORKOS_API_KEY"), + WORKOS_CLIENT_ID: Env.string("WORKOS_CLIENT_ID"), + WORKOS_COOKIE_PASSWORD: Env.string("WORKOS_COOKIE_PASSWORD"), +}; + +type CloudEnv = Readonly<{ + NODE_ENV: "development" | "test" | "production"; + DATABASE_URL: string; + PGLITE_DATA_DIR: string; + ENCRYPTION_KEY: string; + WORKOS_API_KEY: string; + WORKOS_CLIENT_ID: string; + WORKOS_COOKIE_PASSWORD: string; +}>; + +export const env = createEnv({ + server, + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as CloudEnv; diff --git a/apps/cloud/src/services/db.ts b/apps/cloud/src/services/db.ts index b162d23b3a..dec76a8de8 100644 --- a/apps/cloud/src/services/db.ts +++ b/apps/cloud/src/services/db.ts @@ -7,6 +7,7 @@ import { resolve } from "node:path"; import * as sharedSchema from "@executor/storage-postgres/schema"; import * as cloudSchema from "./schema"; import type { DrizzleDb } from "@executor/storage-postgres"; +import { env } from "../env"; const schema = { ...sharedSchema, ...cloudSchema }; @@ -23,11 +24,11 @@ type DbResource = { }; const createDbResource = async (): Promise => { - if (process.env.DATABASE_URL) { + if (env.DATABASE_URL) { const { drizzle } = await import("drizzle-orm/node-postgres"); const { migrate } = await import("drizzle-orm/node-postgres/migrator"); const { Pool } = await import("pg"); - const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + const pool = new Pool({ connectionString: env.DATABASE_URL }); const db = drizzle(pool, { schema }) as DrizzleDb; await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR }); return { @@ -39,7 +40,7 @@ const createDbResource = async (): Promise => { const { PGlite } = await import("@electric-sql/pglite"); const { drizzle } = await import("drizzle-orm/pglite"); const { migrate } = await import("drizzle-orm/pglite/migrator"); - const dataDir = process.env.PGLITE_DATA_DIR ?? ".pglite"; + const dataDir = env.PGLITE_DATA_DIR; const client = new PGlite(dataDir); const db = drizzle(client, { schema }) as DrizzleDb; await migrate(db, { migrationsFolder: MIGRATIONS_DIR }); diff --git a/apps/cloud/src/web/auth.tsx b/apps/cloud/src/web/auth.tsx index 9de98bdbe5..e0c9d16f2f 100644 --- a/apps/cloud/src/web/auth.tsx +++ b/apps/cloud/src/web/auth.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext } from "react"; -import { useAtomValue, Result } from "@effect-atom/atom-react"; +import { Atom, useAtomValue, Result } from "@effect-atom/atom-react"; import { CloudApiClient } from "./client"; @@ -19,13 +19,19 @@ type AuthTeam = { name: string; }; +type MeResponse = { + user: AuthUser; + team: AuthTeam | null; +}; + // --------------------------------------------------------------------------- // Auth atom — typed query against CloudAuthApi // --------------------------------------------------------------------------- -export const authAtom = CloudApiClient.query("cloudAuth", "me", { - timeToLive: "5 minutes", -}); +export const authAtom: Atom.Atom> = + CloudApiClient.query("cloudAuth", "me", { + timeToLive: "5 minutes", + }); // --------------------------------------------------------------------------- // Provider + hook diff --git a/apps/cloud/vite.config.ts b/apps/cloud/vite.config.ts index 8de032c45d..285a7aae8d 100644 --- a/apps/cloud/vite.config.ts +++ b/apps/cloud/vite.config.ts @@ -2,10 +2,25 @@ import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; +import { createEnv, Env } from "@executor/env"; + +const server = { + PORT: Env.numberOr("PORT", 5173), +}; + +type ViteEnv = Readonly<{ + PORT: number; +}>; + +const viteEnv = createEnv({ + server, + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as ViteEnv; export default defineConfig({ server: { - port: parseInt(process.env.PORT ?? "5173", 10), + port: viteEnv.PORT, host: "127.0.0.1", }, resolve: { tsconfigPaths: true }, diff --git a/bun.lock b/bun.lock index 8d5efcfab1..1dd59e990b 100644 --- a/bun.lock +++ b/bun.lock @@ -47,6 +47,7 @@ "@effect-atom/atom-react": "^0.5.0", "@effect/platform": "catalog:", "@executor/api": "workspace:*", + "@executor/env": "workspace:*", "@executor/execution": "workspace:*", "@executor/plugin-google-discovery": "workspace:*", "@executor/plugin-graphql": "workspace:*", From d3fede2a77ebe0ce20a5e5718bdca73b976c8410 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:46:06 -0700 Subject: [PATCH 2/5] refactor(cloud): export shared web and server env --- apps/cloud/src/api.ts | 6 +++--- apps/cloud/src/auth/handlers.ts | 4 ++-- apps/cloud/src/auth/workos.ts | 8 ++++---- apps/cloud/src/env.ts | 32 +++++++++++++++++++++++++++----- apps/cloud/src/services/db.ts | 8 ++++---- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/apps/cloud/src/api.ts b/apps/cloud/src/api.ts index 0b619dbb8d..da5dcaf7e9 100644 --- a/apps/cloud/src/api.ts +++ b/apps/cloud/src/api.ts @@ -31,7 +31,7 @@ import { CloudAuthHandlers, CloudAuthPublicHandlers } from "./auth/handlers"; import { WorkOSAuth } from "./auth/workos"; import { DbService } from "./services/db"; import { createTeamExecutor } from "./services/executor"; -import { env } from "./env"; +import { server } from "./env"; const ProtectedCloudApi = addGroup(OpenApiGroup) .add(McpGroup) @@ -105,7 +105,7 @@ const COOKIE_OPTIONS = { httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: env.NODE_ENV === "production", + secure: server.NODE_ENV === "production", }; const resolveAuth = (request: Request) => @@ -152,7 +152,7 @@ const resolveExecutor = (teamId: string) => const users = yield* UserStoreService; const team = yield* users.use((store) => store.getTeam(teamId)); const teamName = team?.name ?? "Unknown Team"; - const encryptionKey = env.ENCRYPTION_KEY; + const encryptionKey = server.ENCRYPTION_KEY; return yield* createTeamExecutor(teamId, teamName, encryptionKey); }); diff --git a/apps/cloud/src/auth/handlers.ts b/apps/cloud/src/auth/handlers.ts index f0a2798c5a..049281c1d0 100644 --- a/apps/cloud/src/auth/handlers.ts +++ b/apps/cloud/src/auth/handlers.ts @@ -6,14 +6,14 @@ import { addGroup } from "@executor/api"; import { AUTH_PATHS, CloudAuthApi, CloudAuthPublicApi } from "./api"; import { AuthContext, UserStoreService } from "./context"; import { WorkOSAuth } from "./workos"; -import { env } from "../env"; +import { server } from "../env"; const COOKIE_OPTIONS = { path: "/", httpOnly: true, sameSite: "lax" as const, maxAge: 60 * 60 * 24 * 7, - secure: env.NODE_ENV === "production", + secure: server.NODE_ENV === "production", }; // --------------------------------------------------------------------------- diff --git a/apps/cloud/src/auth/workos.ts b/apps/cloud/src/auth/workos.ts index fd9d724d80..ae7ddc3294 100644 --- a/apps/cloud/src/auth/workos.ts +++ b/apps/cloud/src/auth/workos.ts @@ -5,7 +5,7 @@ import { Context, Effect, Layer } from "effect"; import { WorkOS } from "@workos-inc/node"; import { WorkOSError } from "./errors"; -import { env } from "../env"; +import { server } from "../env"; const COOKIE_NAME = "wos-session"; @@ -15,9 +15,9 @@ const COOKIE_NAME = "wos-session"; const make = Effect.gen(function* () { - const apiKey = env.WORKOS_API_KEY; - const clientId = env.WORKOS_CLIENT_ID; - const cookiePassword = env.WORKOS_COOKIE_PASSWORD; + const apiKey = server.WORKOS_API_KEY; + const clientId = server.WORKOS_CLIENT_ID; + const cookiePassword = server.WORKOS_COOKIE_PASSWORD; if (!cookiePassword || cookiePassword.length < 32) { return yield* Effect.die(new Error("WORKOS_COOKIE_PASSWORD must be at least 32 characters")); diff --git a/apps/cloud/src/env.ts b/apps/cloud/src/env.ts index 3cefbe52cf..ea121d0faf 100644 --- a/apps/cloud/src/env.ts +++ b/apps/cloud/src/env.ts @@ -1,6 +1,6 @@ import { createEnv, Env } from "@executor/env"; -const server = { +const sharedShape = { NODE_ENV: Env.literalOr( "NODE_ENV", "development", @@ -8,6 +8,9 @@ const server = { "test", "production", ), +}; + +const serverShape = { DATABASE_URL: Env.stringOr("DATABASE_URL", ""), PGLITE_DATA_DIR: Env.stringOr("PGLITE_DATA_DIR", ".pglite"), ENCRYPTION_KEY: Env.stringOr( @@ -19,8 +22,11 @@ const server = { WORKOS_COOKIE_PASSWORD: Env.string("WORKOS_COOKIE_PASSWORD"), }; -type CloudEnv = Readonly<{ +type SharedEnv = Readonly<{ NODE_ENV: "development" | "test" | "production"; +}>; + +type ServerEnv = SharedEnv & Readonly<{ DATABASE_URL: string; PGLITE_DATA_DIR: string; ENCRYPTION_KEY: string; @@ -29,8 +35,24 @@ type CloudEnv = Readonly<{ WORKOS_COOKIE_PASSWORD: string; }>; -export const env = createEnv({ - server, +type WebEnv = Readonly>; + +export const shared = createEnv({ + server: sharedShape, + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as SharedEnv; + +export const web = createEnv({ + clientPrefix: "PUBLIC_", + client: {}, + runtimeEnv: process.env, + emptyStringAsUndefined: true, +}) as WebEnv; + +export const server = createEnv({ + server: serverShape, + extends: [shared], runtimeEnv: process.env, emptyStringAsUndefined: true, -}) as CloudEnv; +}) as ServerEnv; diff --git a/apps/cloud/src/services/db.ts b/apps/cloud/src/services/db.ts index dec76a8de8..c954ada5af 100644 --- a/apps/cloud/src/services/db.ts +++ b/apps/cloud/src/services/db.ts @@ -7,7 +7,7 @@ import { resolve } from "node:path"; import * as sharedSchema from "@executor/storage-postgres/schema"; import * as cloudSchema from "./schema"; import type { DrizzleDb } from "@executor/storage-postgres"; -import { env } from "../env"; +import { server } from "../env"; const schema = { ...sharedSchema, ...cloudSchema }; @@ -24,11 +24,11 @@ type DbResource = { }; const createDbResource = async (): Promise => { - if (env.DATABASE_URL) { + if (server.DATABASE_URL) { const { drizzle } = await import("drizzle-orm/node-postgres"); const { migrate } = await import("drizzle-orm/node-postgres/migrator"); const { Pool } = await import("pg"); - const pool = new Pool({ connectionString: env.DATABASE_URL }); + const pool = new Pool({ connectionString: server.DATABASE_URL }); const db = drizzle(pool, { schema }) as DrizzleDb; await migrate(db as any, { migrationsFolder: MIGRATIONS_DIR }); return { @@ -40,7 +40,7 @@ const createDbResource = async (): Promise => { const { PGlite } = await import("@electric-sql/pglite"); const { drizzle } = await import("drizzle-orm/pglite"); const { migrate } = await import("drizzle-orm/pglite/migrator"); - const dataDir = env.PGLITE_DATA_DIR; + const dataDir = server.PGLITE_DATA_DIR; const client = new PGlite(dataDir); const db = drizzle(client, { schema }) as DrizzleDb; await migrate(db, { migrationsFolder: MIGRATIONS_DIR }); From 9d648ecc902b07fc02ecfb82b215c6b4494f5235 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:26:36 -0700 Subject: [PATCH 3/5] refactor(cloud): use simplified createEnv signature --- apps/cloud/src/env.ts | 11 ++++------- apps/cloud/vite.config.ts | 3 +-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/cloud/src/env.ts b/apps/cloud/src/env.ts index ea121d0faf..1466de9a92 100644 --- a/apps/cloud/src/env.ts +++ b/apps/cloud/src/env.ts @@ -37,21 +37,18 @@ type ServerEnv = SharedEnv & Readonly<{ type WebEnv = Readonly>; -export const shared = createEnv({ - server: sharedShape, +export const shared = createEnv(sharedShape, { runtimeEnv: process.env, emptyStringAsUndefined: true, }) as SharedEnv; -export const web = createEnv({ - clientPrefix: "PUBLIC_", - client: {}, +export const web = createEnv({}, { + prefix: "PUBLIC_", runtimeEnv: process.env, emptyStringAsUndefined: true, }) as WebEnv; -export const server = createEnv({ - server: serverShape, +export const server = createEnv(serverShape, { extends: [shared], runtimeEnv: process.env, emptyStringAsUndefined: true, diff --git a/apps/cloud/vite.config.ts b/apps/cloud/vite.config.ts index 285a7aae8d..17162a3b6b 100644 --- a/apps/cloud/vite.config.ts +++ b/apps/cloud/vite.config.ts @@ -12,8 +12,7 @@ type ViteEnv = Readonly<{ PORT: number; }>; -const viteEnv = createEnv({ - server, +const viteEnv = createEnv(server, { runtimeEnv: process.env, emptyStringAsUndefined: true, }) as ViteEnv; From aa604e2bd60ed3bba466968f27456fe7a956ee39 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:53:16 -0700 Subject: [PATCH 4/5] refactor(cloud): simplify authAtom type annotation --- apps/cloud/src/web/auth.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/cloud/src/web/auth.tsx b/apps/cloud/src/web/auth.tsx index e0c9d16f2f..df88b13561 100644 --- a/apps/cloud/src/web/auth.tsx +++ b/apps/cloud/src/web/auth.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext } from "react"; -import { Atom, useAtomValue, Result } from "@effect-atom/atom-react"; +import { useAtomValue, Result } from "@effect-atom/atom-react"; import { CloudApiClient } from "./client"; @@ -19,16 +19,11 @@ type AuthTeam = { name: string; }; -type MeResponse = { - user: AuthUser; - team: AuthTeam | null; -}; - // --------------------------------------------------------------------------- // Auth atom — typed query against CloudAuthApi // --------------------------------------------------------------------------- -export const authAtom: Atom.Atom> = +export const authAtom: ReturnType> = CloudApiClient.query("cloudAuth", "me", { timeToLive: "5 minutes", }); From 1b950e4d4418a3517716bfc337e3c7fe397ce93c Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:57:34 -0700 Subject: [PATCH 5/5] fix(cloud): avoid TS2742 by disabling declaration emit --- apps/cloud/src/web/auth.tsx | 2 +- apps/cloud/tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cloud/src/web/auth.tsx b/apps/cloud/src/web/auth.tsx index df88b13561..e746966a97 100644 --- a/apps/cloud/src/web/auth.tsx +++ b/apps/cloud/src/web/auth.tsx @@ -23,7 +23,7 @@ type AuthTeam = { // Auth atom — typed query against CloudAuthApi // --------------------------------------------------------------------------- -export const authAtom: ReturnType> = +export const authAtom = CloudApiClient.query("cloudAuth", "me", { timeToLive: "5 minutes", }); diff --git a/apps/cloud/tsconfig.json b/apps/cloud/tsconfig.json index e9713c832b..7d2b670967 100644 --- a/apps/cloud/tsconfig.json +++ b/apps/cloud/tsconfig.json @@ -8,8 +8,8 @@ "skipLibCheck": true, "outDir": "dist", "rootDir": ".", - "declaration": true, - "declarationMap": true, + "declaration": false, + "declarationMap": false, "sourceMap": true, "jsx": "react-jsx", "plugins": [