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
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