Coolify's `compose up -d` returns non-zero whenever any sidecar container
hits a `depends_on: condition: service_healthy` timeout. For slow-booting
apps like Twenty (where the worker waits ~3 min for twenty's healthcheck),
this caused apps.create to return started=false even when the primary
stack was running fine.
Now ensureServiceUp probes the host with `docker ps` after a non-zero
compose exit and returns started=true whenever any container is running,
surfacing the compose stderr in startDiag so agents can decide whether
to retry apps.containers.up later.
Made-with: Cursor
Coolify's POST /services/{uuid}/start writes the rendered compose
files but its Laravel queue worker routinely fails to actually
invoke `docker compose up -d`. Until now agents had to SSH to
recover. For an MVP that promises "tell vibn what app you want,
get a URL", that's unacceptable.
- lib/coolify-compose.ts: composeUp/composeDown/composePs over SSH
via a one-shot docker:cli container that bind-mounts the rendered
compose dir (works around vibn-logs being in docker group but not
having read access to /data/coolify/services).
- apps.create (template + composeRaw pathways) now uses
ensureServiceUp which probes whether Coolify's queue actually
spawned containers and falls back to direct docker compose up -d
if not. Result includes startMethod for visibility.
- apps.containers.up / apps.containers.ps exposed as MCP tools for
recovery scenarios and post-env-change recreations.
- Tenant safety: resolveAppOrService validates uuid against the
caller's project before touching anything on the host.
Made-with: Cursor
Adds Coolify one-click template support — 320+ vetted apps deployable
in one MCP call (Twenty, n8n, Supabase, Ghost, etc).
- apps.create gains a 4th pathway: { template: "<slug>", ... }. Auto-
rewrites the Coolify-assigned sslip URL to the workspace FQDN and
applies user envs before starting.
- apps.templates.list / apps.templates.search expose the catalog so
agents can discover slugs. Catalog is fetched from upstream GitHub
and cached in-memory for 1h.
- lib/coolify.ts: + setServiceDomains, updateService, listService-
Templates, searchServiceTemplates. Reuses existing createService.
- next.config.ts: externalize ssh2 + cpu-features from turbopack so
`next build` can complete (native .node binaries can't be ESM-bundled).
Made-with: Cursor
Coolify auto-creates env entries (with empty values) for every ${VAR}
reference it finds in the compose YAML at service-creation time. So
POST /services/{uuid}/envs returns 'already exists' for any env we
try to set after creation. The fix is to fall back to PATCH on that
specific error, making the helper a true upsert.
Made-with: Cursor
Coolify's /applications/dockercompose creates a Service (not Application)
with its own API surface. Wire it up correctly:
lib/coolify.ts
- createDockerComposeApp returns { uuid, resourceType: 'service' }
- Add startService, stopService, getService, listAllServices helpers
- Add listServiceEnvs, upsertServiceEnv, bulkUpsertServiceEnvs for
the /services/{uuid}/envs endpoint
app/api/mcp/route.ts
- toolAppsList: includes Services (compose stacks) alongside Applications
- toolAppsDeploy: falls back to /services/{uuid}/start for service UUIDs
- toolAppsCreate composeRaw path: uses upsertServiceEnv + startService
instead of Application deploy; notes that domain routing must be
configured post-startup via SERVER_URL env
Made-with: Cursor
Coolify's /applications/dockercompose endpoint rejects build_pack (it
hardcodes dockercompose), is_force_https_enabled, and
docker_compose_domains at creation time. Move those to a follow-up
PATCH call that runs immediately after creation.
Made-with: Cursor
Third-party apps (Twenty, Directus, Cal.com, Plane…) should never need
a Gitea repo. This adds two new apps.create pathways:
image: "twentyhq/twenty:1.23.0" → Coolify /applications/dockerimage
composeRaw: "services:\n..." → Coolify /applications/dockercompose
No repo is created, no git clone, no PAT embedding. Agents can fetch
the official docker-compose.yml and pass it inline, or just give an
image name. Pathway 1 (repo) is unchanged.
Also adds volume management tools so agents can self-recover from the
most common compose failure (stale DB volume blocking fresh migrations):
apps.volumes.list { uuid } → list volumes + sizes
apps.volumes.wipe { uuid, volume, confirm } → stop containers,
rm volume, done
Both volume tools go through the same vibn-logs SSH channel. The wipe
tool requires confirm == volume name to prevent accidents and verifies
the volume belongs to the target app (uuid in name).
lib/coolify.ts: createDockerImageApp + createDockerComposeApp helpers,
dockerimage added to CoolifyBuildPack union.
app/api/mcp/route.ts: resolveFqdn + applyEnvsAndDeploy extracted as
shared helpers; toolAppsCreate now dispatches on image/composeRaw/repo.
toolAppsVolumesList + toolAppsVolumesWipe added.
sq() moved to module scope (shared by exec + volumes tools).
Version bumped to 2.3.0.
Made-with: Cursor
Companion to apps.logs. SSH to the Coolify host as vibn-logs, resolve
the target container by app uuid + service, and run the caller's
command through `docker exec ... sh -lc`. No TTY, no stdin — this is
the write-path sibling of apps.logs, purpose-built for migrations,
seeds, CLI invocations, and ad-hoc debugging.
- lib/coolify-containers.ts extracts container enumeration + service
resolution into a shared helper used by both logs and exec.
- lib/coolify-exec.ts wraps docker exec with timeout (60s default,
10-min cap), output byte cap (1 MB default, 5 MB cap), optional
--user / --workdir, and structured audit logging of the command +
target (never the output).
- app/api/mcp/route.ts wires `apps.exec` into the dispatcher and
advertises it in the capabilities manifest.
- app/api/workspaces/[slug]/apps/[uuid]/exec/route.ts exposes the same
tool over REST for session-cookie callers.
Tenant safety: every entrypoint runs getApplicationInProject before
touching SSH, so an agent can only exec in apps belonging to their
workspace.
Made-with: Cursor
Two live-test bugs surfaced while deploying Twenty CRM:
1. apps.domains.set silently 422'd on compose apps
Coolify hard-rejects top-level `domains` for dockercompose build
packs — they must use `docker_compose_domains` (per-service JSON).
setApplicationDomains now detects build_pack (fetched via GET if
not passed) and dispatches correctly. Default service is `server`
(matches Twenty, Plane, Cal.com); override with `service` param.
2. apps.update silently dropped unrecognised fields
Caller got `{ok:true}` even when zero fields persisted. This
created false-positive "bug reports" (e.g. the user-reported
"fqdn returns ok but doesn't persist" — fqdn was never forwarded
at all). apps.update now returns:
- applied: fields that were forwarded to Coolify
- ignored: unknown fields (agent typos, stale field names)
- rerouted: fields that belong to a different tool
(fqdn/domains → apps.domains.set,
git_repository → apps.rewire_git)
400 when nothing applied, 200 with diagnostics otherwise.
Made-with: Cursor
The per-workspace GCS backend (bucket, service account, HMAC keys) was
already provisioned for P5.3 but wasn't reachable through MCP, so
agents using vibn_sk_* tokens couldn't actually use object storage.
Three new tools:
- storage.describe → bucket, region, endpoint, access_key_id.
No secret in response.
- storage.provision → idempotent ensureWorkspaceGcsProvisioned().
- storage.inject_env → writes STORAGE_* (or user-chosen prefix) env
vars into a Coolify app. SECRET_ACCESS_KEY is
tagged is_shown_once so Coolify masks it in
the UI, and it never leaves our backend — the
agent kicks off injection, but the HMAC secret
is read from our DB and pushed directly to
Coolify.
Apps can then hit the bucket with any S3 SDK (aws-sdk, boto3, etc.)
using force_path_style=true and the standard endpoint.
Made-with: Cursor
Coolify was failing all Gitea clones with "Permission denied (publickey)"
because the helper container's SSH hits git.vibnai.com:22 (Ubuntu host
sshd, which doesn't know Gitea keys), while Gitea's builtin SSH is on
host port 22222 (not publicly reachable).
Rather than fight the SSH topology, switch every Vibn-provisioned app
to clone over HTTPS with the workspace bot's PAT embedded in the URL.
The PAT is already stored encrypted per workspace and scoped to that
org, so this gives equivalent isolation with zero SSH dependency.
Changes:
- lib/naming.ts: add giteaHttpsUrl() + redactGiteaHttpsUrl(); mark
giteaSshUrl() as deprecated-for-deploys with a comment.
- lib/coolify.ts: extend CreatePublicAppOpts with install/build/start
commands, base_directory, dockerfile_location, docker_compose_location,
manual_webhook_secret_gitea so it's at parity with the SSH variant.
- app/api/mcp/route.ts:
- apps.create now uses createPublicApp(giteaHttpsUrl(...)) and pulls
the bot PAT via getWorkspaceBotCredentials(). No more private-
deploy-key path for new apps.
- apps.update adds git_commit_sha + docker_compose_location to the
whitelist.
- New apps.rewire_git tool: re-points an app's git_repository at the
canonical HTTPS+PAT URL. Unblocks older apps stuck on SSH URLs
and provides a path for PAT rotation without rebuilding the app.
- lib/gitea.ts: createUser() now issues an immediate PATCH to set
active: true. Gitea's admin-create endpoint creates users as inactive
by default, and inactive users fail permission checks even though
they're org members. GiteaUser gains optional `active` field.
- scripts/activate-workspace-bots.ts: idempotent backfill that flips
active=true for any existing workspace bot that was created before
this fix. Safe to re-run.
- AI_CAPABILITIES.md: document apps.rewire_git; clarify apps.create
uses HTTPS+PAT (no SSH).
Already unblocked in prod for the mark workspace:
- vibn-bot-mark activated.
- twenty-crm's git_repository PATCHed to HTTPS+PAT form; git clone
now succeeds (remaining unrelated error: docker-compose file path).
Made-with: Cursor
Coolify v4's POST/PATCH /applications/{uuid}/envs only accepts key,
value, is_preview, is_literal, is_multiline, is_shown_once. Sending
is_build_time triggers a 422 "This field is not allowed." — it's now
a derived read-only flag (is_buildtime) computed from Dockerfile ARG
usage. Breaks agents trying to upsert env vars.
Three-layer fix so this can't regress:
- lib/coolify.ts: COOLIFY_ENV_WRITE_FIELDS whitelist enforced at the
network boundary, regardless of caller shape
- app/api/workspaces/[slug]/apps/[uuid]/envs: stops forwarding the
field; returns a deprecation warning when callers send it; GET
reads both is_buildtime and is_build_time for version parity
- app/api/mcp/route.ts: same treatment in the MCP dispatcher;
AI_CAPABILITIES.md doc corrected
Also bundles (not related to the above):
- Workspace API keys are now revealable from settings. New
key_encrypted column stores AES-256-GCM(VIBN_SECRETS_KEY, token).
POST /api/workspaces/[slug]/keys/[keyId]/reveal returns plaintext
for session principals only; API-key principals cannot reveal
siblings. Legacy keys stay valid for auth but can't reveal.
- P5.3 Object storage: lib/gcp/storage.ts + lib/workspace-gcs.ts
idempotently provision a per-workspace GCS bucket, service
account, IAM binding and HMAC key. New POST /api/workspaces/
[slug]/storage/buckets endpoint. Migration script + smoke test
included. Proven end-to-end against prod master-ai-484822.
Made-with: Cursor
Adds end-to-end custom apex domain support: workspace-scoped
registration via OpenSRS (Tucows), authoritative DNS via Google
Cloud DNS, and one-call attach that wires registrar nameservers,
DNS records, and Coolify app routing in a single transactional
flow.
Schema (additive, idempotent — run /api/admin/migrate after deploy)
- vibn_workspaces.dns_provider TEXT DEFAULT 'cloud_dns'
Per-workspace DNS backend choice. Future: 'cira_dzone' for
strict CA-only residency on .ca.
- vibn_domains
One row per registered/intended apex. Tracks status
(pending|active|failed|expired), registrar order id, encrypted
registrar manage-user creds (AES-256-GCM, VIBN_SECRETS_KEY),
period, dates, dns_provider/zone_id/nameservers, and a
created_by audit field.
- vibn_domain_events
Append-only lifecycle audit (register.attempt/success/fail,
attach.success, ns.update, lock.toggle, etc).
- vibn_billing_ledger
Workspace-scoped money ledger (CAD by default) with
ref_type/ref_id back to the originating row.
OpenSRS XML client (lib/opensrs.ts)
- Mode-gated host/key (OPENSRS_MODE=test → horizon sandbox,
rejectUnauthorized:false; live → rr-n1-tor, strict TLS).
- MD5 double-hash signature.
- Pure Node https module (no undici dep).
- Verbs: lookupDomain, getDomainPrice, checkDomain, registerDomain,
updateDomainNameservers, setDomainLock, getResellerBalance.
- TLD policy: minPeriodFor() bumps .ai to 2y; CPR/legalType
plumbed through for .ca; registrations default to UNLOCKED so
immediate NS updates succeed without a lock toggle.
DNS provider abstraction (lib/dns/{provider,cloud-dns}.ts)
- DnsProvider interface (createZone/getZone/setRecords/deleteZone)
so the workspace residency knob can swap backends later.
- cloudDnsProvider implementation against Google Cloud DNS using
the existing vibn-workspace-provisioner SA (roles/dns.admin).
- Idempotent zone creation, additions+deletions diff for rrsets.
Shared GCP auth (lib/gcp-auth.ts)
- Single getGcpAccessToken() helper used by Cloud DNS today and
future GCP integrations. Prefers GOOGLE_SERVICE_ACCOUNT_KEY_B64,
falls back to ADC.
Workspace-scoped helpers (lib/domains.ts)
- listDomainsForWorkspace, getDomainForWorkspace, createDomainIntent,
markDomainRegistered, markDomainFailed, markDomainAttached,
recordDomainEvent, recordLedgerEntry.
Attach orchestrator (lib/domain-attach.ts)
Single function attachDomain() reused by REST + MCP. For one
apex it:
1. Resolves target → Coolify app uuid OR raw IP OR CNAME.
2. Ensures Cloud DNS managed zone exists.
3. Writes A / CNAME records (apex + requested subdomains).
4. Updates registrar nameservers, with auto unlock-retry-relock
fallback for TLDs that reject NS changes while locked.
5. PATCHes the Coolify application's domain list so Traefik
routes the new hostname.
6. Persists dns_provider/zone_id/nameservers and emits an
attach.success domain_event.
AttachError carries a stable .tag + http status so the caller
can map registrar/dns/coolify failures cleanly.
REST endpoints
- POST /api/workspaces/[slug]/domains/search
- GET /api/workspaces/[slug]/domains
- POST /api/workspaces/[slug]/domains
- GET /api/workspaces/[slug]/domains/[domain]
- POST /api/workspaces/[slug]/domains/[domain]/attach
All routes go through requireWorkspacePrincipal (session OR
Authorization: Bearer vibn_sk_...). Register is idempotent:
re-issuing for an existing intent re-attempts at OpenSRS without
duplicating the row or charging twice.
MCP bridge (app/api/mcp/route.ts → version 2.2.0)
Adds five tools backed by the same library code:
- domains.search (batch availability + pricing)
- domains.list (workspace-owned)
- domains.get (single + recent events)
- domains.register (idempotent OpenSRS register)
- domains.attach (full Cloud DNS + registrar + Coolify)
Sandbox smoke tests (scripts/smoke-opensrs-*.ts)
Standalone Node scripts validating each new opensrs.ts call against
horizon.opensrs.net: balance + lookup + check, TLD policy
(.ca/.ai/.io/.com), full register flow, NS update with systemdns
nameservers, and the lock/unlock toggle that backs the attach
fallback path.
Post-deploy checklist
1. POST https://vibnai.com/api/admin/migrate
-H "x-admin-secret: $ADMIN_MIGRATE_SECRET"
2. Set OPENSRS_* env vars on the vibn-frontend Coolify app
(RESELLER_USERNAME, API_KEY_LIVE, API_KEY_TEST, HOST_LIVE,
HOST_TEST, PORT, MODE). Without them, only domains.list/get
work; search/register/attach return 500.
3. GCP_PROJECT_ID is read from env or defaults to master-ai-484822.
4. Live attach end-to-end against a real apex is queued as a
follow-up — sandbox path is fully proven.
Not in this commit (deliberate)
- The 100+ unrelated in-flight files (mvp-setup wizard, justine
homepage rework, BuildLivePlanPanel, etc) — kept local to keep
blast radius minimal.
Made-with: Cursor
Coolify exposes the template slug on `service_type`; the list endpoint
returns only summaries, so the auth list handler now fetches each
service individually to classify it reliably. Users can name auth
services anything (e.g. "my-login") and they still show up as auth
providers.
Made-with: Cursor
GET /projects/{uuid}/{envName} returns databases split into
postgresqls/mysqls/mariadbs/mongodbs/redis/keydbs/dragonflies/clickhouses
sibling arrays instead of a unified `databases` list. Combine all of
them in listDatabasesInProject. Also normalize setApplicationDomains
to prepend https:// on bare hostnames (Coolify validates as URL).
Made-with: Cursor
The v4 /applications, /databases, /services list endpoints don't
return project_uuid; authoritative link is environment_id. Replace
the explicit-only tenant check (which was rejecting every resource)
with a check that:
- trusts explicit project_uuid if present
- else looks up project envs via GET /projects/{uuid} and matches
environment_id
Also switch the project list helpers to use GET /projects/{uuid}/{env}
so listing returns only the resources scoped to the workspace's
project + environments.
Made-with: Cursor
Workspace-owned deploy infra so AI agents can create and destroy
Coolify resources without ever touching the root admin token.
vibn_workspaces
+ coolify_server_uuid, coolify_destination_uuid
+ coolify_environment_name (default "production")
+ coolify_private_key_uuid, gitea_bot_ssh_key_id
ensureWorkspaceProvisioned
+ generates an ed25519 keypair per workspace
+ pushes pubkey to the Gitea bot user (read/write scoped by team)
+ registers privkey in Coolify as a reusable deploy key
New endpoints under /api/workspaces/[slug]/
apps/ POST (private-deploy-key from Gitea repo)
apps/[uuid] PATCH, DELETE?confirm=<name>
apps/[uuid]/domains GET, PATCH (policy: *.{ws}.vibnai.com only)
databases/ GET, POST (8 types incl. postgres, clickhouse, dragonfly)
databases/[uuid] GET, PATCH, DELETE?confirm=<name>
auth/ GET, POST (Pocketbase, Authentik, Keycloak, Pocket-ID, Logto, Supertokens)
auth/[uuid] DELETE?confirm=<name>
MCP (/api/mcp) gains 15 new tools that mirror the REST surface and
enforce the same workspace tenancy + delete-confirm guard.
Safety: destructive ops require ?confirm=<exact-resource-name>; volumes
are kept by default (pass delete_volumes=true to drop).
Made-with: Cursor
ensureWorkspaceProvisioned was bailing out as soon as provision_status=='ready',
even if gitea_bot_token_encrypted had been cleared (e.g. after a manual rotation).
Check every sub-resource is present before skipping.
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
Gitea's POST /users/{name}/tokens is explicitly Basic-auth only;
neither the admin token nor Sudo header is accepted. Keep the random
password we generate at createUser time and pass it straight into
createAccessTokenFor as Basic auth.
For bots that already exist from a half-failed previous provision
run, reset their password via PATCH /admin/users/{name} so we can
Basic-auth as them and mint a fresh token.
Made-with: Cursor
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.
Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
a Writers team (write perms only) on the workspace's org, and stores
its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
bot PAT + clone URL template; session or vibn_sk_ bearer auth.
Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
keys, exposing workspace.describe, gitea.credentials, projects.*,
apps.* (list/get/deploy/deployments, envs.list/upsert/delete).
Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
.cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
fallback. Minted-key modal also shows the bootstrap one-liner.
Ops prerequisites:
- Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
- Run /api/admin/migrate to add the three bot columns.
- GITEA_API_TOKEN must be a site-admin token (needed for admin/users
+ Sudo PAT mint); otherwise provision_status lands on 'partial'.
Made-with: Cursor
The settings page imported `auth` from `@/lib/firebase/config` and called
`auth.currentUser` inside an unguarded `useEffect`. Since the app runs on
PostgreSQL + NextAuth (Firebase isn't configured), `auth` was `undefined`
and the uncaught TypeError crashed React's commit, leaving the page blank
behind the Next.js dev error overlay. The WorkspaceKeysPanel never got a
chance to mount even though `/api/workspaces` was returning fine.
Swap to `useSession()` from `next-auth/react` to read display name + email
from the existing NextAuth session. Drop the dead fetch to
`/api/workspace/{slug}/settings`, which was never implemented.
Made-with: Cursor
The panel was fetching /api/workspaces/{urlSlug} where {urlSlug}
is whatever is in the `[workspace]` dynamic segment (e.g.
"mark-account"). That slug has nothing to do with vibn_workspaces.slug,
which is derived from the user's email — so the fetch 404'd, the
component showed "Loading workspace…" forever, and minting/revoking
would target a non-existent workspace.
Now:
- GET /api/workspaces lazy-creates a workspace row if the signed-in
user has none (migration path for accounts created before the
signIn hook was added).
- WorkspaceKeysPanel discovers the user's actual workspace from that
list and uses *its* slug for all subsequent calls (details, keys,
provisioning, revocation).
- Empty / error states render a proper card with a retry button
instead of a bare "Workspace not found." line.
Made-with: Cursor
Coolify build for acb63a2 failed with:
Module not found: Can't resolve '@/lib/auth/session-server'
in app/api/projects/create/route.ts, app/api/workspaces/route.ts,
and lib/auth/workspace-auth.ts.
The file existed locally but was never committed in any prior
turn, so the previous build still worked (no consumers) and the
new workspaces feature could not. Adding it now unblocks the
deploy.
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
- Add app/styles/justine/01-homepage.css: rules from 01_homepage.html scoped to [data-justine]
- Replace app/(marketing) with app/(justine): layout wraps data-justine + Plus Jakarta
- JustineHomePage/Nav/Footer: original class names (btn-ink, hero-grid, …) + inline styles from HTML
- Remove app/justine-marketing.css; move /features /pricing /privacy /terms under (justine)
Made-with: Cursor
- Replace blue/purple gradients with ink gradient text and cream/parch CTA surface
- Step badges and transformation icons use primary (ink) fills
- /features page icons unified to text-primary; Lora section titles
- Tree view status colors use semantic tokens instead of blue/green
Made-with: Cursor
- Map Justine tokens to shadcn CSS variables (--vibn-* aliases)
- Switch fonts to Inter + Lora via next/font (IBM Plex Mono for code)
- Base typography: body Inter, h1–h3 Lora; marketing hero + wordmark serif
- Project shell and global chrome use semantic colors
- Replace Outfit/Newsreader references across TSX inline styles
Made-with: Cursor
prisma db push compared DB to schema-only NextAuth models and proposed
dropping fs_*, agent_*, atlas_*, etc. on every container start.
Use CREATE TABLE IF NOT EXISTS for users/accounts/sessions/verification_tokens
plus existing app tables — same pattern as admin migrate.
Made-with: Cursor
- CreateProjectFlow now defaults to setup/fresh mode; type selector never shown
- FreshIdeaSetup simplified to just project name + Start button
(removed description field, 6-phase explanation copy, SetupHeader)
Made-with: Cursor
Two-column layout on the Atlas tab:
- Left: Atlas discovery chat (full height, flex 1)
- Right: 240px PRD section panel showing all 12 sections with
live status dots (filled = phase saved, empty = pending)
plus a progress bar showing phases complete out of 6
- Discovery banner (all 6 done) now lives inside the left column
- "Generate PRD" footer CTA appears in right panel when all done
Made-with: Cursor
- SECTIONS back to 6 tabs: Atlas → /overview, PRD, Build, Growth, Assist, Analytics
- Remove persistent CooChat left panel and drag-resize handle
- Content area is now full-width again (no 320px sidebar eating space)
- Clean up unused imports (useSearchParams, useRouter, CooChat, Lucide icons, TOOLS constant)
Made-with: Cursor