Canvas: Github repository picker

Change-Id: Icb8f2ffbef2894b2fdea4e4c13c74c0f4970506b
diff --git a/apps/canvas/back/github.ts b/apps/canvas/back/github.ts
new file mode 100644
index 0000000..f234788
--- /dev/null
+++ b/apps/canvas/back/github.ts
@@ -0,0 +1,51 @@
+import axios from 'axios';
+import { z } from 'zod';
+
+export const GithubRepositorySchema = z.object({
+    id: z.number(),
+    name: z.string(),
+    full_name: z.string(),
+    html_url: z.string(),
+    ssh_url: z.string(),
+});
+
+export type GithubRepository = z.infer<typeof GithubRepositorySchema>;
+
+export class GithubClient {
+    private token: string;
+
+    constructor(token: string) {
+        this.token = token;
+    }
+
+    private getHeaders() {
+        return {
+            "Authorization": `Bearer ${this.token}`,
+            "Accept": "application/vnd.github.v3+json",
+        };
+    }
+
+    async getRepositories(): Promise<GithubRepository[]> {
+        const response = await axios.get("https://api.github.com/user/repos", {
+            headers: this.getHeaders(),
+        });
+        return z.array(GithubRepositorySchema).parse(response.data);
+    }
+
+    async addDeployKey(repoPath: string, key: string) {
+        const sshUrl = repoPath;
+        const repoOwnerAndName = sshUrl.replace('git@github.com:', '').replace('.git', '');
+
+        await axios.post(
+            `https://api.github.com/repos/${repoOwnerAndName}/keys`,
+            {
+                title: "dodo",
+                key: key,
+                read_only: true
+            },
+            {
+                headers: this.getHeaders(),
+            }
+        );
+    }
+} 
\ No newline at end of file
diff --git a/apps/canvas/back/index.ts b/apps/canvas/back/index.ts
index 258bad9..cdcf9a0 100644
--- a/apps/canvas/back/index.ts
+++ b/apps/canvas/back/index.ts
@@ -2,6 +2,7 @@
 import express from "express";
 import { env } from "node:process";
 import axios from "axios";
+import { GithubClient } from "./github";
 
 const db = new PrismaClient();
 
@@ -134,7 +135,7 @@
                 where: {
                     id: projectId,
                 },
-            });    
+            });
         }
         resp.status(200);
     } catch (e) {
@@ -155,6 +156,8 @@
             },
             select: {
                 instanceId: true,
+                githubToken: true,
+                deployKey: true,
             },
         });
         if (p === null) {
@@ -178,6 +181,7 @@
                     config: req.body.config,
                 },
             });
