Deploy (Docker + Vercel)
Standing up a Handoff registry locally with Docker, or in production on Vercel.
Both paths deploy the same handoff-app repository, unmodified: no per-client
materialization, no customized build. See Model for why. Docker is for
local dev/test against a real Postgres; Vercel is the production path.
Start Postgres
docker run -d \
--name handoff-registry \
-e POSTGRES_DB=handoff_registry \
-e POSTGRES_USER=handoff \
-e POSTGRES_PASSWORD=changeme \
-p 5433:5432 \
postgres:16-alpine
until docker exec handoff-registry pg_isready -U handoff; do sleep 1; doneStart the registry from handoff-app
cd /path/to/handoff-app
DATABASE_URL=postgresql://handoff:changeme@localhost:5433/handoff_registry \
AUTH_SECRET=$(openssl rand -hex 32) \
HANDOFF_SYNC_SECRET=dev-registry-secret \
npm run build:registry && \
cd src/app && npx next start -p 4002
# Or for hot-reload development: npm run dev (from the handoff-app root)The registry runs at http://localhost:4002. npm run build:registry applies pending
migrations as a build step (scripts/migrate-on-deploy.mjs) before next build runs. Outside
Vercel, instrumentation.ts also applies pending migrations at process startup as a fallback,
see First-admin setup for the /setup flow that follows.
Point a workspace at it
cd my-design-system
# Device OAuth: opens a browser, saves a token to .handoff/cli-auth.json
handoff-app login --url http://localhost:4002
# Or shared-secret auth via env vars
echo 'HANDOFF_CLOUD_URL=http://localhost:4002' >> .env
echo 'HANDOFF_CLOUD_TOKEN=dev-registry-secret' >> .env
handoff-app sync-status # confirm the connectionPush content
handoff-app push:allVisit http://localhost:4002/ to see the design system. See
Workspace → CLI reference for every push/
push:all flag.
Create the Vercel project
In the Vercel dashboard: Add New → Project → Import Git Repository, pointing at the
handoff-app repository, branch main. Vercel auto-detects the committed vercel.json at the
repo root:
{
"framework": "nextjs",
"installCommand": "npm install",
"buildCommand": "npm run build:registry",
"outputDirectory": "src/app/.next",
"crons": [
{ "path": "/api/handoff/ai/design-jobs/run", "schedule": "* * * * *" }
]
}The crons entry drains the AI design-image generation queue once a minute; it needs
CRON_SECRET set (see Environment variables) or the route hard-fails
with a 503 and queued images never render.
Leave Root Directory blank (repo root). Don't click Deploy yet: env vars first.
Add Postgres
Storage → Connect Store → Create New → Postgres on the new project. Vercel auto-injects
DATABASE_URL (and POSTGRES_*) into the project's env vars. An existing Postgres instance from
another project can be attached instead (Storage → Connect Store → Existing).
Set the remaining env vars
Settings → Environment Variables, all environments, see Environment variables for the full reference:
| Variable | Value |
|---|---|
AUTH_SECRET | openssl rand -hex 32 |
HANDOFF_SYNC_SECRET | openssl rand -hex 32 |
AUTH_URL | The production URL Vercel assigns this project. |
CRON_SECRET | openssl rand -hex 32. Gates the design-job cron route in vercel.json; without it the route returns 503 and queued AI images never generate. |
HANDOFF_REGISTRY_MODE is not needed. It's only read by the deprecated vercel-build
command, which the current build path doesn't run; DATABASE_URL alone puts the app in registry
mode.
Deploy
Click Deploy. vercel.json runs npm run build:registry, which compiles the CLI, bundles
the MCP Apps, applies pending database migrations (scripts/migrate-on-deploy.mjs), then runs
next build into src/app/.next/. Migrations happen at build time, not on the first
request, instrumentation.ts deliberately skips migrating when VERCEL is set, because a
serverless instance can be frozen mid-migration while still holding the advisory lock. Once the
build lands, the app redirects to /setup because no users exist yet.
Complete first-admin setup, then push
Visit the deployment URL, create the first admin account at /setup, then point a workspace at
it the same way as the Docker path above (handoff-app login --url <production URL>, or
HANDOFF_CLOUD_URL/HANDOFF_CLOUD_TOKEN), and handoff-app push:all. See
First-admin setup for the full first-boot walkthrough.
Updating an existing registry
- Ship a
handoff-appfix or feature to all clients: merge tomainand every registry pointed at it auto-redeploys on Vercel. To hold a client back, point that project's Settings → Git → Production Branch at a branch you control, the publishedv1.xtags are Handoff 1 and are not a valid pin for a 2.0 registry. - Ship a content change: no registry redeploy at all:
handoff-app push:all(or a selectivepush) from the workspace. - Move a workspace to a newer CLI:
npm update handoff-appin the workspace repo.
Not the same as the deprecated per-project deploy
If you're coming from an older handoff-app deployment that materializes and deploys the
client's own repo (prepare-runtime / vercel-build run against a workspace, path-contract
options like materialization_layout), that model still runs for existing deployments but is
deprecated, see Model → What this replaced. New
registries deploy handoff-app itself, as above; the workspace repo never deploys.