/** * Coolify API client for Vibn project provisioning. * * Used server-side only. Credentials from env vars: * COOLIFY_URL — e.g. http://34.19.250.135:8000 * COOLIFY_API_TOKEN — admin bearer token */ const COOLIFY_URL = process.env.COOLIFY_URL ?? 'http://34.19.250.135:8000'; const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? ''; export interface CoolifyProject { uuid: string; name: string; description?: string; } export interface CoolifyDatabase { uuid: string; name: string; type: string; status: string; internal_db_url?: string; external_db_url?: string; /** When true, Coolify publishes a host port for remote connections */ is_public?: boolean; /** Host port mapped to 5432 inside the container */ public_port?: number; } export interface CoolifyApplication { uuid: string; name: string; status: string; fqdn?: string; git_repository?: string; git_branch?: string; project_uuid?: string; environment_name?: string; /** Coolify sometimes nests these under an `environment` object */ environment?: { project_uuid?: string; project?: { uuid?: string } }; } export interface CoolifyEnvVar { uuid?: string; key: string; value: string; is_preview?: boolean; is_build_time?: boolean; is_literal?: boolean; is_multiline?: boolean; is_shown_once?: boolean; } async function coolifyFetch(path: string, options: RequestInit = {}) { const url = `${COOLIFY_URL}/api/v1${path}`; const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${COOLIFY_API_TOKEN}`, ...(options.headers ?? {}), }, }); if (!res.ok) { const text = await res.text(); throw new Error(`Coolify API error ${res.status} on ${path}: ${text}`); } if (res.status === 204) return null; return res.json(); } // ────────────────────────────────────────────────── // Projects // ────────────────────────────────────────────────── export async function listProjects(): Promise { return coolifyFetch('/projects'); } export async function createProject(name: string, description?: string): Promise { return coolifyFetch('/projects', { method: 'POST', body: JSON.stringify({ name, description }), }); } export async function getProject(uuid: string): Promise { return coolifyFetch(`/projects/${uuid}`); } export async function deleteProject(uuid: string): Promise { await coolifyFetch(`/projects/${uuid}`, { method: 'DELETE' }); } // ────────────────────────────────────────────────── // Databases // ────────────────────────────────────────────────── type DBType = 'postgresql' | 'mysql' | 'mariadb' | 'redis' | 'mongodb' | 'keydb'; export async function createDatabase(opts: { projectUuid: string; name: string; type: DBType; serverUuid?: string; environmentName?: string; }): Promise { const { projectUuid, name, type, serverUuid = '0', environmentName = 'production' } = opts; return coolifyFetch(`/databases`, { method: 'POST', body: JSON.stringify({ project_uuid: projectUuid, name, type, server_uuid: serverUuid, environment_name: environmentName, }), }); } export async function getDatabase(uuid: string): Promise { return coolifyFetch(`/databases/${uuid}`); } export async function deleteDatabase(uuid: string): Promise { await coolifyFetch(`/databases/${uuid}`, { method: 'DELETE' }); } // ────────────────────────────────────────────────── // Applications // ────────────────────────────────────────────────── export async function createApplication(opts: { projectUuid: string; name: string; gitRepo: string; // e.g. "https://git.vibnai.com/mark/taskmaster.git" gitBranch?: string; serverUuid?: string; environmentName?: string; buildPack?: string; // nixpacks, static, dockerfile ports?: string; // e.g. "3000" }): Promise { const { projectUuid, name, gitRepo, gitBranch = 'main', serverUuid = process.env.COOLIFY_SERVER_UUID ?? 'jws4g4cgssss4cw48s488woc', environmentName = 'production', buildPack = 'nixpacks', ports = '3000', } = opts; return coolifyFetch(`/applications`, { method: 'POST', body: JSON.stringify({ project_uuid: projectUuid, name, git_repository: gitRepo, git_branch: gitBranch, server_uuid: serverUuid, environment_name: environmentName, build_pack: buildPack, ports_exposes: ports, }), }); } /** * Create a Coolify service for one app inside a Turborepo monorepo. * Build command uses `turbo run build --filter` to target just that app. */ export async function createMonorepoAppService(opts: { projectUuid: string; appName: string; gitRepo: string; gitBranch?: string; domain: string; serverUuid?: string; environmentName?: string; }): Promise { const { projectUuid, appName, gitRepo, gitBranch = 'main', domain, serverUuid = process.env.COOLIFY_SERVER_UUID ?? 'jws4g4cgssss4cw48s488woc', environmentName = 'production', } = opts; return coolifyFetch(`/applications`, { method: 'POST', body: JSON.stringify({ project_uuid: projectUuid, name: appName, git_repository: gitRepo, git_branch: gitBranch, server_uuid: serverUuid, environment_name: environmentName, build_pack: 'nixpacks', build_command: `pnpm install && turbo run build --filter=${appName}`, start_command: `turbo run start --filter=${appName}`, ports_exposes: '3000', fqdn: `https://${domain}`, }), }); } export async function listApplications(): Promise { return coolifyFetch('/applications'); } export async function deployApplication(uuid: string): Promise<{ deployment_uuid: string }> { return coolifyFetch(`/applications/${uuid}/deploy`, { method: 'POST' }); } export async function getApplication(uuid: string): Promise { return coolifyFetch(`/applications/${uuid}`); } export async function getDeploymentLogs(deploymentUuid: string): Promise<{ logs: string }> { return coolifyFetch(`/deployments/${deploymentUuid}/logs`); } export async function listApplicationDeployments(uuid: string): Promise> { return coolifyFetch(`/applications/${uuid}/deployments`); } // ────────────────────────────────────────────────── // Environment variables // ────────────────────────────────────────────────── export async function listApplicationEnvs(uuid: string): Promise { return coolifyFetch(`/applications/${uuid}/envs`); } export async function upsertApplicationEnv( uuid: string, env: CoolifyEnvVar & { is_preview?: boolean } ): Promise { // Coolify accepts PATCH for updates and POST for creates. We try // PATCH first (idempotent upsert on key), fall back to POST. try { return await coolifyFetch(`/applications/${uuid}/envs`, { method: 'PATCH', body: JSON.stringify(env), }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (msg.includes('404') || msg.includes('405')) { return coolifyFetch(`/applications/${uuid}/envs`, { method: 'POST', body: JSON.stringify(env), }); } throw err; } } export async function deleteApplicationEnv(uuid: string, key: string): Promise { await coolifyFetch(`/applications/${uuid}/envs/${encodeURIComponent(key)}`, { method: 'DELETE', }); } // ────────────────────────────────────────────────── // Tenant helpers // ────────────────────────────────────────────────── /** * Return the Coolify project UUID an application belongs to, working * around Coolify v4 sometimes nesting it under `environment`. */ export function projectUuidOf(app: CoolifyApplication): string | null { return ( app.project_uuid ?? app.environment?.project_uuid ?? app.environment?.project?.uuid ?? null ); } /** * Fetch an application AND verify it lives in the expected Coolify * project. Throws a `TenantError` when the app is cross-tenant so * callers can translate to HTTP 403. */ export class TenantError extends Error { status = 403 as const; } export async function getApplicationInProject( appUuid: string, expectedProjectUuid: string ): Promise { const app = await getApplication(appUuid); const actualProject = projectUuidOf(app); if (!actualProject || actualProject !== expectedProjectUuid) { throw new TenantError( `Application ${appUuid} does not belong to project ${expectedProjectUuid}` ); } return app; } /** List applications that belong to the given Coolify project. */ export async function listApplicationsInProject( projectUuid: string ): Promise { const all = await listApplications(); return all.filter(a => projectUuidOf(a) === projectUuid); }