Files
vibn-frontend/lib/coolify-workspace.ts
Mark Henderson a22d5a0f18 feat: provision dedicated per-project Theia workspaces
- lib/coolify-workspace.ts: creates a Coolify docker-image app at
  {slug}.ide.vibnai.com for each project, patches in vibn-auth Traefik
  labels, sets env vars, and starts deployment
- create/route.ts: provisions Theia workspace after Gitea repo creation;
  stores theiaWorkspaceUrl + theiaAppUuid on the project record
- theia-auth/route.ts: for *.ide.vibnai.com hosts, verifies the
  authenticated user is the project owner (slug → fs_projects lookup)
- overview/page.tsx: Open IDE always links (dedicated URL or shared fallback)
- project-creation-modal.tsx: shows dedicated workspace URL in success screen

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 13:14:21 -08:00

136 lines
5.1 KiB
TypeScript

/**
* 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<ProvisionResult> {
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<void> {
await fetch(`${COOLIFY_URL}/api/v1/applications/${appUuid}`, {
method: 'DELETE',
headers: coolifyHeaders(),
});
}