/** * Coolify Workspace Provisioning * * Provisions a dedicated Theia IDE instance per Vibn project using the * Coolify Docker image application API. Each workspace is: * - Hosted at {slug}.ide.vibnai.com * - Protected by the vibn-auth ForwardAuth (project-owner only) * - Running ghcr.io/eclipse-theia/theia-blueprint/theia-ide:latest */ const COOLIFY_URL = process.env.COOLIFY_URL ?? 'http://34.19.250.135:8000'; const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? ''; // Coolify resource IDs (stable — tied to the Vibn server/project setup) const COOLIFY_PROJECT_UUID = 'f4owwggokksgw0ogo0844os0'; // "Vibn" project const COOLIFY_ENVIRONMENT = 'production'; const COOLIFY_SERVER_UUID = 'jws4g4cgssss4cw48s488woc'; // localhost (Coolify host) const THEIA_IMAGE_NAME = 'ghcr.io/eclipse-theia/theia-blueprint/theia-ide'; const THEIA_IMAGE_TAG = 'latest'; const THEIA_PORT = '3000'; const IDE_DOMAIN_SUFFIX = '.ide.vibnai.com'; function coolifyHeaders() { return { Authorization: `Bearer ${COOLIFY_API_TOKEN}`, 'Content-Type': 'application/json', }; } /** * Builds the newline-separated Traefik label string that Coolify stores * as custom_labels. We add vibn-auth@file to the HTTPS router middleware * chain after Coolify's generated labels. * * Router naming convention observed in Coolify: * https-0-{uuid} → the TLS router for the app */ function buildCustomLabels(appUuid: string): string { const routerName = `https-0-${appUuid}`; return [ 'traefik.enable=true', `traefik.http.routers.${routerName}.middlewares=vibn-auth@file,gzip`, ].join('\n'); } export interface ProvisionResult { appUuid: string; workspaceUrl: string; } /** * Creates a new Coolify Docker-image application for a Vibn project Theia workspace. * Sets the vibn-auth ForwardAuth middleware so only the project owner can access it. */ export async function provisionTheiaWorkspace( slug: string, projectId: string, giteaRepo: string | null, ): Promise { const workspaceUrl = `https://${slug}${IDE_DOMAIN_SUFFIX}`; const appName = `theia-${slug}`; // ── Step 1: Create the app ──────────────────────────────────────────────── const createRes = await fetch(`${COOLIFY_URL}/api/v1/applications/dockerimage`, { method: 'POST', headers: coolifyHeaders(), body: JSON.stringify({ project_uuid: COOLIFY_PROJECT_UUID, environment_name: COOLIFY_ENVIRONMENT, server_uuid: COOLIFY_SERVER_UUID, docker_registry_image_name: THEIA_IMAGE_NAME, docker_registry_image_tag: THEIA_IMAGE_TAG, name: appName, description: `Theia IDE for Vibn project ${slug}`, ports_exposes: THEIA_PORT, domains: workspaceUrl, instant_deploy: false, // we deploy after patching labels }), }); if (!createRes.ok) { const body = await createRes.text(); throw new Error(`Coolify create app failed (${createRes.status}): ${body}`); } const { uuid: appUuid } = await createRes.json() as { uuid: string }; // ── Step 2: Patch with vibn-auth Traefik labels ─────────────────────────── const patchRes = await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}`, { method: 'PATCH', headers: coolifyHeaders(), body: JSON.stringify({ custom_labels: buildCustomLabels(appUuid), }), }); if (!patchRes.ok) { console.warn(`[workspace] PATCH labels failed (${patchRes.status}) — continuing`); } // ── Step 3: Set environment variables ──────────────────────────────────── const envVars = [ { key: 'VIBN_PROJECT_ID', value: projectId, is_preview: false }, { key: 'VIBN_PROJECT_SLUG', value: slug, is_preview: false }, { key: 'GITEA_REPO', value: giteaRepo ?? '', is_preview: false }, { key: 'GITEA_API_URL', value: process.env.GITEA_API_URL ?? 'https://git.vibnai.com', is_preview: false }, ]; await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/envs/bulk`, { method: 'POST', headers: coolifyHeaders(), body: JSON.stringify({ data: envVars }), }); // ── Step 4: Deploy ──────────────────────────────────────────────────────── await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}/start`, { method: 'POST', headers: coolifyHeaders(), }); console.log(`[workspace] Provisioned ${appName} → ${workspaceUrl} (uuid: ${appUuid})`); return { appUuid, workspaceUrl }; } /** * Deletes a provisioned Theia workspace from Coolify. */ export async function deleteTheiaWorkspace(appUuid: string): Promise { await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}`, { method: 'DELETE', headers: coolifyHeaders(), }); }