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
User can now click "Connect GitHub" inside the Import-existing-code
flow, sign in via GitHub, and pick a repo from a searchable list of
their own + collaborator + org repos. Both public and private repos
work — the encrypted access token on the user's account is auto-
attached when the create endpoint runs the agent-runner mirror.
OAuth flow:
- GET /api/integrations/github/connect — generates state, sets
a 10-min httpOnly cookie, 302s to GitHub authorize.
- GET /api/integrations/github/callback — verifies state,
exchanges code for token, fetches /user, encrypts the
token with secret-box (AES-256-GCM, VIBN_SECRETS_KEY) and
persists it on fs_users.data.integrations.github.
Bounces back to ?gh_connected=login or ?gh_error=msg.
- GET /api/integrations/github/repos — server-side fetches
the connected user's repos (per_page=100, sort=pushed,
affiliation=owner+collaborator+org_member). Returns the
GitHub login + a stripped repo summary; never the token.
- POST /api/integrations/github/disconnect — drops the integration
from fs_users (does NOT revoke on github.com).
Scopes requested: repo, read:user.
Token storage:
- Encrypted at rest with secret-box (lib/auth/secret-box.ts) using
VIBN_SECRETS_KEY. Tokens never leave the server.
- One token per fs_users row, keyed by email.
ImportSetup UI:
- On mount, fires /repos to detect connection state.
- If connected: shows a connected-as-@login chip with disconnect
link, a search-as-you-type repo picker (max 220px scroll, badges
for Private / language), and a "paste a different URL instead"
escape hatch.
- If not connected: shows a Connect GitHub card with a public-URL
fallback inline.
- On return from OAuth (?gh_connected=… or ?gh_error=…), surfaces
a toast and silently refreshes the repo list.
- Selected repo carries default_branch + repo id into the create
payload so we can store them on the project for later UI hints.
/api/projects/create:
- When a githubRepoUrl is mirrored, falls back to the user's
OAuth-linked token if no PAT is explicitly passed. Means the
flow "just works" for private repos once GitHub is connected.
Required env (already set in production):
- GITHUB_CLIENT_ID
- GITHUB_CLIENT_SECRET
Made-with: Cursor
"Myself / A client" was about who *owns* the project (a billing
concern), but at creation time we want to know who *uses* it — that's
what determines which Infrastructure providers we should pre-stage.
team = internal users (your team / employees)
→ SSO-style auth, no payments by default, simple roles
customers = external users (the public)
→ public sign-up + payments + transactional email by
default, custom domain matters
Both choices are reversible from the Infrastructure tab later — the
selector copy makes that explicit so users don't feel locked in.
Changes: - setup-shared: ForWhomSelector ("Myself" / "A client") replaced by
AudienceSelector ("My team" / "Customers"), with an "you can
change this later" hint underneath. New Audience union type
exported for the three setup screens to share.
- BuildSetup / OssSetup / ImportSetup: swap state + import + payload.
Defaults: BuildSetup → customers (most "vibe coder" projects are
public products), ImportSetup → customers (existing repos usually
are too), OssSetup → team (Twenty / n8n / Plausible style tools
are most often deployed for internal use).
- /api/projects/create: drop isForClient (we never read it
anywhere), persist audience as a first-class field on the
project record so the AI can branch on it during the first chat.
Made-with: Cursor
User feedback: the previous flow was a single-screen "name + audience"
dialog that gave AI no context about what the user actually wanted to
make. That worked for the demo but produced messy projects in practice
because everything was decided after the fact in chat.
The new flow asks the user one human question first ("How would you
like to begin?") and then captures the minimum context needed to seed
the AI's first conversation in the project.
Three paths, each is a 2-step setup screen with internal step dots:
- Build your own idea — Step 1: name + audience. Step 2: free-text
"what do you want to build". Becomes the project's vision and the
AI's first-message context.
- Run an open source tool — Step 1: name + audience. Step 2:
segmented tabs to either (a) paste a GitHub link or (b) describe
the kind of tool you want and have Vibn find one. Vision is set
to either "Install and host this open-source project: <url>" or
"Find and install an open-source tool that fits this need: <desc>"
so the AI knows which mode to operate in on first chat.
- Import existing code — Step 1: name + audience + repo URL.
Step 2: optional "what do you want to do with it" textarea.
Public repos only for v1; private-repo OAuth lands later.
Backend:
- /api/projects/create now accepts and persists `creationMode` and
`sourceData` on the project record under a `kickoff` blob:
{ mode, sourceData, vision, createdAt }
The chat endpoint will read this on first turn to seed the AI
with the user's stated intent rather than asking them to re-type
it in chat.
Cleanup:
- Removed FreshIdeaSetup, CodeImportSetup, ChatImportSetup,
MigrateSetup — replaced by BuildSetup, OssSetup, ImportSetup.
- Removed the unused initialWorkspacePath prop from
project-association-prompt (the new flow doesn't take it).
- TypeSelector defaults are restored — the modal opens on the
type-picker step now, not directly on a setup form.
UI building blocks added to setup-shared:
- TextArea (multi-line input)
- StepDots (page indicator)
- SegmentedTabs (generic-typed tab selector, used in OSS Step 2)
- SecondaryButton (used as ← Back inside Step 2)
Made-with: Cursor
UX rework after iteration with the user:
- Drop SMS, Analytics, Search, Monitoring categories from the rail.
They were detection-only with no first-class UX behind them; surface
is cleaner without them and they can return when each gets real
flows (auth-style "edit configurables", payment-style "connect").
- Storage no longer tries to detect S3/R2/GCS env vars. Instead it
surfaces the workspace's bundled Vibn-provisioned GCS bucket
(S3-compatible HMAC), with status, region, access id, and a
one-shot env snippet for app config.
- Email category no longer mixes in SMS providers.
- LLM renamed to "Models"; empty state mentions BYOK as upcoming.
- Payments empty state has a "Connect Stripe (coming soon)" CTA;
Stripe detail surfaces the webhook URL guidance.
- Secrets detail now lists actual env-var key names per resource,
grouped by detected provider (Stripe block, OpenAI block, etc.)
with an "Other (project-defined)" catch-all. Each row has Edit +
Rotate icon buttons (currently disabled with tooltips — wire-up
to apps.envs.upsert / services.envs.upsert lands in iter 2).
Live database inspection (Postgres only for now):
- New /api/projects/[id]/databases/[uuid]/tables — auth-scoped, lists
user-tables across non-system schemas via SSH-exec into the
database container's psql. Hard caps: 50 tables, 8s timeout, no
mutating queries possible (only SELECT row_to_json with LIMIT).
- New /api/projects/[id]/databases/[uuid]/preview — returns first 50
rows of a single table. Identifiers locked to /[A-Za-z0-9_]+/ so
splicing them into the SELECT is safe.
- DatabaseTableTree (lazy-fetch, schema-grouped, public-flat,
approximate row counts from pg_class.reltuples) and TableViewer
(sticky-header data grid, zebra rows, per-cell ellipsis at 360px).
- Fix in lib/coolify.ts: listDatabasesInProject was flattening every
db endpoint array (postgresqls, redises, mongodbs…) without
tagging the output rows with the engine. Every consumer was
seeing type=undefined which then bucketed as "unknown" and
blocked the table inspector. Now we tag at the flatten step so
every CoolifyDatabase has a stable type.
- Infrastructure tab: database tile is now expandable inline like
Codebases on Product. Auto-expands the first DB; click any table
to preview rows on the right.
Made-with: Cursor
Three sub-areas, all real, no static placeholders:
Databases — listDatabasesInProject(coolifyProjectUuid). Type is
normalised (postgresql / redis / mongodb / mysql / keydb
/ dragonfly / clickhouse) so the tile subtitle is stable
regardless of how Coolify spells the engine.
Providers — auto-detected from env-var keys across every app + service
in the project. 35+ patterns covering Auth (Clerk, Auth0,
Supabase, NextAuth, SuperTokens, WorkOS, Firebase Auth),
Email (Resend, Mailgun, Postmark, SendGrid, SES, Loops),
SMS (Twilio, Vonage), Payments (Stripe, LemonSqueezy,
Paddle), Analytics (PostHog, Mixpanel, Amplitude, Plausible,
Umami), LLM (OpenAI, Anthropic, Google AI, Mistral, Cohere,
Groq, OpenRouter), Storage (S3, R2, GCS, Supabase),
Search (Algolia, Meilisearch, Typesense), Monitoring
(Sentry, Datadog, LogSnag). Each tile drills down to show
which app/service the keys live in and which keys matched.
Secrets — env-var totals per app/service, sorted by count. Values
are never read or returned from this surface — keys only.
The detail pane explains how to read/edit (via AI chat
with services.envs.* / apps.envs.* MCP tools).
Anatomy endpoint extended in the same single-fetch shape: env vars are
loaded once, then both detectProviders() and summariseSecrets() run
against that one source so we don't double-fetch.
The static What-lives-here grid is gone — every tile shown corresponds
to something that actually exists in the project.
Made-with: Cursor
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