CRITICAL: every Vibn project was rendering every other project's
services in the same workspace (Twenty CRM, n8n, all databases,
all secrets). Tenancy was effectively broken — cross-project data
exposure inside a workspace.
Root cause:
- Coolify's POST /projects validates `description` against a strict
allowlist (letters, numbers, spaces, and `- _ . , ! ? ( ) ' " + = * / @ &`).
- Our description "Vibn project: <name> (workspace: <slug>)" contains
two colons. Every project-create on Coolify returned 422.
- lib/projects.ts caught that 422 and fell back to
`workspace.coolify_project_uuid` so deploys "weren't blocked."
- That UUID is shared by every Vibn project in the workspace, so
listServicesInProject(coolifyProjectUuid) returned the union of
all projects' services, applications, and databases for any
project in the workspace. The Product, Hosting, and Infrastructure
tabs all rendered cross-tenant data as if it were the current
project's.
Fixes (defense in depth — fix at every layer):
1. lib/coolify.ts createProject(): sanitize the description against
Coolify's allowlist at the boundary so no caller can ever ship
a description that 422s. Replaces disallowed chars with `-`,
collapses runs, caps at 255 chars.
2. lib/projects.ts ensureProjectCoolifyProject():
- Pre-sanitize the description we pass (belt + suspenders).
- Detect when `stored === workspace.coolify_project_uuid` (the
legacy bad state) and re-provision a dedicated project.
- REMOVE the workspace-UUID fallback on create failure. A 422
now leaves coolifyProjectUuid null and the UI shows an empty
state, which is correct: better to surface "no resources" than
to lie about which project owns what.
- Export sanitizeCoolifyDescription helper for reuse.
3. /api/projects/[projectId]/anatomy/route.ts: SELF-HEAL on every
read. If the project's stored Coolify UUID matches the
workspace's UUID, we treat it as missing, re-provision a
dedicated Coolify project on the fly (idempotent — reuses the
existing one if found by name), persist the new UUID, and
continue serving with the corrected scope. If provisioning
fails we fall back to undefined, NOT the workspace UUID, so
no cross-tenant data ever surfaces again.
The self-heal means existing already-broken projects will fix
themselves on the next page load — no manual data migration needed.
Made-with: Cursor
Stage 3 of per-project Coolify isolation. Adds an authoritative ownership
table so apps_list { projectId } returns ONLY the resources actually owned
by that Vibn project, even when multiple Vibn projects share a single
Coolify project (the legacy workspace-level vibn-ws-{slug}).
- New table fs_project_resources (project_id, resource_uuid, type, workspace).
Auto-created on first use.
- lib/projects.ts: linkResourceToProject / unlinkResource /
getProjectResourceUuids / getProjectIdForResource helpers.
- apps_list { projectId }: when the project's coolifyProjectUuid equals the
legacy workspace project, restrict results to explicitly-linked resources.
When it has a dedicated Coolify project, return everything in that project.
- apps_create / databases_create: auto-link the newly-created resource to
the requesting Vibn project.
- apps_delete / databases_delete / services_delete: unlink on success.
- projects_get → possibleDeployments: prefer explicit links; fuzzy-match
fallback only fires when no link table entry exists yet.
- POST /api/projects/backfill-isolation: idempotent migration that mints a
dedicated Coolify project for every Vibn project AND records existing
coolifyServiceUuid/coolifyAppUuid/coolifyDatabaseUuid links. Resolves
the "Twenty CRM project shows n8n" bug for legacy projects without
needing to physically move services in Coolify.
Made-with: Cursor
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