Per-project Coolify project isolation (Stage 1)
Each Vibn project now gets its OWN Coolify project named
vibn-{workspace-slug}-{project-slug}. All apps/databases/services
deployed for the project land inside that Coolify project, giving
us clean grouping, cascading delete, and per-project domain
namespaces.
Changes:
- New lib/projects.ts: ensureProjectCoolifyProject (idempotent
create/lookup), getProjectCoolifyUuid, getOwnedCoolifyProjectUuids
- /api/projects/create: pre-insert row, mint per-project Coolify
project, then complete the row with productData (preserves the
coolifyProjectUuid that was just set)
- apps.list (MCP): without projectId, aggregates across ALL
workspace-owned Coolify projects; with projectId, scopes to
that project's Coolify project. Returns coolifyProjectUuid
on each result so the AI knows where things live.
- apps.create (MCP): accepts projectId; auto-mints the Vibn
project's Coolify project on first deploy if missing
- apps_list/apps_create tool defs: projectId param surfaced
- System prompt: Project as first-class — planning + live as
facets of ONE thing, never as separate worlds. AI told to
always pass projectId on apps_create.
Stage 2 (next): set-aware ensureResourceInProject across all
single-resource MCP tools (apps.get/delete/exec/etc.) and
cascading delete via projects.delete.
Made-with: Cursor
This commit is contained in:
@@ -6,6 +6,7 @@ import { createRepo, createWebhook, getRepo, listWebhooks, GITEA_ADMIN_USER_EXPO
|
||||
import { pushTurborepoScaffold } from '@/lib/scaffold';
|
||||
import { createMonorepoAppService } from '@/lib/coolify';
|
||||
import { getOrCreateProvisionedWorkspace } from '@/lib/workspaces';
|
||||
import { ensureProjectCoolifyProject } from '@/lib/projects';
|
||||
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
||||
|
||||
const GITEA_ADMIN_USER = GITEA_ADMIN_USER_EXPORT;
|
||||
@@ -181,9 +182,25 @@ export async function POST(request: Request) {
|
||||
name: string; path: string; coolifyServiceUuid: string | null; domain: string | null;
|
||||
}> = appNames.map(name => ({ name, path: `apps/${name}`, coolifyServiceUuid: null, domain: null }));
|
||||
|
||||
// The workspace's Coolify Project IS our team boundary. All Vibn
|
||||
// projects for a workspace share one Coolify Project namespace.
|
||||
const coolifyProjectUuid: string | null = vibnWorkspace.coolify_project_uuid;
|
||||
// Each Vibn project gets its OWN Coolify Project under the workspace.
|
||||
// Naming: `vibn-{workspace-slug}-{project-slug}`. Falls back to the
|
||||
// workspace's legacy Coolify Project UUID if Coolify provisioning fails,
|
||||
// so apps still deploy (with degraded isolation).
|
||||
//
|
||||
// Note: ensureProjectCoolifyProject reads the row, but we INSERT the row
|
||||
// further below. To break the chicken-and-egg we insert a minimal row
|
||||
// first, then provision Coolify, then complete the row.
|
||||
await query(
|
||||
`INSERT INTO fs_projects (id, data, user_id, workspace, slug, vibn_workspace_id)
|
||||
VALUES ($1, '{}'::jsonb, $2, $3, $4, $5)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[projectId, firebaseUserId, workspace, slug, vibnWorkspace.id],
|
||||
);
|
||||
const coolifyProjectUuid: string | null = await ensureProjectCoolifyProject(
|
||||
projectId,
|
||||
vibnWorkspace,
|
||||
{ projectSlug: slug, projectName },
|
||||
);
|
||||
|
||||
if (giteaCloneUrl && coolifyProjectUuid) {
|
||||
for (const app of provisionedApps) {
|
||||
@@ -259,9 +276,18 @@ export async function POST(request: Request) {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
// Update the row we pre-inserted above with the full project data.
|
||||
// We merge with existing data so the coolifyProjectUuid set by
|
||||
// ensureProjectCoolifyProject() above is preserved.
|
||||
await query(`
|
||||
INSERT INTO fs_projects (id, data, user_id, workspace, slug, vibn_workspace_id)
|
||||
VALUES ($1, $2::jsonb, $3, $4, $5, $6)
|
||||
UPDATE fs_projects
|
||||
SET data = data || $2::jsonb,
|
||||
user_id = $3,
|
||||
workspace = $4,
|
||||
slug = $5,
|
||||
vibn_workspace_id = $6,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, [projectId, JSON.stringify(projectData), firebaseUserId, workspace, slug, vibnWorkspace.id]);
|
||||
|
||||
// Associate any unlinked sessions for this workspace path
|
||||
|
||||
Reference in New Issue
Block a user