/** * Coolify API client for Vibn project provisioning. * * Used server-side only. Credentials from env vars: * COOLIFY_URL — e.g. https://coolify.vibnai.com * COOLIFY_API_TOKEN — admin bearer token * COOLIFY_DEFAULT_SERVER_UUID — which Coolify server workspaces deploy to * COOLIFY_DEFAULT_DESTINATION_UUID — Docker destination on that server */ const COOLIFY_URL = process.env.COOLIFY_URL ?? 'https://coolify.vibnai.com'; const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? ''; const COOLIFY_DEFAULT_SERVER_UUID = process.env.COOLIFY_DEFAULT_SERVER_UUID ?? 'jws4g4cgssss4cw48s488woc'; const COOLIFY_DEFAULT_DESTINATION_UUID = process.env.COOLIFY_DEFAULT_DESTINATION_UUID ?? 'zkogkggkw0wg40gccks80oo0'; export interface CoolifyProject { uuid: string; name: string; description?: string; environments?: Array<{ id: number; uuid: string; name: string; project_id: number }>; } /** All database flavors Coolify v4 can provision. */ export type CoolifyDatabaseType = | 'postgresql' | 'mysql' | 'mariadb' | 'mongodb' | 'redis' | 'keydb' | 'dragonfly' | 'clickhouse'; export interface CoolifyDatabase { uuid: string; name: string; type?: string; status: string; internal_db_url?: string; external_db_url?: string; is_public?: boolean; public_port?: number; project_uuid?: string; environment_id?: number; environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } }; } export interface CoolifyApplication { uuid: string; name: string; status: string; fqdn?: string; domains?: string; git_repository?: string; git_branch?: string; project_uuid?: string; environment_id?: number; environment_name?: string; environment?: { id?: number; 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; } export interface CoolifyPrivateKey { uuid: string; name: string; description?: string; fingerprint?: string; } export interface CoolifyServer { uuid: string; name: string; ip: string; is_reachable?: boolean; settings?: { wildcard_domain?: string | null }; } export interface CoolifyDeployment { uuid: string; status: string; created_at?: string; finished_at?: string; commit?: string; } 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' }); } // ────────────────────────────────────────────────── // Servers // ────────────────────────────────────────────────── export async function listServers(): Promise { return coolifyFetch('/servers'); } // ────────────────────────────────────────────────── // Private keys (SSH deploy keys) // ────────────────────────────────────────────────── export async function listPrivateKeys(): Promise { return coolifyFetch('/security/keys'); } export async function createPrivateKey(opts: { name: string; privateKeyPem: string; description?: string; }): Promise { return coolifyFetch('/security/keys', { method: 'POST', body: JSON.stringify({ name: opts.name, private_key: opts.privateKeyPem, description: opts.description ?? '', }), }); } export async function deletePrivateKey(uuid: string): Promise { await coolifyFetch(`/security/keys/${uuid}`, { method: 'DELETE' }); } // ────────────────────────────────────────────────── // Databases (all 8 types) // ────────────────────────────────────────────────── /** Shared context used by every database-create call. */ export interface CoolifyDbCreateContext { projectUuid: string; serverUuid?: string; environmentName?: string; destinationUuid?: string; instantDeploy?: boolean; } export interface CreateDatabaseOpts extends CoolifyDbCreateContext { type: CoolifyDatabaseType; name: string; description?: string; image?: string; isPublic?: boolean; publicPort?: number; /** Type-specific credentials / config */ credentials?: Record; /** Resource limits */ limits?: { memory?: string; cpus?: string }; } /** * Create a database of the requested type. Dispatches to the right * POST /databases/{type} endpoint and packs the right credential * fields for each flavor. */ export async function createDatabase(opts: CreateDatabaseOpts): Promise<{ uuid: string }> { const { type, name, projectUuid, serverUuid = COOLIFY_DEFAULT_SERVER_UUID, environmentName = 'production', destinationUuid = COOLIFY_DEFAULT_DESTINATION_UUID, instantDeploy = true, description, image, isPublic, publicPort, credentials = {}, limits = {}, } = opts; const common: Record = { project_uuid: projectUuid, server_uuid: serverUuid, environment_name: environmentName, destination_uuid: destinationUuid, name, description, image, is_public: isPublic, public_port: publicPort, instant_deploy: instantDeploy, limits_memory: limits.memory, limits_cpus: limits.cpus, }; return coolifyFetch(`/databases/${type}`, { method: 'POST', body: JSON.stringify(stripUndefined({ ...common, ...credentials })), }); } export async function listDatabases(): Promise { return coolifyFetch('/databases'); } export async function getDatabase(uuid: string): Promise { return coolifyFetch(`/databases/${uuid}`); } export async function updateDatabase(uuid: string, patch: Record): Promise<{ uuid: string }> { return coolifyFetch(`/databases/${uuid}`, { method: 'PATCH', body: JSON.stringify(stripUndefined(patch)), }); } export async function deleteDatabase( uuid: string, opts: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; } = {} ): Promise { const q = new URLSearchParams(); if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations)); if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes)); if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup)); if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks)); const qs = q.toString(); await coolifyFetch(`/databases/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' }); } export async function startDatabase(uuid: string): Promise { await coolifyFetch(`/databases/${uuid}/start`, { method: 'POST' }); } export async function stopDatabase(uuid: string): Promise { await coolifyFetch(`/databases/${uuid}/stop`, { method: 'POST' }); } export async function restartDatabase(uuid: string): Promise { await coolifyFetch(`/databases/${uuid}/restart`, { method: 'POST' }); } // ────────────────────────────────────────────────── // Applications // ────────────────────────────────────────────────── export type CoolifyBuildPack = 'nixpacks' | 'static' | 'dockerfile' | 'dockercompose'; export interface CreatePrivateDeployKeyAppOpts { projectUuid: string; privateKeyUuid: string; gitRepository: string; // SSH URL: git@git.vibnai.com:vibn-mark/repo.git gitBranch?: string; portsExposes: string; // "3000" serverUuid?: string; environmentName?: string; destinationUuid?: string; buildPack?: CoolifyBuildPack; name?: string; description?: string; domains?: string; // comma-separated FQDNs isAutoDeployEnabled?: boolean; isForceHttpsEnabled?: boolean; instantDeploy?: boolean; installCommand?: string; buildCommand?: string; startCommand?: string; baseDirectory?: string; dockerfileLocation?: string; manualWebhookSecretGitea?: string; } export async function createPrivateDeployKeyApp( opts: CreatePrivateDeployKeyAppOpts ): Promise<{ uuid: string }> { const body = stripUndefined({ project_uuid: opts.projectUuid, server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID, environment_name: opts.environmentName ?? 'production', destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID, private_key_uuid: opts.privateKeyUuid, git_repository: opts.gitRepository, git_branch: opts.gitBranch ?? 'main', build_pack: opts.buildPack ?? 'nixpacks', ports_exposes: opts.portsExposes, name: opts.name, description: opts.description, domains: opts.domains, is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true, is_force_https_enabled: opts.isForceHttpsEnabled ?? true, instant_deploy: opts.instantDeploy ?? true, install_command: opts.installCommand, build_command: opts.buildCommand, start_command: opts.startCommand, base_directory: opts.baseDirectory, dockerfile_location: opts.dockerfileLocation, manual_webhook_secret_gitea: opts.manualWebhookSecretGitea, }); return coolifyFetch('/applications/private-deploy-key', { method: 'POST', body: JSON.stringify(body), }); } export interface CreatePublicAppOpts { projectUuid: string; gitRepository: string; // https URL gitBranch?: string; portsExposes: string; serverUuid?: string; environmentName?: string; destinationUuid?: string; buildPack?: CoolifyBuildPack; name?: string; description?: string; domains?: string; isAutoDeployEnabled?: boolean; isForceHttpsEnabled?: boolean; instantDeploy?: boolean; } export async function createPublicApp(opts: CreatePublicAppOpts): Promise<{ uuid: string }> { const body = stripUndefined({ project_uuid: opts.projectUuid, server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID, environment_name: opts.environmentName ?? 'production', destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID, git_repository: opts.gitRepository, git_branch: opts.gitBranch ?? 'main', build_pack: opts.buildPack ?? 'nixpacks', ports_exposes: opts.portsExposes, name: opts.name, description: opts.description, domains: opts.domains, is_auto_deploy_enabled: opts.isAutoDeployEnabled ?? true, is_force_https_enabled: opts.isForceHttpsEnabled ?? true, instant_deploy: opts.instantDeploy ?? true, }); return coolifyFetch('/applications/public', { method: 'POST', body: JSON.stringify(body), }); } export async function updateApplication( uuid: string, patch: Record ): Promise<{ uuid: string }> { return coolifyFetch(`/applications/${uuid}`, { method: 'PATCH', body: JSON.stringify(stripUndefined(patch)), }); } export async function setApplicationDomains( uuid: string, domains: string[], opts: { forceOverride?: boolean } = {} ): Promise<{ uuid: string }> { // Coolify validates each entry as a URL, so bare hostnames need a scheme. const normalized = domains.map(d => { const trimmed = d.trim(); if (/^https?:\/\//i.test(trimmed)) return trimmed; return `https://${trimmed}`; }); return updateApplication(uuid, { domains: normalized.join(','), force_domain_override: opts.forceOverride ?? true, is_force_https_enabled: true, }); } export async function deleteApplication( uuid: string, opts: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; } = {} ): Promise { const q = new URLSearchParams(); if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations)); if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes)); if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup)); if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks)); const qs = q.toString(); await coolifyFetch(`/applications/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' }); } export async function listApplications(): Promise { return coolifyFetch('/applications'); } export async function deployApplication(uuid: string, opts: { force?: boolean } = {}): Promise<{ deployment_uuid: string }> { // Coolify v4 exposes deploy as POST /deploy?uuid=...&force=... // The older /applications/{uuid}/deploy path is a 404 on current // Coolify releases. const q = new URLSearchParams({ uuid }); if (opts.force) q.set('force', 'true'); const res = await coolifyFetch(`/deploy?${q.toString()}`, { method: 'POST' }); // Response shape: { deployments: [{ deployment_uuid, ... }] } or direct object. const first = Array.isArray(res?.deployments) ? res.deployments[0] : res; return { deployment_uuid: first?.deployment_uuid ?? first?.uuid ?? '', }; } export async function getApplication(uuid: string): Promise { return coolifyFetch(`/applications/${uuid}`); } export async function startApplication(uuid: string): Promise { await coolifyFetch(`/applications/${uuid}/start`, { method: 'POST' }); } export async function stopApplication(uuid: string): Promise { await coolifyFetch(`/applications/${uuid}/stop`, { method: 'POST' }); } export async function restartApplication(uuid: string): Promise { await coolifyFetch(`/applications/${uuid}/restart`, { method: 'POST' }); } export async function getDeploymentLogs(deploymentUuid: string): Promise<{ logs: string }> { return coolifyFetch(`/deployments/${deploymentUuid}/logs`); } export async function listApplicationDeployments(uuid: string): Promise { // Coolify v4 nests this under /deployments/applications/{uuid} // and returns { count, deployments }. Normalize to a flat array. const raw = await coolifyFetch(`/deployments/applications/${uuid}?take=50`); if (Array.isArray(raw)) return raw as CoolifyDeployment[]; return (raw?.deployments ?? []) as CoolifyDeployment[]; } // ────────────────────────────────────────────────── // Legacy monorepo helper (still used by older flows) // ────────────────────────────────────────────────── 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 = COOLIFY_DEFAULT_SERVER_UUID, 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}`, }), }); } // ────────────────────────────────────────────────── // 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 { 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', }); } // ────────────────────────────────────────────────── // Services (raw Coolify service resources) // ────────────────────────────────────────────────── export interface CoolifyService { uuid: string; name: string; status?: string; /** Coolify template slug the service was provisioned from, e.g. "pocketbase". */ service_type?: string; project_uuid?: string; environment_id?: number; environment?: { id?: number; project_uuid?: string; project?: { uuid?: string } }; } export async function listServices(): Promise { return coolifyFetch('/services'); } export async function getService(uuid: string): Promise { return coolifyFetch(`/services/${uuid}`); } export async function createService(opts: { projectUuid: string; type: string; // e.g. "pocketbase", "authentik", "zitadel" name: string; description?: string; serverUuid?: string; environmentName?: string; destinationUuid?: string; instantDeploy?: boolean; }): Promise<{ uuid: string }> { const body = stripUndefined({ project_uuid: opts.projectUuid, server_uuid: opts.serverUuid ?? COOLIFY_DEFAULT_SERVER_UUID, environment_name: opts.environmentName ?? 'production', destination_uuid: opts.destinationUuid ?? COOLIFY_DEFAULT_DESTINATION_UUID, type: opts.type, name: opts.name, description: opts.description, instant_deploy: opts.instantDeploy ?? true, }); return coolifyFetch('/services', { method: 'POST', body: JSON.stringify(body) }); } export async function deleteService( uuid: string, opts: { deleteConfigurations?: boolean; deleteVolumes?: boolean; dockerCleanup?: boolean; deleteConnectedNetworks?: boolean; } = {} ): Promise { const q = new URLSearchParams(); if (opts.deleteConfigurations !== undefined) q.set('delete_configurations', String(opts.deleteConfigurations)); if (opts.deleteVolumes !== undefined) q.set('delete_volumes', String(opts.deleteVolumes)); if (opts.dockerCleanup !== undefined) q.set('docker_cleanup', String(opts.dockerCleanup)); if (opts.deleteConnectedNetworks !== undefined) q.set('delete_connected_networks', String(opts.deleteConnectedNetworks)); const qs = q.toString(); await coolifyFetch(`/services/${uuid}${qs ? '?' + qs : ''}`, { method: 'DELETE' }); } // ────────────────────────────────────────────────── // Tenant helpers — every endpoint that returns an app/db/service runs // through one of these so cross-project access is impossible. // // Coolify v4's /applications, /databases and /services list endpoints do NOT // include `project_uuid` on the returned resources. The authoritative link is // `environment_id`, which we match against the project's environments (fetched // via /projects/{uuid}). // // For efficient listing per project, we use `/projects/{uuid}/{envName}` which // returns the resources scoped to that project+environment in one call. // ────────────────────────────────────────────────── export class TenantError extends Error { status = 403 as const; } function envIdOf( resource: CoolifyApplication | CoolifyDatabase | CoolifyService ): number | null { return ( (typeof resource.environment_id === 'number' ? resource.environment_id : null) ?? (resource.environment && typeof resource.environment.id === 'number' ? resource.environment.id : null) ); } function explicitProjectUuidOf( resource: CoolifyApplication | CoolifyDatabase | CoolifyService ): string | null { return ( resource.project_uuid ?? resource.environment?.project_uuid ?? resource.environment?.project?.uuid ?? null ); } /** Fetch the set of environment IDs that belong to a project. */ async function projectEnvIds(projectUuid: string): Promise> { const project = await getProject(projectUuid); const ids = new Set(); for (const env of project.environments ?? []) { if (typeof env.id === 'number') ids.add(env.id); } return ids; } async function ensureResourceInProject( resource: CoolifyApplication | CoolifyDatabase | CoolifyService, resourceKind: string, expectedProjectUuid: string ): Promise { const explicit = explicitProjectUuidOf(resource); if (explicit && explicit === expectedProjectUuid) return; if (explicit && explicit !== expectedProjectUuid) { throw new TenantError( `${resourceKind} ${resource.uuid} does not belong to project ${expectedProjectUuid}` ); } const envId = envIdOf(resource); if (envId == null) { throw new TenantError( `${resourceKind} ${resource.uuid} has no environment_id; cannot verify project ${expectedProjectUuid}` ); } const envIds = await projectEnvIds(expectedProjectUuid); if (!envIds.has(envId)) { throw new TenantError( `${resourceKind} ${resource.uuid} does not belong to project ${expectedProjectUuid}` ); } } export async function getApplicationInProject( appUuid: string, expectedProjectUuid: string ): Promise { const app = await getApplication(appUuid); await ensureResourceInProject(app, 'Application', expectedProjectUuid); return app; } export async function getDatabaseInProject( dbUuid: string, expectedProjectUuid: string ): Promise { const db = await getDatabase(dbUuid); await ensureResourceInProject(db, 'Database', expectedProjectUuid); return db; } export async function getServiceInProject( serviceUuid: string, expectedProjectUuid: string ): Promise { const svc = await getService(serviceUuid); await ensureResourceInProject(svc, 'Service', expectedProjectUuid); return svc; } /** * Response shape of GET /projects/{uuid}/{envName}. * Coolify splits databases by flavor across sibling arrays. */ interface CoolifyProjectEnvResources { id: number; uuid: string; name: string; applications?: CoolifyApplication[]; services?: CoolifyService[]; postgresqls?: CoolifyDatabase[]; mysqls?: CoolifyDatabase[]; mariadbs?: CoolifyDatabase[]; mongodbs?: CoolifyDatabase[]; redis?: CoolifyDatabase[]; keydbs?: CoolifyDatabase[]; dragonflies?: CoolifyDatabase[]; clickhouses?: CoolifyDatabase[]; } const DB_ARRAY_KEYS: Array = [ 'postgresqls', 'mysqls', 'mariadbs', 'mongodbs', 'redis', 'keydbs', 'dragonflies', 'clickhouses', ]; async function getProjectEnvResources( projectUuid: string, envName: string ): Promise { return coolifyFetch(`/projects/${projectUuid}/${encodeURIComponent(envName)}`); } async function forEachEnv( projectUuid: string, collect: (envResources: CoolifyProjectEnvResources) => T[] ): Promise { const project = await getProject(projectUuid); const out: T[] = []; for (const env of project.environments ?? []) { const envResources = await getProjectEnvResources(projectUuid, env.name); out.push(...collect(envResources)); } return out; } export async function listApplicationsInProject( projectUuid: string ): Promise { return forEachEnv(projectUuid, r => r.applications ?? []); } export async function listDatabasesInProject( projectUuid: string ): Promise { return forEachEnv(projectUuid, r => { const out: CoolifyDatabase[] = []; for (const k of DB_ARRAY_KEYS) { const arr = r[k]; if (Array.isArray(arr)) out.push(...(arr as CoolifyDatabase[])); } return out; }); } export async function listServicesInProject( projectUuid: string ): Promise { return forEachEnv(projectUuid, r => r.services ?? []); } /** @deprecated Use getApplicationInProject / ensureResourceInProject instead. */ export function projectUuidOf( resource: CoolifyApplication | CoolifyDatabase | CoolifyService ): string | null { return explicitProjectUuidOf(resource); } // ────────────────────────────────────────────────── // util // ────────────────────────────────────────────────── function stripUndefined>(obj: T): Partial { const out: Partial = {}; for (const [k, v] of Object.entries(obj)) { if (v !== undefined) (out as Record)[k] = v; } return out; } export { COOLIFY_DEFAULT_SERVER_UUID, COOLIFY_DEFAULT_DESTINATION_UUID };