Documentation

Workspace identity

The .vibe/project.json file that pins every workspace's tenant, project, owner, and audience.

Every Vibe Studio workspace has a permanent, machine-readable identity file at .vibe/project.json. It tells the agent who owns the workspace, what tenant it lives in, and which auth endpoints to use. It's deliberately the first thing the agent reads on every turn — without it, the AI would have to guess orgId from the brand, hostname, or product copy, which is the most reliable way to break a deploy.

The schema

{
  "schemaVersion": 1,
  "orgId":       "localhost",
  "projectName": "appbuild",
  "owner":       { "email": "[email protected]", "type": "builder" },
  "appAudience": "customer",
  "framework":   null,
  "domains":     ["demoone.appmint.app"],
  "createdAt":   "2026-04-15T...",
  "updatedAt":   "2026-04-15T..."
}
FieldTypeDescription
schemaVersion*number

Currently 1. Older workspaces may lack this field; the back-fill route stamps it.

orgId*string

The AppMint tenant id. Sent as the orgid HTTP header on every AppEngine call. Fixed at workspace creation. Never derived from the app's brand, product copy, or hostname — it's a machine id.

projectName*string

The dev env's slug. Also the SpinForge site name component (<orgId>-<projectName>.spinforge.dev). Fixed at creation.

owner*object

{ email, type } for the Vibe Studio builder. email is the builder's AppMint user email; type is always "builder" (other actor types are not the workspace owner).

appAudience*'customer' | 'staff'

Decides which AppMint auth endpoints the deployed app uses. customer (default) → /profile/customer/signin. staff/profile/user/signin. Almost every app is customer. Only set staff if you're explicitly building an internal admin tool.

frameworkstring | null

The framework the agent has detected. null until detected. Common values: next, vite-react, vanilla-html, astro. The agent updates this when it scaffolds or recognises a framework. See Frameworks.

domainsstring[]

Custom domains attached to this dev env on AppMint. Cached; the source of truth is AppMint's dev-env record. Updated when a domain is attached or detached.

createdAt*ISO-8601 string

Set at creation. Never mutated.

updatedAtISO-8601 string

Bumped on every write to the file.

Where it's written

session-manager/src/services/project-identity.service.ts owns the file. Two routes touch it:

  • POST /api/dev-envs — creates the workspace, calls projectIdentityService.ensure(...) to stamp the file with the requested orgId, projectName, and owner.
  • POST /api/dev-envs/:orgId/:projectName/sync-ai-instructions — back-fills the file for older workspaces, syncs AI-INSTRUCTIONS.md and the planner instructions into .vibe/, and patches CLAUDE.md so the agent reads them.

Direct file mutation is allowed for the framework and domains fields. Mutating orgId, projectName, createdAt, or owner from inside the workspace is forbidden — those are immutable once stamped. The back-fill route enforces this.

Why an env-var copy too

The same values are also imprinted into the agent's environment as APPMINT_ORG_ID, APPMINT_PROJECT_NAME, APPMINT_USER_EMAIL, and APPMINT_TOKEN (the builder's JWT — see actors and auth). The agent can read either source — the file or the env vars — but never guesses. The duplication is intentional: env vars are authoritative when the agent runs in a constrained subprocess; the file is authoritative when a tool inspects the workspace from outside.

Mutation rules

The agent's instructions (AI-INSTRUCTIONS.md § 1) make this explicit. To recap:

  • Allowed to update: framework, domains, updatedAt.
  • Forbidden to update: orgId, projectName, owner, createdAt, schemaVersion.

Tools that need to mutate the allowed fields go through projectIdentityService.update(...) rather than rewriting the file directly. The service:

  1. Reads the current file.
  2. Validates the patch (rejects forbidden field changes).
  3. Writes atomically (write-temp + rename).
  4. Bumps updatedAt.

Reading the file from your own code

If you're building a tool that runs in a workspace and needs the identity:

import fs from 'node:fs/promises';
import path from 'node:path';

const file = path.join(process.cwd(), '.vibe', 'project.json');
const identity = JSON.parse(await fs.readFile(file, 'utf-8'));
console.log(identity.orgId, identity.projectName);

Or pull from environment, which is faster and avoids the read on every turn:

const orgId = process.env.APPMINT_ORG_ID;
const projectName = process.env.APPMINT_PROJECT_NAME;

What happens if it's missing

For workspaces that pre-date the identity model, hitting any agent turn triggers a back-fill via projectIdentityService.ensure(...). The service infers orgId and projectName from the workspace path (workspaces/<orgId>/<projectName>/) and stamps the file with owner: null. The owner is filled in next time the studio opens the workspace with the builder's session.

Security implications

Because every AppMint call uses orgId as the tenancy boundary, getting the wrong orgId into the file is a tenancy leak. session-manager enforces this in two ways:

  1. The workspace folder is scoped under workspaces/<orgId>/<projectName>/. Path traversal guards in src/routes/index.ts reject any projectName that escapes the org folder.
  2. projectIdentityService.ensure(...) writes the orgId from the route param, not from the request body for fields that already exist on disk.

Live URL

https://<orgId>-<projectName>.spinforge.dev is the canonical preview URL. It's only meaningful after a successful deploy — before that the subdomain doesn't exist.

Related