+            console.log(r);
             if (r.status === 200) {
                 await db.project.update({
                     where: {
@@ -190,6 +194,20 @@
                         deployKey: r.data.deployKey,
                     },
                 });
+
+                if (p.githubToken && r.data.deployKey) {
+                    const stateObj = JSON.parse(JSON.parse(state.toString()));
+                    const githubNodes = stateObj.nodes.filter((n: any) => n.type === "github" && n.data?.repository?.id);
+
+                    const github = new GithubClient(p.githubToken);
+                    for (const node of githubNodes) {
+                        try {
+                            await github.addDeployKey(node.data.repository.sshURL, r.data.deployKey);
+                        } catch (error) {
+                            console.error(`Failed to add deploy key to repository ${node.data.repository.sshURL}:`, error);
+                        }
+                    }
+                }
             }
         } else {
             r = await axios.request({
@@ -255,6 +273,94 @@
     }
 };
 
+const handleGithubRepos: express.Handler = async (req, resp) => {
+    try {
+        const projectId = Number(req.params["projectId"]);
+        const project = await db.project.findUnique({
+            where: { id: projectId },
+            select: { githubToken: true }
+        });
+
+        if (!project?.githubToken) {
+            resp.status(400);
+            resp.write(JSON.stringify({ error: "GitHub token not configured" }));
+            return;
+        }
+
+        const github = new GithubClient(project.githubToken);
+        const repositories = await github.getRepositories();
+
+        resp.status(200);
+        resp.header("Content-Type", "application/json");
+        resp.write(JSON.stringify(repositories));
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+        resp.write(JSON.stringify({ error: "Failed to fetch repositories" }));
+    } finally {
+        resp.end();
+    }
+};
+
+const handleUpdateGithubToken: express.Handler = async (req, resp) => {
+    try {
+        const projectId = Number(req.params["projectId"]);
+        const { githubToken } = req.body;
+
+        await db.project.update({
+            where: { id: projectId },
+            data: { githubToken },
+        });
+
+        resp.status(200);
+    } catch (e) {
+        console.log(e);
+        resp.status(500);
+    } finally {
+        resp.end();
+    }
+};
+
+const handleEnv: express.Handler = async (req, resp) => {
+    const projectId = Number(req.params["projectId"]);
+    try {
+        const project = await db.project.findUnique({
+            where: { id: projectId },
+            select: {
+                deployKey: true,
+                githubToken: true
+            }
+        });
+
+        if (!project) {
+            resp.status(404);
+            resp.write(JSON.stringify({ error: "Project not found" }));
+            return;
+        }
+
+        resp.status(200);
+        resp.write(JSON.stringify({
+            deployKey: project.deployKey,
+            integrations: {
+                github: !!project.githubToken,
+            },
+            networks: [{
+                name: "Public",
+                domain: "v1.dodo.cloud",
+            }, {
+                name: "Private",
+                domain: "p.v1.dodo.cloud",
+            }]
+        }));
+    } catch (error) {
+        console.error("Error checking integrations:", error);
+        resp.status(500);
+        resp.write(JSON.stringify({ error: "Internal server error" }));
+    } finally {
+        resp.end();
+    }
+};
+
 async function start() {
     await db.$connect();
     const app = express();
@@ -266,6 +372,9 @@
     app.delete("/api/project/:projectId", handleDelete);
     app.get("/api/project", handleProjectAll);
     app.post("/api/project", handleProjectCreate);
+    app.get("/api/project/:projectId/repos/github", handleGithubRepos);
+    app.post("/api/project/:projectId/github-token", handleUpdateGithubToken);
+    app.get("/api/project/:projectId/env", handleEnv);
     app.use("/", express.static("../front/dist"));
     app.listen(env.DODO_PORT_WEB, () => {
         console.log("started");
diff --git a/apps/canvas/back/package-lock.json b/apps/canvas/back/package-lock.json
index cf48f57..89b0272 100644
--- a/apps/canvas/back/package-lock.json
+++ b/apps/canvas/back/package-lock.json
@@ -12,7 +12,8 @@
         "@prisma/client": "^6.6.0",
         "axios": "^1.8.4",
         "express": "^4.21.1",
-        "sqlite3": "^5.1.7"
+        "sqlite3": "^5.1.7",
+        "zod": "^3.24.4"
       },
       "devDependencies": {
         "@types/express": "^5.0.0",
@@ -2941,6 +2942,14 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+    },
+    "node_modules/zod": {
+      "version": "3.24.4",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
+      "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
     }
   }
 }
diff --git a/apps/canvas/back/package.json b/apps/canvas/back/package.json
index b05fb1e..360c453 100644
--- a/apps/canvas/back/package.json
+++ b/apps/canvas/back/package.json
@@ -13,7 +13,8 @@
     "@prisma/client": "^6.6.0",
     "axios": "^1.8.4",
     "express": "^4.21.1",
-    "sqlite3": "^5.1.7"
+    "sqlite3": "^5.1.7",
+    "zod": "^3.24.4"
   },
   "devDependencies": {
     "@types/express": "^5.0.0",
diff --git a/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql b/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql
new file mode 100644
index 0000000..e9eac71
--- /dev/null
+++ b/apps/canvas/back/prisma/migrations/20250507131125_add_github_token/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Project" ADD COLUMN "githubToken" TEXT;
diff --git a/apps/canvas/back/prisma/schema.prisma b/apps/canvas/back/prisma/schema.prisma
index e19e84b..8dd7ceb 100644
--- a/apps/canvas/back/prisma/schema.prisma
+++ b/apps/canvas/back/prisma/schema.prisma
@@ -21,4 +21,5 @@
   draft Bytes?
   instanceId String?
   deployKey String?
+  githubToken String?
 }
