Anatomy + UI rewrite — locked the conceptual model after user feedback:
Product = "what makes up the thing you're shipping":
- Codebases (Gitea repos)
- Images (Coolify services backed by upstream Docker images: Twenty
CRM, n8n, etc.)
- Dev containers no longer surface here. The vibn-dev-* container is
the AI's workshop, not a product surface; previews it serves still
appear under Hosting → Previews.
Hosting = "where it lives + how it gets there", unified:
- Live: every running endpoint as one list. Each item carries a
source badge ("repo" | "image"), status dot, attached domain, and
last-build summary inline. No separate Build, Domains or Services
categories — those are properties on each Live item.
- Previews: dev container preview URLs (unchanged).
Anatomy endpoint reshaped accordingly:
- product.{codebases, images}
- hosting.{live, previews} (was production/services/previewUrls/domains)
- lastBuild summary fetched per repo-app via listApplicationDeployments
in parallel.
ProjectStagePill rewired to derive Live/Down/Building from hosting.live
+ hosting.previews. dev-container-detail.tsx removed.
services.* MCP tools added so AI agents can manage Coolify services
(Twenty CRM, n8n, …) the same way they manage apps:
- services.list, services.get
- services.start, services.stop
- services.envs.list, services.envs.upsert
All tenant-scoped via getServiceInWorkspace + getOwnedCoolifyProjectUuids.
vibn-dev-* containers stay hidden from services.list.
Made-with: Cursor
The vibn-dev-* services that the AI authors code in conceptually
belong to Product (build surface), not Hosting (runtime + reach).
Anatomy endpoint now splits Coolify services by name prefix:
- vibn-dev-* → product.devContainers[]
- everything else → hosting.services[]
Product tab gains a "Workspace" section above the codebases stack
with a single dev-container tile. Selecting it shows status +
active dev servers in the right pane. Codebase + file selection
behaves the same as before.
Hosting tab restructured from a stack of always-visible cards to
the same tile-rail pattern Product uses: left rail has 4 always-
present categories (Production / Services / Previews / Domains)
each with a count badge, items inside are clickable tiles, right
pane shows details for the selected item. Empty categories show a
one-liner explaining what would appear there — teaches the user
the model on a brand-new project without being preachy.
Made-with: Cursor
Adds GET /api/projects/[id]/anatomy returning the full project shape
in one shot — codebases (Gitea), production apps (Coolify
applications matched by repo URL), dev services (Coolify services in
the project's coolifyProjectUuid), preview URLs (active fs_dev_servers
rows), and aggregated domains. Each tab reads its own slice via the
new useAnatomy() hook so the page never fans out 3+ requests.
Hosting tab is now real: surfaces production / dev services / preview
URLs / domains with empty-state CTAs explaining what each means and
why it's empty when applicable. Includes a banner when nothing at all
is deployed for the project.
Project header pill (previously hard-coded from data.status, which
historically lied) now derives stage from hosting reality:
- any production app running → Live (green)
- any failed app → Down (red)
- any service / preview → Building (blue)
- else → fallback to data.status
Product tab refactored onto the same useAnatomy hook so we no longer
maintain two near-identical fetchers.
Made-with: Cursor
Adds GET /api/projects/[id]/codebases that inspects the project's
Gitea repo:
- apps/* present → one codebase per subdir (Turborepo)
- else → single codebase rooted at the repo root
- no repo → empty list with reason="no_repo"
Product tab now fetches this list, picks the first as the default
selection, and surfaces explicit loading / error / empty states
(previously it hung on "Loading…" when apps/web 404'd in single-
repo projects).
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
Every new Vibn project was being seeded with:
- a turborepo scaffold pushed to its Gitea repo
(apps/product, apps/website, apps/admin, apps/storybook)
- 4 corresponding Coolify services that nobody ever deployed
Both predate templates / GitHub imports / on-demand AI deploys and
created noise in every workspace's Coolify view (and confused the AI
about what was actually running).
Now project creation provisions just:
- a Gitea repo (empty unless GitHub mirror is requested)
- a dedicated Coolify project ready to receive deploys
Apps land in the project via apps_create on demand — what the user
actually wants, not a guess. The lib/scaffold/turborepo/ files remain
in source for future opt-in re-introduction.
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
Without this the bot PAT 403s on POST /orgs/{org}/repos, which is
the single most important operation — creating new project repos
inside the workspace's Gitea org.
Made-with: Cursor
Adds logical multi-tenancy on top of Coolify + Gitea so every Vibn
account gets its own isolated tenant boundary, and exposes that
boundary to AI agents (Cursor, Claude Code, scripts) through
per-workspace bearer tokens.
Schema (additive, idempotent — run /api/admin/migrate once after deploy)
- vibn_workspaces: slug, name, owner, coolify_project_uuid,
coolify_team_id (reserved for when Coolify ships POST /teams),
gitea_org, provision_status
- vibn_workspace_members: room for multi-user workspaces later
- vibn_workspace_api_keys: sha256-hashed bearer tokens
- fs_projects.vibn_workspace_id: nullable FK linking projects
to their workspace
Provisioning
- On first sign-in, ensureWorkspaceForUser() inserts the row
(no network calls — keeps signin fast).
- On first project create, ensureWorkspaceProvisioned() lazily
creates a Coolify Project (vibn-ws-{slug}) and a Gitea org
(vibn-{slug}). Failures are recorded on the row, not thrown,
and POST /api/workspaces/{slug}/provision retries.
Auth surface
- lib/auth/workspace-auth.ts: requireWorkspacePrincipal() accepts
either a NextAuth session or "Authorization: Bearer vibn_sk_...".
The bearer key is hard-pinned to one workspace — it cannot
reach any other tenant.
- mintWorkspaceApiKey / listWorkspaceApiKeys / revokeWorkspaceApiKey
Routes
- GET /api/workspaces list
- GET /api/workspaces/[slug] details
- POST /api/workspaces/[slug]/provision retry provisioning
- GET /api/workspaces/[slug]/keys list keys
- POST /api/workspaces/[slug]/keys mint key (token shown once)
- DELETE /api/workspaces/[slug]/keys/[keyId] revoke
UI
- components/workspace/WorkspaceKeysPanel.tsx: identity card,
keys CRUD with one-time secret reveal, and a "Connect Cursor"
block with copy/download for:
.cursor/rules/vibn-workspace.mdc — rule telling the agent
about the API + workspace IDs + house rules
~/.cursor/mcp.json — MCP server registration with key
embedded (server URL is /api/mcp; HTTP MCP route lands next)
.env.local — VIBN_API_KEY + smoke-test curl
- Slotted into existing /[workspace]/settings between Workspace
and Notifications cards (no other layout changes).
projects/create
- Resolves the user's workspace (creating + provisioning lazily).
- Repos go under workspace.gitea_org (falls back to GITEA_ADMIN_USER
for backwards compat).
- Coolify services are created inside workspace.coolify_project_uuid
(renamed {slug}-{appName} to stay unique within the namespace) —
no more per-Vibn-project Coolify Project sprawl.
- Stamps vibn_workspace_id on fs_projects.
lib/gitea
- createOrg, getOrg, addOrgOwner, getUser
- createRepo now routes /orgs/{owner}/repos when owner != admin
Also includes prior-turn auth hardening that was already in
authOptions.ts (CredentialsProvider for dev-local, isLocalNextAuth
cookie config) bundled in to keep the auth layer in one consistent
state.
.env.example
- Documents GITEA_API_URL / GITEA_API_TOKEN / GITEA_ADMIN_USER /
GITEA_WEBHOOK_SECRET and COOLIFY_URL / COOLIFY_API_TOKEN /
COOLIFY_SERVER_UUID, with the canonical hostnames
(git.vibnai.com, coolify.vibnai.com).
Post-deploy
- Run once: curl -X POST https://vibnai.com/api/admin/migrate \\
-H "x-admin-secret: \$ADMIN_MIGRATE_SECRET"
- Existing users get a workspace row on next sign-in.
- Existing fs_projects keep working (legacy gitea owner + their
own per-project Coolify Projects); new projects use the
workspace-scoped path.
Not in this commit (follow-ups)
- Wiring requireWorkspacePrincipal into the rest of /api/projects/*
so API keys can drive existing routes
- HTTP MCP server at /api/mcp (the mcp.json snippet already
points at the right URL — no client re-setup when it lands)
- Backfill script to assign legacy fs_projects to a workspace
Made-with: Cursor
- PRD page now has a tabbed view: PRD | Architecture
Architecture tab renders apps, packages, infrastructure, integrations,
and risk notes as structured cards. Only shown when arch doc exists.
- Advisor route now includes the architecture summary and key fields
in the COO's knowledge context so the orchestrator knows what's
been planned technically
Made-with: Cursor
The advisor route now proxies to /orchestrator/chat on agents.vibnai.com
instead of calling Gemini directly. The Orchestrator (Claude Sonnet 4.6)
has full tool access — Gitea, Coolify, web search, memory, agent spawning.
- Build project knowledge_context from DB (name, vision, repo, PRD,
phases, apps, recent sessions) and inject as COO persona + data
- Convert frontend history format (model→assistant) for the orchestrator
- Return orchestrator reply as streaming text response
- Session scoped per project for in-memory context persistence
Made-with: Cursor
MigrateSetup now sends the PAT field to the API; create route
forwards it as github_token so the agent runner can clone private repos.
Made-with: Cursor
- Top bar left section (320px) = logo + project name, aligns with chat panel
- Top bar right section = Build|Market|Assist pills + tool icons (Preview, Tasks, Code, Design, Backend) + avatar
- Read GOOGLE_API_KEY inside POST handler (not top-level) to ensure env is resolved at request time
Made-with: Cursor
- New CooChat component: streaming Gemini-backed advisor chat, message
bubbles, typing cursor animation, Shift+Enter for newlines
- New /api/projects/[projectId]/advisor streaming endpoint: builds a
COO system prompt from project context (name, description, vision,
repo), proxies Gemini SSE stream back to the client
- Restructured BuildHubInner layout:
Left (340px): CooChat — persistent across all Build sections
Inner nav (200px): Build pills + contextual items (apps, tree, surfaces)
Main area: File viewer for Code, Layouts content, Infra content
- AgentMode removed from main view — execution surfaces via COO delegation
Made-with: Cursor
- Fall back to CODEBASE_MAP.md parsing when no apps/ dir exists
- Further fallback: scan top-level dirs for deployable app signals
(package.json, Dockerfile, requirements.txt, next.config.*, etc.)
- Skips docs, scripts, keys, and other non-app directories
- Returns isImport flag to frontend for context
Made-with: Cursor
- sessions POST: look up coolifyServiceUuid, pass autoApprove:true to runner
- sessions PATCH: approved added to terminal statuses (sets completed_at)
- build/page.tsx: approved status, STATUS_COLORS/LABELS for "Shipped",
auto-committed UI in changed files panel, bottom bar for approved state
- Architecture doc: fully updated with current state
Made-with: Cursor
- retry/route.ts: reset failed/stopped session and re-fire agent runner
with optional continueTask follow-up text
- build/page.tsx: Retry button and Follow up input appear on failed/stopped
sessions so users can continue without losing context or creating a
duplicate session; task input hint clarifies each Run = new session
Made-with: Cursor
PostgreSQL can't implicitly coerce text params to UUID columns.
Add explicit ::uuid casts on id and project_id in all agent session
routes (list, get, patch, stop, approve).
Made-with: Cursor
- Wire Approve & commit button: shows commit message input, calls
POST /api/.../sessions/[id]/approve which asks agent runner to
git commit + push, then marks session as approved in DB
- Adaptive polling: 500ms while session running, 5s when idle —
output feels near-real-time without hammering the API
- Auto-refresh session list when a session completes
- Open in Theia links to theia.vibnai.com (escape hatch for manual edits)
Made-with: Cursor
- Sessions route now reads giteaRepo from project.data and forwards it
to /agent/execute so the runner can clone/update the correct repo
- PATCH route now validates x-agent-runner-secret header to prevent
unauthorized session output injection
Made-with: Cursor
- Build page: full file tree (lazy-load dirs) + code preview panel
with line numbers and token-level syntax colouring (VS Code dark theme)
- New API route /api/projects/[id]/file proxies Gitea contents API
returning directory listings or decoded file content
- Sidebar Apps section now links to /build instead of raw Gitea URL
- Status indicator replaced with a proper coloured dot (amber/blue/green)
alongside the status label text
Made-with: Cursor
Add ::text cast to all $1/$2 parameters so PostgreSQL never needs
to infer types. Split SELECT and UPDATE into separate try/catch blocks
with distinct error labels so logs show exactly which query fails.
Made-with: Cursor
PostgreSQL could not determine the type of $2 in 'WHERE id = $2'
when id column type is UUID. Casting the column (id::text = $1)
sidesteps the extended-protocol type inference issue. Also moves
projectId to $1 to match the proven working pattern in other routes.
Made-with: Cursor
The PATCH handler used SQL 'updated_at = NOW()' which doesn't exist
on fs_projects (all timestamps live inside the data JSONB blob).
Rewrote to use the same read-merge-write pattern as other working
routes: fetch current data, merge in JS, write back as data::jsonb.
Made-with: Cursor
- Architecture route now uses /generate endpoint (no Atlas session
overhead, no conflicting system prompt) for clean JSON generation
- Design page fetches saved architecture on load and maps designSurfaces
to known surface IDs via fuzzy match; AI-suggested surfaces are
pre-selected in the picker with an "AI" badge and explanatory note
Made-with: Cursor
- GET /api/projects/[id]/atlas-chat returns stored user+assistant messages
- POST handles __atlas_init__ trigger: runs once when no history exists,
not stored as a user turn so Atlas intro appears cleanly
- Rewrite AtlasChat.tsx: fully self-contained component with own message
state; loads history from DB on mount, only greets on first open
- Remove assistant-ui runtime dependency for message persistence
- Add Vision & Success Metrics, Integrations & Dependencies, Open Questions
to PRD section tracker (now 12 sections matching the PDF)
Made-with: Cursor
Phase 1: user picks which surfaces their product needs (Web App,
Marketing Site, Admin, Mobile, Email, Docs). Phase 2: per-surface
horizontal card gallery with mini visual previews of each UI library.
Lock in confirms the choice; locked themes are saved to DB and shown
to the AI coder. Surfaces and themes stored in fs_projects.data.
Made-with: Cursor