Canvas: Edit/Deploy mode

Change-Id: I51e5b6c2a1f06009433b0d0824ffcf3dfe39d34e
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index 653eeaa..3c276d7 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -79,24 +79,26 @@
 	}
 };
 
-const handleSavedGet: express.Handler = async (req, resp) => {
-	try {
-		const r = await db.project.findUnique({
-			where: {
-				id: Number(req.params["projectId"]),
-				userId: resp.locals.userId,
-			},
-			select: {
-				state: true,
-				draft: true,
-			},
-		});
-		if (r == null) {
-			resp.status(404);
-		} else {
+function handleSavedGet(state: "deploy" | "draft"): express.Handler {
+	return async (req, resp) => {
+		try {
+			const r = await db.project.findUnique({
+				where: {
+					id: Number(req.params["projectId"]),
+					userId: resp.locals.userId,
+				},
+				select: {
+					state: true,
+					draft: true,
+				},
+			});
+			if (r == null) {
+				resp.status(404);
+				return;
+			}
 			resp.status(200);
 			resp.header("content-type", "application/json");
-			if (r.draft == null) {
+			if (state === "deploy") {
 				if (r.state == null) {
 					resp.send({
 						nodes: [],
@@ -107,16 +109,28 @@
 					resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
 				}
 			} else {
-				resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
+				if (r.draft == null) {
+					if (r.state == null) {
+						resp.send({
+							nodes: [],
+							edges: [],
+							viewport: { x: 0, y: 0, zoom: 1 },
+						});
+					} else {
+						resp.send(JSON.parse(Buffer.from(r.state).toString("utf8")));
+					}
+				} else {
+					resp.send(JSON.parse(Buffer.from(r.draft).toString("utf8")));
+				}
 			}
+		} catch (e) {
+			console.log(e);
+			resp.status(500);
+		} finally {
+			resp.end();
 		}
-	} catch (e) {
-		console.log(e);
-		resp.status(500);
-	} finally {
-		resp.end();
-	}
-};
+	};
+}
 
 const handleDelete: express.Handler = async (req, resp) => {
 	try {
@@ -554,7 +568,8 @@
 	app.use(express.json());
 	app.use(auth);
 	app.post("/api/project/:projectId/saved", handleSave);
-	app.get("/api/project/:projectId/saved", handleSavedGet);
+	app.get("/api/project/:projectId/saved/deploy", handleSavedGet("deploy"));
+	app.get("/api/project/:projectId/saved/draft", handleSavedGet("draft"));
 	app.post("/api/project/:projectId/deploy", handleDeploy);
 	app.get("/api/project/:projectId/status", handleStatus);
 	app.delete("/api/project/:projectId", handleDelete);
diff --git a/apps/canvas/front/package-lock.json b/apps/canvas/front/package-lock.json
index 856057f..94ca1b2 100644
--- a/apps/canvas/front/package-lock.json
+++ b/apps/canvas/front/package-lock.json
@@ -14,6 +14,7 @@
 				"@radix-ui/react-checkbox": "^1.3.1",
 				"@radix-ui/react-collapsible": "^1.1.1",
 				"@radix-ui/react-dialog": "^1.1.2",
+				"@radix-ui/react-dropdown-menu": "^2.1.14",
 				"@radix-ui/react-icons": "^1.3.1",
 				"@radix-ui/react-label": "^2.1.0",
 				"@radix-ui/react-popover": "^1.1.2",
@@ -1476,6 +1477,164 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-dropdown-menu": {
+			"version": "2.1.14",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.14.tgz",
+			"integrity": "sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.2",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-id": "1.1.1",
+				"@radix-ui/react-menu": "2.1.14",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-controllable-state": "1.2.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/primitive": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+			"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+			"license": "MIT"
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-compose-refs": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+			"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+			"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-id": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+			"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
+			"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-slot": "1.2.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
+			"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-controllable-state": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+			"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-effect-event": "0.0.2",
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-use-layout-effect": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+			"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-focus-guards": {
 			"version": "1.1.1",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
@@ -1561,6 +1720,517 @@
 				}
 			}
 		},
+		"node_modules/@radix-ui/react-menu": {
+			"version": "2.1.14",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.14.tgz",
+			"integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.2",
+				"@radix-ui/react-collection": "1.1.6",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-direction": "1.1.1",
+				"@radix-ui/react-dismissable-layer": "1.1.9",
+				"@radix-ui/react-focus-guards": "1.1.2",
+				"@radix-ui/react-focus-scope": "1.1.6",
+				"@radix-ui/react-id": "1.1.1",
+				"@radix-ui/react-popper": "1.2.6",
+				"@radix-ui/react-portal": "1.1.8",
+				"@radix-ui/react-presence": "1.1.4",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-roving-focus": "1.1.9",
+				"@radix-ui/react-slot": "1.2.2",
+				"@radix-ui/react-use-callback-ref": "1.1.1",
+				"aria-hidden": "^1.2.4",
+				"react-remove-scroll": "^2.6.3"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/primitive": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+			"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+			"license": "MIT"
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": {
+			"version": "1.1.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz",
+			"integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.1.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": {
+			"version": "1.1.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
+			"integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-slot": "1.2.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-compose-refs": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+			"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+			"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-direction": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+			"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": {
+			"version": "1.1.9",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz",
+			"integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.2",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-callback-ref": "1.1.1",
+				"@radix-ui/react-use-escape-keydown": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-guards": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+			"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": {
+			"version": "1.1.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz",
+			"integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-callback-ref": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-id": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+			"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": {
+			"version": "1.2.6",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz",
+			"integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==",
+			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/react-dom": "^2.0.0",
+				"@radix-ui/react-arrow": "1.1.6",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-callback-ref": "1.1.1",
+				"@radix-ui/react-use-layout-effect": "1.1.1",
+				"@radix-ui/react-use-rect": "1.1.1",
+				"@radix-ui/react-use-size": "1.1.1",
+				"@radix-ui/rect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": {
+			"version": "1.1.8",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz",
+			"integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-presence": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+			"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
+			"version": "2.1.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
+			"integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-slot": "1.2.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-roving-focus": {
+			"version": "1.1.9",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz",
+			"integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/primitive": "1.1.2",
+				"@radix-ui/react-collection": "1.1.6",
+				"@radix-ui/react-compose-refs": "1.1.2",
+				"@radix-ui/react-context": "1.1.2",
+				"@radix-ui/react-direction": "1.1.1",
+				"@radix-ui/react-id": "1.1.1",
+				"@radix-ui/react-primitive": "2.1.2",
+				"@radix-ui/react-use-callback-ref": "1.1.1",
+				"@radix-ui/react-use-controllable-state": "1.2.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"@types/react-dom": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+				"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				},
+				"@types/react-dom": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
+			"integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-compose-refs": "1.1.2"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-callback-ref": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+			"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-controllable-state": {
+			"version": "1.2.2",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+			"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-effect-event": "0.0.2",
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-escape-keydown": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+			"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-callback-ref": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-layout-effect": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+			"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+			"license": "MIT",
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-rect": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+			"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/rect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-use-size": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+			"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@radix-ui/react-use-layout-effect": "1.1.1"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/rect": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+			"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+			"license": "MIT"
+		},
+		"node_modules/@radix-ui/react-menu/node_modules/react-remove-scroll": {
+			"version": "2.6.3",
+			"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz",
+			"integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==",
+			"license": "MIT",
+			"dependencies": {
+				"react-remove-scroll-bar": "^2.3.7",
+				"react-style-singleton": "^2.2.3",
+				"tslib": "^2.1.0",
+				"use-callback-ref": "^1.3.3",
+				"use-sidecar": "^1.1.3"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"peerDependencies": {
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+			},
+			"peerDependenciesMeta": {
+				"@types/react": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/@radix-ui/react-popover": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz",
@@ -4173,14 +4843,6 @@
 				"node": ">=0.8.19"
 			}
 		},
-		"node_modules/invariant": {
-			"version": "2.2.4",
-			"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
-			"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
-			"dependencies": {
-				"loose-envify": "^1.0.0"
-			}
-		},
 		"node_modules/is-arrayish": {
 			"version": "0.3.2",
 			"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -4990,19 +5652,20 @@
 			}
 		},
 		"node_modules/react-remove-scroll-bar": {
-			"version": "2.3.6",
-			"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
-			"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
+			"version": "2.3.8",
+			"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+			"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+			"license": "MIT",
 			"dependencies": {
-				"react-style-singleton": "^2.2.1",
+				"react-style-singleton": "^2.2.2",
 				"tslib": "^2.0.0"
 			},
 			"engines": {
 				"node": ">=10"
 			},
 			"peerDependencies": {
-				"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-				"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
 			},
 			"peerDependenciesMeta": {
 				"@types/react": {
@@ -5020,20 +5683,20 @@
 			}
 		},
 		"node_modules/react-style-singleton": {
-			"version": "2.2.1",
-			"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
-			"integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+			"version": "2.2.3",
+			"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+			"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+			"license": "MIT",
 			"dependencies": {
 				"get-nonce": "^1.0.0",
-				"invariant": "^2.2.4",
 				"tslib": "^2.0.0"
 			},
 			"engines": {
 				"node": ">=10"
 			},
 			"peerDependencies": {
-				"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-				"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
 			},
 			"peerDependenciesMeta": {
 				"@types/react": {
@@ -5581,9 +6244,10 @@
 			}
 		},
 		"node_modules/use-callback-ref": {
-			"version": "1.3.2",
-			"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
-			"integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
+			"version": "1.3.3",
+			"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+			"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+			"license": "MIT",
 			"dependencies": {
 				"tslib": "^2.0.0"
 			},
@@ -5591,8 +6255,8 @@
 				"node": ">=10"
 			},
 			"peerDependencies": {
-				"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
-				"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
 			},
 			"peerDependenciesMeta": {
 				"@types/react": {
@@ -5643,9 +6307,10 @@
 			}
 		},
 		"node_modules/use-sidecar": {
-			"version": "1.1.2",
-			"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
-			"integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+			"version": "1.1.3",
+			"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+			"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+			"license": "MIT",
 			"dependencies": {
 				"detect-node-es": "^1.1.0",
 				"tslib": "^2.0.0"
@@ -5654,8 +6319,8 @@
 				"node": ">=10"
 			},
 			"peerDependencies": {
-				"@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
-				"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+				"@types/react": "*",
+				"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
 			},
 			"peerDependenciesMeta": {
 				"@types/react": {
diff --git a/apps/canvas/front/package.json b/apps/canvas/front/package.json
index c99ec28..55116cb 100644
--- a/apps/canvas/front/package.json
+++ b/apps/canvas/front/package.json
@@ -22,6 +22,7 @@
 		"@radix-ui/react-checkbox": "^1.3.1",
 		"@radix-ui/react-collapsible": "^1.1.1",
 		"@radix-ui/react-dialog": "^1.1.2",
+		"@radix-ui/react-dropdown-menu": "^2.1.14",
 		"@radix-ui/react-icons": "^1.3.1",
 		"@radix-ui/react-label": "^2.1.0",
 		"@radix-ui/react-popover": "^1.1.2",
diff --git a/apps/canvas/front/src/Canvas.tsx b/apps/canvas/front/src/Canvas.tsx
index c794ade..453176c 100644
--- a/apps/canvas/front/src/Canvas.tsx
+++ b/apps/canvas/front/src/Canvas.tsx
@@ -3,19 +3,25 @@
 import { Details } from "@/components/details";
 import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./components/ui/resizable";
 import { Tools } from "./Tootls";
+import { useStateStore } from "./lib/state";
 
 export function CanvasBuilder() {
+	const store = useStateStore();
 	return (
 		<ResizablePanelGroup direction="horizontal" className="w-full h-full">
 			<ResizablePanel defaultSize={80}>
 				<ResizablePanelGroup direction="vertical">
 					<ResizablePanel defaultSize={80}>
 						<ResizablePanelGroup direction="horizontal">
-							<ResizablePanel defaultSize={15}>
-								<Resources />
-							</ResizablePanel>
-							<ResizableHandle withHandle />
-							<ResizablePanel defaultSize={85}>
+							{store.mode === "edit" && (
+								<>
+									<ResizablePanel defaultSize={15}>
+										<Resources />
+									</ResizablePanel>
+									<ResizableHandle withHandle />
+								</>
+							)}
+							<ResizablePanel defaultSize={store.mode === "edit" ? 85 : 100}>
 								<Canvas />
 							</ResizablePanel>
 						</ResizablePanelGroup>
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 509b51c..f292c30 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -4,6 +4,14 @@
 import { generateDodoConfig } from "@/lib/config";
 import { useNodes, useReactFlow } from "@xyflow/react";
 import { useToast } from "@/hooks/use-toast";
+import {
+	DropdownMenuGroup,
+	DropdownMenuItem,
+	DropdownMenu,
+	DropdownMenuContent,
+	DropdownMenuTrigger,
+} from "./ui/dropdown-menu";
+import { Menu } from "lucide-react";
 
 function toNodeType(t: string): string {
 	if (t === "ingress") {
@@ -64,6 +72,7 @@
 			return;
 		}
 		setLoading(true);
+		store.setMode("deploy");
 		try {
 			const config = generateDodoConfig(projectId, nodes, env);
 			if (config == null) {
@@ -92,6 +101,7 @@
 				});
 			}
 		} catch (e) {
+			store.setMode("edit");
 			console.log(e);
 			toast({
 				variant: "destructive",
@@ -100,7 +110,7 @@
 		} finally {
 			setLoading(false);
 		}
-	}, [projectId, instance, nodes, env, setLoading, toast, monitor]);
+	}, [projectId, instance, nodes, env, setLoading, toast, monitor, store]);
 	const save = useCallback(async () => {
 		if (projectId == null) {
 			return;
@@ -128,7 +138,7 @@
 		if (projectId == null) {
 			return;
 		}
-		const resp = await fetch(`/api/project/${projectId}/saved`, {
+		const resp = await fetch(`/api/project/${projectId}/saved/${store.mode === "deploy" ? "deploy" : "draft"}`, {
 			method: "GET",
 		});
 		const inst = await resp.json();
@@ -142,7 +152,9 @@
 		store.setNodes([]);
 		instance.setViewport({ x: 0, y: 0, zoom: 1 });
 	}, [store, instance]);
-	// TODO(gio): Update store
+	const edit = useCallback(async () => {
+		store.setMode("edit");
+	}, [store]);
 	const deleteProject = useCallback(async () => {
 		if (projectId == null) {
 			return;
@@ -154,12 +166,12 @@
 			clear();
 			store.setProject(undefined);
 			toast({
-				title: "Save succeeded",
+				title: "Project deleted",
 			});
 		} else {
 			toast({
 				variant: "destructive",
-				title: "Save failed",
+				title: "Failed to delete project",
 				description: await resp.text(),
 			});
 		}
@@ -214,22 +226,71 @@
 			setReloadProps({ disabled: projectId === undefined });
 		}
 	}, [ok, loading, reloading, projectId]);
-	return (
-		<>
-			<Button onClick={deploy} {...deployProps}>
-				Deploy
-			</Button>
-			<Button onClick={reload} {...reloadProps}>
-				Reload
-			</Button>
-			<Button onClick={save}>Save</Button>
-			<Button onClick={restoreSaved}>Restore</Button>
-			<Button onClick={clear} variant="destructive">
-				Clear
-			</Button>
-			<Button onClick={deleteProject} variant="destructive" disabled={projectId === undefined}>
-				Delete
-			</Button>
-		</>
-	);
+	if (store.mode === "deploy") {
+		return (
+			<div className="flex flex-row gap-1 items-center">
+				<Button onClick={edit} {...reloadProps}>
+					Edit
+				</Button>
+				<DropdownMenu>
+					<DropdownMenuTrigger>
+						<Menu className="rounded-md bg-gray-200 opacity-50" />
+					</DropdownMenuTrigger>
+					<DropdownMenuContent className="w-56">
+						<DropdownMenuGroup>
+							<DropdownMenuItem
+								onClick={reload}
+								className="cursor-pointer hover:bg-gray-200"
+								{...reloadProps}
+							>
+								Reload Services
+							</DropdownMenuItem>
+							<DropdownMenuItem
+								onClick={deleteProject}
+								disabled={projectId === undefined}
+								className="cursor-pointer hover:bg-gray-200"
+							>
+								Delete Project
+							</DropdownMenuItem>
+						</DropdownMenuGroup>
+					</DropdownMenuContent>
+				</DropdownMenu>
+			</div>
+		);
+	} else {
+		return (
+			<div className="flex flex-row gap-1 items-center">
+				<Button onClick={deploy} {...deployProps}>
+					Deploy
+				</Button>
+				<Button onClick={save}>Save</Button>
+				<DropdownMenu>
+					<DropdownMenuTrigger>
+						<Menu />
+					</DropdownMenuTrigger>
+					<DropdownMenuContent className="w-56">
+						<DropdownMenuGroup>
+							<DropdownMenuItem
+								onClick={restoreSaved}
+								disabled={projectId === undefined}
+								className="cursor-pointer hover:bg-gray-200"
+							>
+								Restore
+							</DropdownMenuItem>
+							<DropdownMenuItem onClick={clear} className="cursor-pointer hover:bg-gray-200">
+								Clear
+							</DropdownMenuItem>
+							<DropdownMenuItem
+								onClick={deleteProject}
+								disabled={projectId === undefined}
+								className="cursor-pointer hover:bg-gray-200"
+							>
+								Delete Project
+							</DropdownMenuItem>
+						</DropdownMenuGroup>
+					</DropdownMenuContent>
+				</DropdownMenu>
+			</div>
+		);
+	}
 }
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 122de3f..8ab4aaa 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -123,8 +123,12 @@
 				fitView
 				proOptions={{ hideAttribution: true }}
 			>
-				<Controls />
-				<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
+				<Controls showInteractive={false} />
+				<Background
+					variant={store.mode === "deploy" ? BackgroundVariant.Dots : BackgroundVariant.Lines}
+					gap={12}
+					size={1}
+				/>
 				<Panel position="bottom-right">
 					<Actions />
 				</Panel>
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index cfbbbde..d2aa36e 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -11,6 +11,7 @@
 	GatewayTCPNode,
 	GatewayHttpsNode,
 	AppNode,
+	GithubNode,
 } from "@/lib/state";
 import { KeyboardEvent, FocusEvent, useCallback, useEffect, useMemo, useState } from "react";
 import { z } from "zod";
@@ -69,7 +70,7 @@
 
 const portSchema = z.object({
 	name: z.string().min(1, "required"),
-	value: z.coerce.number().gt(0, "can not be negative"),
+	value: z.coerce.number().gt(0, "must be positive").lte(65535, "must be less than 65535"),
 });
 
 const sourceSchema = z.object({
@@ -103,7 +104,7 @@
 			store.updateNodeData<"app">(id, {
 				ports: (data.ports || []).concat({
 					id: portId,
-					name: values.name,
+					name: values.name.toLowerCase(),
 					value: values.value,
 				}),
 				envVars: (data.envVars || []).concat({
@@ -476,7 +477,7 @@
 											<SelectItem
 												key={n.id}
 												value={n.id}
-											>{`${n.data.repository?.sshURL}`}</SelectItem>
+											>{`${n.data.repository?.fullName}`}</SelectItem>
 										))}
 									</SelectContent>
 								</Select>
@@ -515,11 +516,13 @@
 				{data &&
 					data.ports &&
 					data.ports.map((p) => (
-						<li key={p.id}>
+						<li key={p.id} className="flex flex-row items-center gap-1">
 							<Button size={"icon"} variant={"ghost"} onClick={() => removePort(p.id)}>
 								<XIcon />
-							</Button>{" "}
-							{p.name} - {p.value}
+							</Button>
+							<div>
+								{p.name} - {p.value}
+							</div>
 						</li>
 					))}
 			</ul>
@@ -578,10 +581,12 @@
 									<TooltipProvider>
 										<Tooltip>
 											<TooltipTrigger>
-												<Button size={"icon"} variant={"ghost"}>
-													<PencilIcon />
-												</Button>
-												{value}
+												<div className="flex flex-row items-center gap-1">
+													<Button size={"icon"} variant={"ghost"}>
+														<PencilIcon />
+													</Button>
+													<div>{value}</div>
+												</div>
 											</TooltipTrigger>
 											<TooltipContent>{v.name}</TooltipContent>
 										</Tooltip>
diff --git a/apps/canvas/front/src/components/node-gateway-https.tsx b/apps/canvas/front/src/components/node-gateway-https.tsx
index e3f2c42..07eb3c1 100644
--- a/apps/canvas/front/src/components/node-gateway-https.tsx
+++ b/apps/canvas/front/src/components/node-gateway-https.tsx
@@ -390,8 +390,10 @@
 						name="enabled"
 						render={({ field }) => (
 							<FormItem>
-								<Checkbox id="authEnabled" onCheckedChange={field.onChange} checked={field.value} />
-								<Label htmlFor="authEnabled">Enabled</Label>
+								<div className="flex flex-row gap-1 items-center">
+									<Checkbox id="authEnabled" onCheckedChange={field.onChange} checked={field.value} />
+									<Label htmlFor="authEnabled">Enabled</Label>
+								</div>
 								<FormMessage />
 							</FormItem>
 						)}
@@ -403,11 +405,11 @@
 					Authorized Groups
 					<ul>
 						{(data.auth.groups || []).map((p) => (
-							<li key={p}>
+							<li key={p} className="flex flex-row gap-1 items-center">
 								<Button size={"icon"} variant={"ghost"} onClick={() => removeGroup(p)}>
 									<XIcon />
-								</Button>{" "}
-								{p}
+								</Button>
+								<div>{p}</div>
 							</li>
 						))}
 					</ul>
@@ -431,11 +433,11 @@
 					Auth optional path patterns
 					<ul>
 						{(data.auth.noAuthPathPatterns || []).map((p) => (
-							<li key={p}>
+							<li key={p} className="flex flex-row gap-1 items-center">
 								<Button size={"icon"} variant={"ghost"} onClick={() => removeNoAuthPathPattern(p)}>
 									<XIcon />
-								</Button>{" "}
-								{p}
+								</Button>
+								<div>{p}</div>
 							</li>
 						))}
 					</ul>
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 37213b8..ae9d2ab 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -95,6 +95,7 @@
 									repository: {
 										id: repo.id,
 										sshURL: repo.ssh_url,
+										fullName: repo.full_name,
 									},
 								});
 							}
@@ -164,8 +165,7 @@
 									<Alert variant="destructive" className="mt-2">
 										<AlertCircle className="h-4 w-4" />
 										<AlertDescription>
-											GitHub access token is not configured. Please configure it in the
-											Integrations tab.
+											Please configure Github Personal Access Token in the Integrations tab.
 										</AlertDescription>
 									</Alert>
 								)}
diff --git a/apps/canvas/front/src/components/node-rect.tsx b/apps/canvas/front/src/components/node-rect.tsx
index 362dc54..99dbd82 100644
--- a/apps/canvas/front/src/components/node-rect.tsx
+++ b/apps/canvas/front/src/components/node-rect.tsx
@@ -7,7 +7,7 @@
 	selected?: boolean;
 	children: React.ReactNode;
 	type: NodeType;
-	state: string | null;
+	state?: string | null;
 };
 
 export function NodeRect(p: Props) {
diff --git a/apps/canvas/front/src/components/resources.tsx b/apps/canvas/front/src/components/resources.tsx
index 715f878..fbb1be7 100644
--- a/apps/canvas/front/src/components/resources.tsx
+++ b/apps/canvas/front/src/components/resources.tsx
@@ -1,15 +1,14 @@
 import { Button } from "@/components/ui/button";
-import { ReactFlowInstance, useReactFlow } from "@xyflow/react";
 import { v4 as uuidv4 } from "uuid";
 import { useCallback, useState } from "react";
 import { Accordion, AccordionTrigger } from "./ui/accordion";
 import { AccordionContent, AccordionItem } from "@radix-ui/react-accordion";
-import { NodeType, useCategories } from "@/lib/state";
+import { AppNode, AppState, NodeType, useCategories, useStateStore } from "@/lib/state";
 import { CategoryItem } from "@/lib/categories";
 import { Icon } from "./icon";
 
-function addResource(i: CategoryItem<NodeType>, flow: ReactFlowInstance) {
-	flow.addNodes({
+function addResource(i: CategoryItem<NodeType>, store: AppState) {
+	const node = {
 		id: uuidv4(),
 		position: {
 			x: 0,
@@ -18,17 +17,24 @@
 		type: i.type,
 		connectable: true,
 		data: i.init,
-	});
+		selected: true,
+	};
+	const nodes = store.nodes.map((n) => ({
+		...n,
+		selected: false,
+	}));
+	nodes.push(node as AppNode);
+	store.setNodes(nodes);
 }
 
 export function Resources() {
-	const flow = useReactFlow();
+	const store = useStateStore();
 	const categories = useCategories();
 	const onResourceAdd = useCallback(
 		(item: CategoryItem<NodeType>) => {
-			return () => addResource(item, flow);
+			return () => addResource(item, store);
 		},
-		[flow],
+		[store],
 	);
 	const [open, setOpen] = useState<string[]>(categories.map((c) => c.title));
 	return (
@@ -39,7 +45,7 @@
 						<AccordionTrigger>{c.title}</AccordionTrigger>
 						<AccordionContent>
 							<div className="flex flex-col space-y-1">
-								{c.items.map((item) => (
+								{c.items.map((item: CategoryItem<NodeType>) => (
 									<Button
 										key={item.title}
 										onClick={onResourceAdd(item)}
diff --git a/apps/canvas/front/src/components/ui/dropdown-menu.tsx b/apps/canvas/front/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..e6ac790
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,179 @@
+import * as React from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { cn } from "@/lib/utils";
+import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons";
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+		inset?: boolean;
+	}
+>(({ className, inset, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubTrigger
+		ref={ref}
+		className={cn(
+			"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}
+	>
+		{children}
+		<ChevronRightIcon className="ml-auto" />
+	</DropdownMenuPrimitive.SubTrigger>
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubContent
+		ref={ref}
+		className={cn(
+			"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
+			className,
+		)}
+		{...props}
+	/>
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+	<DropdownMenuPrimitive.Portal>
+		<DropdownMenuPrimitive.Content
+			ref={ref}
+			sideOffset={sideOffset}
+			className={cn(
+				"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+				"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
+				className,
+			)}
+			{...props}
+		/>
+	</DropdownMenuPrimitive.Portal>
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+		inset?: boolean;
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Item
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}
+	/>
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+	<DropdownMenuPrimitive.CheckboxItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		checked={checked}
+		{...props}
+	>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<CheckIcon className="h-4 w-4" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.CheckboxItem>
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.RadioItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		{...props}
+	>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<DotFilledIcon className="h-2 w-2 fill-current" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.RadioItem>
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+		inset?: boolean;
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Label
+		ref={ref}
+		className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+		{...props}
+	/>
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
+	return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />;
+};
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
+
+export {
+	DropdownMenu,
+	DropdownMenuTrigger,
+	DropdownMenuContent,
+	DropdownMenuItem,
+	DropdownMenuCheckboxItem,
+	DropdownMenuRadioItem,
+	DropdownMenuLabel,
+	DropdownMenuSeparator,
+	DropdownMenuShortcut,
+	DropdownMenuGroup,
+	DropdownMenuPortal,
+	DropdownMenuSub,
+	DropdownMenuSubContent,
+	DropdownMenuSubTrigger,
+	DropdownMenuRadioGroup,
+};
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 21dbbc4..b7fe986 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -16,7 +16,7 @@
 
 export type NodeData = InitData & {
 	activeField?: string | undefined;
-	state: string | null;
+	state?: string | null;
 };
 
 export type PortConnectedTo = {
@@ -136,6 +136,7 @@
 	repository?: {
 		id: number;
 		sshURL: string;
+		fullName: string;
 	};
 };
 
@@ -165,7 +166,7 @@
 		case "app":
 			return n.data.label || "Service";
 		case "github":
-			return n.data.repository?.sshURL || "Github";
+			return n.data.repository?.fullName || "Github";
 		case "gateway-https": {
 			if (n.data && n.data.network && n.data.subdomain) {
 				return `https://${n.data.subdomain}.${n.data.network}`;
@@ -356,6 +357,7 @@
 
 export type AppState = {
 	projectId: string | undefined;
+	mode: "edit" | "deploy";
 	projects: Project[];
 	nodes: AppNode[];
 	edges: Edge[];
@@ -369,7 +371,8 @@
 	onConnect: OnConnect;
 	setNodes: (nodes: AppNode[]) => void;
 	setEdges: (edges: Edge[]) => void;
-	setProject: (projectId: string | undefined) => void;
+	setProject: (projectId: string | undefined) => Promise<void>;
+	setMode: (mode: "edit" | "deploy") => void;
 	updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
 	updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
 	replaceEdge: (c: Connection, id?: string) => void;
@@ -428,14 +431,13 @@
 	};
 
 	const restoreSaved = async () => {
-		const resp = await fetch(`/api/project/${get().projectId}/saved`, {
+		const { projectId } = get();
+		const resp = await fetch(`/api/project/${projectId}/saved/${get().mode === "deploy" ? "deploy" : "draft"}`, {
 			method: "GET",
 		});
 		const inst = await resp.json();
-		// const { x = 0, y = 0, zoom = 1 } = inst.viewport;
 		setN(inst.nodes || []);
 		get().setEdges(inst.edges || []);
-		// instance.setViewport({ x, y, zoom });
 	};
 
 	function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
@@ -617,6 +619,7 @@
 	}
 	return {
 		projectId: undefined,
+		mode: "edit",
 		projects: [],
 		nodes: [],
 		edges: [],
@@ -712,12 +715,20 @@
 				}
 			}
 		},
-		setProject: (projectId) => {
+		setMode: (mode) => {
+			set({ mode });
+		},
+		setProject: async (projectId) => {
 			set({
 				projectId,
 			});
 			if (projectId) {
-				get().refreshEnv();
+				await get().refreshEnv();
+				if (get().env.deployKey) {
+					set({ mode: "deploy" });
+				} else {
+					set({ mode: "edit" });
+				}
 				restoreSaved();
 			} else {
 				set({