\ No newline at end of file
diff --git a/apps/canvas/front/src/App.tsx b/apps/canvas/front/src/App.tsx
index 62652f6..2970cc0 100644
--- a/apps/canvas/front/src/App.tsx
+++ b/apps/canvas/front/src/App.tsx
@@ -3,8 +3,7 @@
 import { CanvasBuilder } from './Canvas';
 import { Tabs, TabsTrigger, TabsContent, TabsList } from './components/ui/tabs';
 import { Config } from './Config';
-import { useStateStore } from './lib/state';
-import { useEffect } from 'react';
+import { Integrations } from './Integrations';
 import { Toaster } from './components/ui/toaster';
 import { Header } from './Header';
 
@@ -19,22 +18,22 @@
 }
 
 function AppImpl() {
-  const store = useStateStore();
-  useEffect(() => {
-    setTimeout(async () => await store.refreshEnv(), 1);
-  }, [store])
   return (
-      <Tabs defaultValue="canvas">
-        <TabsList>
-          <TabsTrigger value="canvas">Canvas</TabsTrigger>
-          <TabsTrigger value="config">Config</TabsTrigger>
-        </TabsList>
-        <TabsContent value="canvas">
-          <CanvasBuilder />
-        </TabsContent>
-        <TabsContent value="config">
-          <Config />
-        </TabsContent>
-      </Tabs>
+    <Tabs defaultValue="canvas">
+      <TabsList>
+        <TabsTrigger value="canvas">Canvas</TabsTrigger>
+        <TabsTrigger value="config">Config</TabsTrigger>
+        <TabsTrigger value="integrations">Integrations</TabsTrigger>
+      </TabsList>
+      <TabsContent value="canvas">
+        <CanvasBuilder />
+      </TabsContent>
+      <TabsContent value="config">
+        <Config />
+      </TabsContent>
+      <TabsContent value="integrations">
+        <Integrations />
+      </TabsContent>
+    </Tabs>
   );
 }
