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..."
}
| Field | Type | Description |
|---|---|---|
| schemaVersion* | number | Currently |
| orgId* | string | The AppMint tenant id. Sent as the |
| projectName* | string | The dev env's slug. Also the SpinForge site name component ( |
| owner* | object |
|
| appAudience* | 'customer' | 'staff' | Decides which AppMint auth endpoints the deployed app uses. |
| framework | string | null | The framework the agent has detected. |
| domains | string[] | Custom domains attached to this dev env on AppMint. Cached; the source of truth is AppMint's |
| createdAt* | ISO-8601 string | Set at creation. Never mutated. |
| updatedAt | ISO-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, callsprojectIdentityService.ensure(...)to stamp the file with the requestedorgId,projectName, and owner.POST /api/dev-envs/:orgId/:projectName/sync-ai-instructions— back-fills the file for older workspaces, syncsAI-INSTRUCTIONS.mdand the planner instructions into.vibe/, and patchesCLAUDE.mdso 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:
- Reads the current file.
- Validates the patch (rejects forbidden field changes).
- Writes atomically (write-temp + rename).
- 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:
- The workspace folder is scoped under
workspaces/<orgId>/<projectName>/. Path traversal guards insrc/routes/index.tsreject anyprojectNamethat escapes the org folder. 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
- Actors and auth — what
appAudienceactually changes. - Frameworks — how
frameworkgets detected. - Domains — managing the
domainsarray.