Deploying from Vibe Studio packages the workspace, hands it to session-manager, which forwards it to SpinForge with the partner key. The result is a live site at https://<orgId>-<projectName>.spinforge.dev (plus any custom domains attached to the dev env).
How it works under the hood
Vibe Studio ── POST /api/deploy/spinforge/<orgId>/<projectName> ──▶ session-manager
│
spawns deploy job (background)
│
▼
┌──────────────────────┐
│ init → build → │
│ create-site → │
│ package → upload → │
│ done │
└──────────────────────┘
│
▼
SpinForge partner API
with sfpk_… key
│
▼
Live site at
<orgId>-<projectName>.spinforge.dev
session-manager owns the partner key (sfpk_…) — Vibe Studio never sees it. The browser's only role is to start the job and stream progress.
Trigger a deploy
Two paths.
From the studio
Click Launch → Deploy to SpinForge. The studio:
- Reads attached custom domains from
GET /dev-env/<projectName>on AppMint. - POSTs
/api/deploy/spinforge/<orgId>/<projectName>on session-manager with the builder's JWT and the alias list. - Subscribes to
/api/deploy/job/<jobId>/stream(SSE). - Renders each step's message as it arrives.
This is the path you'll use 99% of the time.
From a script or the agent
# Pull attached custom domains
ALIASES=$(curl -s "$APPMINT_API_URL/dev-env/$APPMINT_PROJECT_NAME" \
-H "Authorization: Bearer $APPMINT_TOKEN" \
-H "orgid: $APPMINT_ORG_ID" \
| jq -c '.data.domains // []')
# Start the deploy
curl -X POST "$SESSION_MANAGER_URL/api/deploy/spinforge/$APPMINT_ORG_ID/$APPMINT_PROJECT_NAME" \
-H "Authorization: Bearer $APPMINT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"customerId\":\"$APPMINT_USER_EMAIL\",\"aliases\":$ALIASES}"
Response:
{ "success": true, "jobId": "job_...", "job": { "status": "running", ... } }
Then poll or stream:
# Poll
curl "$SESSION_MANAGER_URL/api/deploy/job/<jobId>"
# Stream (SSE)
curl -N "$SESSION_MANAGER_URL/api/deploy/job/<jobId>/stream"
The deploy steps
session-manager runs the job through six steps. Each one emits an event the studio shows:
| Step | What happens |
|---|---|
init | Validate the workspace. Read .vibe/project.json, verify orgId / projectName / owner. |
build | If framework requires a build, run npm install && npm run build (or framework-specific equivalent). Skip for vanilla-html. |
create-site | Call SpinForge's partner auth (/_partners/auth) with the partner key + builder's JWT. Mint a sfc_… per-customer session. Create or update the SpinForge site record. |
package | Zip the workspace's build output. Skip node_modules/, .git/ (unless includeGit), and other ignore-listed paths. |
upload | Stream the zip to SpinForge. SpinForge extracts, configures DNS, issues TLS, serves the site. |
done | Final event. Includes the live URL and any custom domain aliases. |
If a step fails, the job emits an error event with the server's actual message. Don't paper over errors — the agent's instructions explicitly say to surface server errors verbatim, not invent recoveries.
Live URL format
https://<orgId>-<projectName>.spinforge.dev
Two things to know:
- The subdomain is fixed. You can't pick a different one —
orgIdandprojectNameare immutable per workspace. - Subdomain DNS takes ~30 seconds for fresh sites. Hitting the URL immediately after
donemay 404 briefly.
Environment variables
For deployed apps that need server-side env vars (Next.js, etc.), set them at the SpinForge level. The agent or builder configures via SpinForge's partner API (proxied through session-manager). Don't bake secrets into the build output.
For build-time vars (VITE_*, NEXT_PUBLIC_*), include them in .env.production in the workspace before deploy. They're zipped into the package and the build step picks them up.
Production env files in the workspace get zipped and uploaded. If you're using git, .gitignore doesn't help — the zip step ignores .git/ but not other dotfiles by default. Use SpinForge's runtime env config for anything sensitive.
Re-deploys and rollback
Every deploy is a fresh package. To re-deploy after edits, just hit Launch again. The previous deploy's job record is kept (see GET /api/deploy/jobs/<orgId>/<projectName>).
For rollback, check out the previous git commit in the workspace and re-deploy. There's no SpinForge-side "previous version" concept — the live site is always whatever the last deploy uploaded.
Sync custom domains without a deploy
If you attached a new custom domain in AppMint and don't want to redeploy:
curl -X POST "$SESSION_MANAGER_URL/api/deploy/spinforge/$APPMINT_ORG_ID/$APPMINT_PROJECT_NAME/sync-aliases" \
-H "Authorization: Bearer $APPMINT_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"customerId\":\"$APPMINT_USER_EMAIL\",\"aliases\":$ALIASES}"
This re-runs SpinForge's /_partners/auth with the desired alias list and updates the site's domain bindings without rebuilding or re-uploading.
Deleting a deploy
curl -X DELETE "$SESSION_MANAGER_URL/api/deploy/spinforge/$APPMINT_ORG_ID/$APPMINT_PROJECT_NAME?customerId=$APPMINT_USER_EMAIL" \
-H "Authorization: Bearer $APPMINT_TOKEN"
This removes the SpinForge site record. The workspace on disk is untouched.
Things that don't exist
The agent's instructions are emphatic about this — none of these are real:
spinforgeCLI binary. There's nospinforge deploy,spinforge login,spinforge whoami.- A SpinForge dashboard upload flow for end users.
- A filesystem drop path like
~/.spinforge/deployments/. deploy.yaml/spinforge.tomlconfig files.- Browser auth flow for SpinForge users.
If a tutorial or AI suggestion mentions any of these, it's wrong. The deploy is API-driven, full stop.
Related
- SpinForge — the deploy target itself.
- Domains — attach a custom domain.
- session-manager — the service that orchestrates deploys.