diff --git a/apps/canvas/front/src/Integrations.tsx b/apps/canvas/front/src/Integrations.tsx
new file mode 100644
index 0000000..a566c22
--- /dev/null
+++ b/apps/canvas/front/src/Integrations.tsx
@@ -0,0 +1,132 @@
+import { useProjectId, useGithubService, useStateStore } from '@/lib/state';
+import { Form, FormControl, FormField, FormItem, FormMessage } from './components/ui/form';
+import { Input } from './components/ui/input';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Button } from './components/ui/button';
+import { useToast } from '@/hooks/use-toast';
+import { Checkbox } from './components/ui/checkbox';
+import { useState, useCallback } from 'react';
+
+const schema = z.object({
+    githubToken: z.string().min(1, "GitHub token is required"),
+});
+
+export function Integrations() {
+    const { toast } = useToast();
+    const store = useStateStore();
+    const projectId = useProjectId();
+    const [isEditing, setIsEditing] = useState(false);
+    const githubService = useGithubService();
+    const [isSaving, setIsSaving] = useState(false);
+
+    const form = useForm<z.infer<typeof schema>>({
+        resolver: zodResolver(schema),
+        mode: "onChange",
+    });
+
+    const onSubmit = useCallback(async (data: z.infer<typeof schema>) => {
+        if (!projectId) return;
+
+        setIsSaving(true);
+
+        try {
+            const response = await fetch(`/api/project/${projectId}/github-token`, {
+                method: "POST",
+                headers: {
+                    "Content-Type": "application/json",
+                },
+                body: JSON.stringify({ githubToken: data.githubToken }),
+            });
+
+            if (!response.ok) {
+                throw new Error("Failed to save GitHub token");
+            }
+
+            await store.refreshEnv();
+            setIsEditing(false);
+            form.reset();
+            toast({
+                title: "GitHub token saved successfully",
+            });
+        } catch (error) {
+            toast({
+                variant: "destructive",
+                title: "Failed to save GitHub token",
+                description: error instanceof Error ? error.message : "Unknown error",
+            });
+        } finally {
+            setIsSaving(false);
+        }
+    }, [projectId, store, form, toast, setIsEditing, setIsSaving]);
+
+    const handleCancel = () => {
+        setIsEditing(false);
+        form.reset();
+    };
+
+    return (
+        <div className="space-y-6">
+            <div>
+                <h3 className="text-md font-medium mb-2">GitHub</h3>
+                <div className="space-y-4">
+                    <div className="flex items-center space-x-2">
+                        <Checkbox checked={!!githubService} disabled />
+                        <label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
+                            GitHub Access Token
+                        </label>
+                    </div>
+
+                    {!!githubService && !isEditing && (
+                        <Button
+                            variant="outline"
+                            onClick={() => setIsEditing(true)}
+                        >
+                            Update Access Token
+                        </Button>
+                    )}
+
+                    {(!!!githubService || isEditing) && (
+                        <Form {...form}>
+                            <form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
+                                <FormField
+                                    control={form.control}
+                                    name="githubToken"
+                                    render={({ field }) => (
+                                        <FormItem>
+                                            <FormControl>
+                                                <Input
+                                                    type="password"
+                                                    placeholder="GitHub Personal Access Token"
+                                                    className="border border-black"
+                                                    {...field}
+                                                />
+                                            </FormControl>
+                                            <FormMessage />
+                                        </FormItem>
+                                    )}
+                                />
+                                <div className="flex space-x-2">
+                                    <Button type="submit" disabled={isSaving}>
+                                        {isSaving ? "Saving..." : "Save"}
+                                    </Button>
+                                    {!!githubService && (
+                                        <Button
+                                            type="button"
+                                            variant="outline"
+                                            onClick={handleCancel}
+                                            disabled={isSaving}
+                                        >
+                                            Cancel
+                                        </Button>
+                                    )}
+                                </div>
+                            </form>
+                        </Form>
+                    )}
+                </div>
+            </div>
+        </div>
+    );
+} 
\ No newline at end of file
diff --git a/apps/canvas/front/src/components/canvas.tsx b/apps/canvas/front/src/components/canvas.tsx
index 2f261f0..8898b58 100644
--- a/apps/canvas/front/src/components/canvas.tsx
+++ b/apps/canvas/front/src/components/canvas.tsx
@@ -47,9 +47,9 @@
             return c.targetHandle === "repository";
         }
         if (sn.type === "app") {
-              if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
+            if (c.sourceHandle === "ports" && (!sn.data.ports || sn.data.ports.length === 0)) {
                 return false;
-              }
+            }
         }
         if (tn.type === "gateway-https") {
             if (c.targetHandle === "https" && tn.data.https !== undefined) {
@@ -81,7 +81,7 @@
                 x: 0,
                 y: 0,
             },
-            isConnectable: true,          
+            isConnectable: true,
             data: {
                 domain: n.domain,
                 label: n.domain,
@@ -109,7 +109,7 @@
                 isValidConnection={isValidConnection}
                 fitView
                 proOptions={{ hideAttribution: true }}
-                >
+            >
                 <Controls />
                 <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
                 <Panel position="bottom-right">
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index dcccd82..5b4b3fa 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -394,8 +394,8 @@
                     </SelectTrigger>
                   </FormControl>
                   <SelectContent>
-                    {nodes.filter((n) => n.type === "github" && n.data.address).map((n) => (
-                      <SelectItem key={n.id} value={n.id}>{`${n.data.address!}`}</SelectItem>
+                    {nodes.filter((n) => n.type === "github" && n.data.repository?.id !== undefined).map((n) => (
+                      <SelectItem key={n.id} value={n.id}>{`${n.data.repository?.sshURL}`}</SelectItem>
                     ))}
                   </SelectContent>
                 </Select>
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index aa48cd1..3ae779e 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -1,12 +1,16 @@
 import { NodeRect } from './node-rect';
-import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore } from '@/lib/state';
-import { useEffect, useMemo } from 'react';
+import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from '@/lib/state';
+import { useEffect, useMemo, useState } from 'react';
 import { z } from "zod";
 import { DeepPartial, EventType, useForm } from 'react-hook-form';
 import { zodResolver } from '@hookform/resolvers/zod';
 import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
-import { Input } from './ui/input';
 import { Handle, Position } from "@xyflow/react";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
+import { GitHubRepository } from '../lib/github';
+import { useProjectId } from '@/lib/state';
+import { Alert, AlertDescription } from './ui/alert';
+import { AlertCircle } from 'lucide-react';
 
 export function NodeGithub(node: GithubNode) {
   const { id, selected } = node;
@@ -22,54 +26,136 @@
           isConnectableStart={isConnectable}
           isConnectableEnd={isConnectable}
           isConnectable={isConnectable}
-         />
+        />
       </div>
     </NodeRect>
   );
 }
 
 const schema = z.object({
-  address: z.string().min(1),
+  repositoryId: z.number().optional(),
 });
 
 export function NodeGithubDetails(node: GithubNode) {
   const { id, data } = node;
   const store = useStateStore();
+  const projectId = useProjectId();
+  const [repos, setRepos] = useState<GitHubRepository[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const githubService = useGithubService();
+
+  useEffect(() => {
+    if (data.repository) {
+      const { id, sshURL } = data.repository;
+      setRepos(prevRepos => {
+        if (!prevRepos.some(r => r.id === id)) {
+          return [...prevRepos, {
+            id,
+            name: sshURL.split('/').pop() || '',
+            full_name: sshURL.split('/').slice(-2).join('/'),
+            html_url: '',
+            ssh_url: sshURL,
+            description: null,
+            private: false,
+            default_branch: 'main'
+          }];
+        }
+        return prevRepos;
+      });
+    }
+  }, [data.repository]);
+
   const form = useForm<z.infer<typeof schema>>({
     resolver: zodResolver(schema),
     mode: "onChange",
     defaultValues: {
-      address: data.address,
+      repositoryId: data.repository?.id,
     }
   });
+
   useEffect(() => {
     const sub = form.watch((value: DeepPartial<z.infer<typeof schema>>, { name, type }: { name?: keyof z.infer<typeof schema> | undefined, type?: EventType | undefined }) => {
       if (type !== "change") {
         return;
       }
       switch (name) {
-        case "address":
-          store.updateNodeData<"github">(id, {
-            address: value.address,
-          });
+        case "repositoryId":
+          if (value.repositoryId) {
+            const repo = repos.find(r => r.id === value.repositoryId);
+            if (repo) {
+              store.updateNodeData<"github">(id, {
+                repository: {
+                  id: repo.id,
+                  sshURL: repo.ssh_url,
+                },
+              });
+            }
+          }
           break;
       }
     });
     return () => sub.unsubscribe();
-  }, [form, store]);
+  }, [form, store, id, repos]);
+
+  useEffect(() => {
+    const fetchRepositories = async () => {
+      if (!!!githubService) return;
+
+      setLoading(true);
+      setError(null);
+      try {
+        const repositories = await githubService.getRepositories();
+        setRepos(repositories);
+      } catch (err) {
+        setError(err instanceof Error ? err.message : "Failed to fetch repositories");
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    fetchRepositories();
+  }, [githubService]);
+
   return (
     <>
       <Form {...form}>
         <form className="space-y-2">
-          <FormField 
+          <FormField
             control={form.control}
-            name="address"
+            name="repositoryId"
             render={({ field }) => (
               <FormItem>
-                <FormControl>
-                  <Input placeholder="address" className="border border-black" {...field} />
-                </FormControl>
+                <Select
+                  onValueChange={(value) => field.onChange(Number(value))}
+                  value={field.value?.toString()}
+                  disabled={loading || !projectId || !!!githubService}
+                >
+                  <FormControl>
+                    <SelectTrigger>
+                      <SelectValue placeholder={!!githubService ? "Select a repository" : "GitHub not configured"} />
+                    </SelectTrigger>
+                  </FormControl>
+                  <SelectContent>
+                    {repos.map((repo) => (
+                      <SelectItem key={repo.id} value={repo.id.toString()}>
+                        {repo.full_name}
+                        {repo.description && ` - ${repo.description}`}
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
                 <FormMessage />
+                {error && <p className="text-sm text-red-500">{error}</p>}
+                {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
+                {!!!githubService && (
+                  <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.
+                    </AlertDescription>
+                  </Alert>
+                )}
               </FormItem>
             )}
           />
diff --git a/apps/canvas/front/src/components/ui/alert.tsx b/apps/canvas/front/src/components/ui/alert.tsx
new file mode 100644
index 0000000..350d1cd
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/alert.tsx
@@ -0,0 +1,58 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+    "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+    {
+        variants: {
+            variant: {
+                default: "bg-background text-foreground",
+                destructive:
+                    "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+            },
+        },
+        defaultVariants: {
+            variant: "default",
+        },
+    }
+)
+
+const Alert = React.forwardRef<
+    HTMLDivElement,
+    React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+    <div
+        ref={ref}
+        role="alert"
+        className={cn(alertVariants({ variant }), className)}
+        {...props}
+    />
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+    HTMLParagraphElement,
+    React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+    <h5
+        ref={ref}
+        className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+        {...props}
+    />
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+    HTMLParagraphElement,
+    React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+    <div
+        ref={ref}
+        className={cn("text-sm [&_p]:leading-relaxed", className)}
+        {...props}
+    />
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription } 
\ No newline at end of file
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index d6646c9..3ea29b6 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -102,7 +102,7 @@
                     type: n.data.type,
                     name: n.data.label,
                     source: {
-                        repository: nodes.filter((i) => i.type === "github").find((i) => i.id === n.data.repository.id)!.data.address,
+                        repository: nodes.filter((i) => i.type === "github").find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
                         branch: n.data.repository.branch,
                         rootDir: n.data.repository.rootDir,
                     },
@@ -245,7 +245,7 @@
 
 function GitRepositoryValidator(nodes: AppNode[]): Message[] {
     const git = nodes.filter((n) => n.type === "github");
-    const noAddress: Message[] = git.filter((n) => n.data == null || n.data.address == null || n.data.address === "").map((n) => ({
+    const noAddress: Message[] = git.filter((n) => n.data == null || n.data.repository == null).map((n) => ({
         id: `${n.id}-no-address`,
         type: "FATAL",
         nodeId: n.id,
diff --git a/apps/canvas/front/src/lib/github.ts b/apps/canvas/front/src/lib/github.ts
new file mode 100644
index 0000000..8537671
--- /dev/null
+++ b/apps/canvas/front/src/lib/github.ts
@@ -0,0 +1,36 @@
+export interface GitHubRepository {
+    id: number;
+    name: string;
+    full_name: string;
+    html_url: string;
+    ssh_url: string;
+    description: string | null;
+    private: boolean;
+    default_branch: string;
+}
+
+export interface GitHubService {
+    /**
+     * Fetches a list of repositories for the authenticated user
+     * @returns Promise resolving to an array of GitHub repositories
+     */
+    getRepositories(): Promise<GitHubRepository[]>;
+}
+
+export class GitHubServiceImpl implements GitHubService {
+    private projectId: string;
+
+    constructor(projectId: string) {
+        this.projectId = projectId;
+    }
+
+    async getRepositories(): Promise<GitHubRepository[]> {
+        const response = await fetch(`/api/project/${this.projectId}/repos/github`);
+
+        if (!response.ok) {
+            throw new Error(`Failed to fetch repositories: ${response.statusText}`);
+        }
+
+        return response.json();
+    }
+} 
\ No newline at end of file
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 02fd983..a3cd755 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,17 +1,18 @@
 import { v4 as uuidv4 } from "uuid";
 import { create } from 'zustand';
 import { addEdge, applyNodeChanges, applyEdgeChanges, Connection, EdgeChange, useNodes } from '@xyflow/react';
-import {
-  type Edge,
-  type Node,
-  type OnNodesChange,
-  type OnEdgesChange,
-  type OnConnect,
+import type {
+  Edge,
+  Node,
+  OnNodesChange,
+  OnEdgesChange,
+  OnConnect,
 } from '@xyflow/react';
-import { DeepPartial } from "react-hook-form";
+import type { DeepPartial } from "react-hook-form";
 import { Category, defaultCategories } from "./categories";
 import { CreateValidators, Validator } from "./config";
 import { z } from "zod";
+import { GitHubService, GitHubServiceImpl } from './github';
 
 export type InitData = {
   label: string;
@@ -135,7 +136,10 @@
 };
 
 export type GithubData = NodeData & {
-  address: string;
+  repository?: {
+    id: number;
+    sshURL: string;
+  };
 };
 
 export type GithubNode = Node<GithubData> & {
@@ -152,7 +156,7 @@
   switch (n.type) {
     case "network": return n.data.domain;
     case "app": return n.data.label || "Service";
-    case "github": return n.data.address || "Github";
+    case "github": return n.data.repository?.sshURL || "Github";
     case "gateway-https": {
       if (n.data && n.data.network && n.data.subdomain) {
         return `https://${n.data.subdomain}.${n.data.network}`;
@@ -189,7 +193,7 @@
       }
       return false;
     case "github":
-      if (n.data !== undefined && n.data.address) {
+      if (n.data.repository?.id !== undefined) {
         return true;
       }
       return false;
@@ -264,6 +268,7 @@
     case "postgresql": return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
     case "volume": return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
     case undefined: throw new Error("MUST NOT REACH");
+    default: throw new Error("MUST NOT REACH");
   }
 }
 
@@ -282,20 +287,38 @@
 };
 
 export const envSchema = z.object({
-  deployKey: z.string(),
+  deployKey: z.optional(z.string().min(1)),
   networks: z.array(z.object({
-    name: z.string(),
-    domain: z.string(),
-  })),
+    name: z.string().min(1),
+    domain: z.string().min(1),
+  })).default([]),
+  integrations: z.object({
+    github: z.boolean(),
+  }),
 });
 
 export type Env = z.infer<typeof envSchema>;
 
+const defaultEnv: Env = {
+  deployKey: undefined,
+  networks: [],
+  integrations: {
+    github: false,
+  }
+};
+
 export type Project = {
   id: string;
   name: string;
 }
 
+export type IntegrationsConfig = {
+  github: boolean;
+};
+
+type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
+type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
+
 export type AppState = {
   projectId: string | undefined;
   projects: Project[];
@@ -303,7 +326,8 @@
   edges: Edge[];
   categories: Category[];
   messages: Message[];
-  env?: Env;
+  env: Env;
+  githubService: GitHubService | null;
   setHighlightCategory: (name: string, active: boolean) => void;
   onNodesChange: OnNodesChange<AppNode>;
   onEdgesChange: OnEdgesChange;
@@ -312,15 +336,16 @@
   setEdges: (edges: Edge[]) => void;
   setProject: (projectId: string | undefined) => void;
   setProjects: (projects: Project[]) => void;
-  updateNode: <T extends NodeType>(id: string, data: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>) => void;
-  updateNodeData: <T extends NodeType>(id: string, data: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>) => 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;
-  refreshEnv: () => Promise<Env | undefined>;
+  refreshEnv: () => Promise<void>;
 };
 
 const projectIdSelector = (state: AppState) => state.projectId;
 const categoriesSelector = (state: AppState) => state.categories;
 const messagesSelector = (state: AppState) => state.messages;
+const githubServiceSelector = (state: AppState) => state.githubService;
 const envSelector = (state: AppState) => state.env;
 
 export function useProjectId(): string | undefined {
@@ -347,88 +372,55 @@
   return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
 }
 
-let envRefresh: Promise<Env | undefined> | null = null;
-
-const fixedEnv: Env = {
-  "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
-  "networks": [{
-    "name": "Public",
-    "domain": "v1.dodo.cloud",
-  }, {
-    "name": "Private",
-    "domain": "p.v1.dodo.cloud",
-  }],
-};
-
 export function useEnv(): Env {
-  return fixedEnv;
-  const store = useStateStore();
-  const env = envSelector(store);
-  console.log(env);
-  if (env != null) {
-    return env;
-  }
-  if (envRefresh == null) {
-    envRefresh = store.refreshEnv();
-    envRefresh.finally(() => envRefresh = null);
-  }
-  return {
-    deployKey: "",
-    networks: [],
-  };
+  return useStateStore(envSelector);
+}
+
+export function useGithubService(): GitHubService | null {
+  return useStateStore(githubServiceSelector);
 }
 
 const v: Validator = CreateValidators();
 
 export const useStateStore = create<AppState>((set, get): AppState => {
-  set({
-    env: {
-      "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
-      "networks": [{
-        "name": "Public",
-        "domain": "v1.dodo.cloud",
-      }, {
-        "name": "Private",
-        "domain": "p.v1.dodo.cloud",
-      }],
-    }
-  });
-  console.log(get().env);
   const setN = (nodes: AppNode[]) => {
-    set({
-      nodes: nodes,
-      messages: v(nodes),
-    })
+    set((state) => ({
+      ...state,
+      nodes,
+    }));
   };
-  function updateNodeData<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>): void {
-    setN(get().nodes.map((n) => {
-      if (n.id !== id) {
-        return n;
-      }
-      const nd = {
-        ...n,
-        data: {
-          ...n.data,
-          ...d,
-        },
-      };
-      return nd;
-    })
-    );
-  };
-  function updateNode<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>): void {
+
+  function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
     setN(
       get().nodes.map((n) => {
-        if (n.id !== id) {
-          return n;
+        if (n.id === id) {
+          return {
+            ...n,
+            data: {
+              ...n.data,
+              ...data,
+            },
+          } as Extract<AppNode, { type: T }>;
         }
-        return {
-          ...n,
-          ...d,
-        };
+        return n;
       })
     );
-  };
+  }
+
+  function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
+    setN(
+      get().nodes.map((n) => {
+        if (n.id === id) {
+          return {
+            ...n,
+            ...node,
+          } as Extract<AppNode, { type: T }>;
+        }
+        return n;
+      })
+    );
+  }
+
   function onConnect(c: Connection) {
     const { nodes, edges } = get();
     set({
@@ -447,64 +439,66 @@
         });
       }
     }
-    if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
-      const sourceEnvVars = nodeEnvVarNames(sn);
-      if (sourceEnvVars.length === 0) {
-        throw new Error("MUST NOT REACH!");
+    if (tn.type === "app") {
+      if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
+        const sourceEnvVars = nodeEnvVarNames(sn);
+        if (sourceEnvVars.length === 0) {
+          throw new Error("MUST NOT REACH!");
+        }
+        const id = uuidv4();
+        if (sourceEnvVars.length === 1) {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                  name: sourceEnvVars[0],
+                  isEditting: false,
+                },
+              ],
+            },
+          });
+        } else {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                },
+              ],
+            },
+          });
+        }
       }
-      const id = uuidv4();
-      if (sourceEnvVars.length === 1) {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-                name: sourceEnvVars[0],
-                isEditting: false,
-              },
-            ],
-          },
-        });
-      } else {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-              },
-            ],
-          },
-        });
-      }
-    }
-    if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
-      const sourcePorts = sn.data.ports || [];
-      const id = uuidv4();
-      if (sourcePorts.length === 1) {
-        updateNode(c.target, {
-          ...tn,
-          data: {
-            ...tn.data,
-            envVars: [
-              ...(tn.data.envVars || []),
-              {
-                id: id,
-                source: c.source,
-                name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
-                portId: sourcePorts[0].id,
-                isEditting: false,
-              },
-            ],
-          },
-        });
+      if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
+        const sourcePorts = sn.data.ports || [];
+        const id = uuidv4();
+        if (sourcePorts.length === 1) {
+          updateNode<"app">(c.target, {
+            ...tn,
+            data: {
+              ...tn.data,
+              envVars: [
+                ...(tn.data.envVars || []),
+                {
+                  id: id,
+                  source: c.source,
+                  name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
+                  portId: sourcePorts[0].id,
+                  isEditting: false,
+                },
+              ],
+            },
+          });
+        }
       }
     }
     if (c.sourceHandle === "volume") {
@@ -580,6 +574,8 @@
     edges: [],
     categories: defaultCategories,
     messages: v([]),
+    env: defaultEnv,
+    githubService: null,
     setHighlightCategory: (name, active) => {
       set({
         categories: get().categories.map(
@@ -639,15 +635,41 @@
     updateNodeData,
     onConnect,
     refreshEnv: async () => {
-      return get().env;
-      const resp = await fetch("/env");
-      if (!resp.ok) {
-        throw new Error("failed to fetch env config");
+      const projectId = get().projectId;
+      let env: Env = defaultEnv;
+
+      try {
+        if (projectId) {
+          const response = await fetch(`/api/project/${projectId}/env`);
+          if (response.ok) {
+            const data = await response.json();
+            const result = envSchema.safeParse(data);
+            if (result.success) {
+              env = result.data;
+            } else {
+              console.error("Invalid env data:", result.error);
+            }
+          }
+        }
+      } catch (error) {
+        console.error("Failed to fetch integrations:", error);
+      } finally {
+        set({ env: env });
+        if (env.integrations.github) {
+          set({ githubService: new GitHubServiceImpl(projectId!) });
+        } else {
+          set({ githubService: null });
+        }
       }
-      set({ env: envSchema.parse(await resp.json()) });
-      return get().env;
     },
-    setProject: (projectId) => set({ projectId }),
+    setProject: (projectId) => {
+      set({
+        projectId,
+      });
+      if (projectId) {
+        get().refreshEnv();
+      }
+    },
     setProjects: (projects) => set({ projects }),
   };
 });