Canvas: App wide env vars

Change-Id: Ia4101ba7c646e33591a1ddd642622e2712865b71
diff --git a/apps/canvas/back/prisma/migrations/20250708064115_env_vars/migration.sql b/apps/canvas/back/prisma/migrations/20250708064115_env_vars/migration.sql
new file mode 100644
index 0000000..f08a271
--- /dev/null
+++ b/apps/canvas/back/prisma/migrations/20250708064115_env_vars/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "envVars" TEXT;
diff --git a/apps/canvas/back/prisma/schema.prisma b/apps/canvas/back/prisma/schema.prisma
index e80b827..3eb8df9 100644
--- a/apps/canvas/back/prisma/schema.prisma
+++ b/apps/canvas/back/prisma/schema.prisma
@@ -22,6 +22,7 @@
   instanceId      String?
   deployKey       String?
   deployKeyPublic String?
+  envVars         String?
   githubToken     String?
   access          String?
   geminiApiKey    String?
diff --git a/apps/canvas/back/src/app_manager.ts b/apps/canvas/back/src/app_manager.ts
index 34bdd5e..4fa8cfd 100644
--- a/apps/canvas/back/src/app_manager.ts
+++ b/apps/canvas/back/src/app_manager.ts
@@ -50,6 +50,7 @@
 	id: z.string(),
 	deployKey: z.string(),
 	access: z.array(accessSchema),
+	envVars: z.array(z.object({ name: z.string(), value: z.string() })),
 });
 
 export type DeployResponse = z.infer<typeof DeployResponseSchema>;
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 1fd9b37..97df61f 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -4,7 +4,7 @@
 import { env } from "node:process";
 import axios from "axios";
 import { GithubClient } from "./github.js";
-import { AppManager } from "./app_manager.js";
+import { AppManager, DeployResponse } from "./app_manager.js";
 import { z } from "zod";
 import { ProjectMonitor, WorkerSchema, LogItem } from "./project_monitor.js";
 import tmp from "tmp";
@@ -386,34 +386,31 @@
 			},
 		};
 		try {
+			let deployResponse: DeployResponse | null = null;
 			if (p.instanceId == null) {
-				const deployResponse = await appManager.deploy(cfg);
-				await db.project.update({
-					where: {
-						id: projectId,
-					},
-					data: {
-						state: JSON.stringify(graph),
-						draft: null,
-						instanceId: deployResponse.id,
-						access: JSON.stringify(deployResponse.access),
-					},
-				});
+				deployResponse = await appManager.deploy(cfg);
 				diff = { toAdd: extractGithubRepos(JSON.stringify(graph)) };
 			} else {
-				const deployResponse = await appManager.update(p.instanceId, cfg);
+				deployResponse = await appManager.update(p.instanceId, cfg);
 				diff = calculateRepoDiff(extractGithubRepos(p.state), extractGithubRepos(JSON.stringify(graph)));
-				await db.project.update({
-					where: {
-						id: projectId,
-					},
-					data: {
-						state: JSON.stringify(graph),
-						draft: null,
-						access: JSON.stringify(deployResponse.access),
-					},
-				});
 			}
+			if (deployResponse == null) {
+				resp.status(500);
+				resp.write(JSON.stringify({ error: "Failed to deploy" }));
+				return;
+			}
+			await db.project.update({
+				where: {
+					id: projectId,
+				},
+				data: {
+					state: JSON.stringify(graph),
+					draft: null,
+					instanceId: deployResponse.id,
+					access: JSON.stringify(deployResponse.access),
+					envVars: JSON.stringify(deployResponse.envVars),
+				},
+			});
 			if (diff && p.githubToken && deployKey) {
 				const github = new GithubClient(p.githubToken);
 				await manageGithubRepos(github, diff, deployKeyPublic!, env.PUBLIC_ADDR);
@@ -819,6 +816,7 @@
 			hasAuth: z.boolean(),
 		}),
 	),
+	envVars: z.array(z.object({ name: z.string(), value: z.string() })),
 });
 
 type InternalEnv = z.infer<typeof internalEnvSchema>;
@@ -833,12 +831,14 @@
 			},
 			select: {
 				githubToken: true,
+				envVars: true,
 			},
 		});
 		const networks = getNetworks(resp.locals.username);
 		const env: InternalEnv = {
 			networks,
 			githubToken: project?.githubToken ?? undefined,
+			envVars: JSON.parse(project?.envVars ?? "[]"),
 		};
 		resp.status(200);
 		resp.write(JSON.stringify(env));