Monorepo Deployment
How each app in the Turborepo monorepo maps to its own Vercel project, and how to add another one.
Monorepo Deployment
Every deployable app lives in one GitHub repository and deploys to its own Vercel project. Vercel supports this natively: each project points at a different Root Directory inside the same repo, has its own environment variables, its own domain, and its own build. A commit that only touches one app rebuilds only that app.
What is in the repo
| Workspace | Path | Port (dev) | Deploys to Vercel |
|---|---|---|---|
@jose-madrid/storefront | apps/storefront | 3000 | Yes — production site |
@jose-madrid/docs | apps/docs | 3002 | Yes — this documentation site |
@jose-madrid/fundraising | apps/fundraising | 3001 | Not yet linked |
@jose-madrid/admin | apps/admin | 3003 | Not yet linked |
@jose-madrid/agent | apps/agent | — | No (local tooling; excluded from npm run build) |
@jose-madrid/shared-types | packages/shared-types | — | No (library) |
@jose-madrid/shared-utils | packages/shared-utils | — | No (library) |
apps/macos-admin is a Swift package, not an npm workspace, and is built with
build-app.sh rather than deployed. See macOS Admin App.
There is no apps/backend. API routes live in apps/storefront/app/api and deploy
with the storefront. Older notes describing a separate backend deployment boundary
are out of date.
Storefront
Vercel project: josemadridsalsa · Root Directory: apps/storefront ·
Production: https://www.josemadrid.net
Configured by apps/storefront/vercel.json:
- Build command touches the various
.env*files (so Next does not fail on a missing file), generates the game-icon manifest, then runsnpm run vercel-build. vercel-buildrunsprisma migrate deploy,prisma generate, the permission seed, and finallynext build.git.deploymentEnabledallowsmainonly — pushes to any other branch do not create a deployment.cronsdeclares the ten scheduled jobs below.
Both the migrate step and the permission seed are wrapped in || echo 'WARN: ...'.
A failed migration therefore produces a green build with a warning buried in the
log. After deploying a schema change, confirm the migration actually applied rather
than trusting the build status — see Database (Production).
Scheduled jobs
Crons run on Vercel (the account is on the Pro plan, so sub-daily schedules are
allowed) and hit routes under apps/storefront/app/api/cron, each guarded by
CRON_SECRET.
| Path | Schedule |
|---|---|
/api/cron/email-automation | every 5 minutes |
/api/cron/email-campaigns | every 5 minutes |
/api/cron/social-publish | every 5 minutes |
/api/cron/abandoned-cart | hourly |
/api/cron/quickbooks-sync | hourly at :15 |
/api/cron/operations-sweep | every 2 hours |
/api/cron/expire-pending-orders | every 2 hours at :20 |
/api/cron/processor-fees | every 2 hours at :30 |
/api/cron/review-requests | daily at 10:00 UTC |
/api/cron/fundraiser-lifecycle | daily at 13:00 UTC |
Documentation site
Root Directory: apps/docs · Framework preset: Next.js
The docs site used to be a standalone repository (salsadocs) deployed from its own
Git remote. It now lives in this monorepo so a feature change and the page describing
it ship together — but it still deploys as a separate Vercel project, so it keeps
its own domain and its own deployment history.
apps/docs/vercel.json keeps that separation safe:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": {
"main": true,
"*": false
}
},
"ignoreCommand": "npx --yes turbo-ignore @jose-madrid/docs"
}deploymentEnabledmatches the storefront:mainonly, no preview deployments from feature branches.ignoreCommandrunsturbo-ignore, which compares the commit against the previous successful docs deployment and exits0— skipping the build — when nothing@jose-madrid/docsdepends on has changed. Storefront-only commits therefore cost no docs build, and vice versa.
Because apps/docs declares no workspace dependencies, Vercel's default install and
build (npm install at the repo root, next build in apps/docs) are correct; no
custom build command is needed.
Adding another app as a Vercel project
- Link the directory.
cd apps/<app> npx vercel link # choose "create new project" - Set Root Directory to
apps/<app>in Project Settings → General. Leave "Include files outside the root directory" enabled so the monorepo lockfile andpackages/*are available. - Add a
vercel.jsonin the app directory with themain-only git guard and aturbo-ignoreignore command, as above. Without the git guard every push to any branch creates a preview deployment for that project. - Set environment variables in Project Settings → Environment Variables. They are
per-project; nothing is inherited from the storefront project.
vercel env pullfrom the storefront project is a convenient starting point, but re-point every URL and use a distinctNEXTAUTH_SECRET. - If the app depends on a workspace package, set the build command to
cd ../.. && npx turbo run build --filter=@jose-madrid/<app>so Turborepo builds the dependency first. - Add the domain in Project Settings → Domains and create the DNS record Vercel shows you.
Root package.json carries the overrides block that pins transitive dependencies.
npm only honours overrides from the root of a workspace, so installs must run from
the repo root — which is what Vercel does when "include files outside the root
directory" is on. Do not add a per-app lockfile.
Continuous integration
.github/workflows/ci.yml runs one pipeline for the whole repo on push and pull
request: lint → type-check → tests → Prisma generate → build → shipping E2E →
Playwright. Coverage and Playwright results are reported but do not gate the merge;
lint, type-check, tests and build do.
Three Node versions are in play and they do not agree: .nvmrc pins 20, CI runs
22, and on Vercel the storefront project is set to 22 while the docs project
is set to 20. Nothing is broken by this today, but check a project's Settings →
General → Node.js Version before blaming a build failure on code — a version
mismatch between local, CI and the deployed project is the cheapest explanation and
the easiest to miss.
Rollback
Vercel keeps every deployment. To roll back, open the project → Deployments → pick the last good one → Promote to Production. This takes seconds and does not rebuild. It rolls back code only — a database migration that already ran stays applied, so a schema change needs a forward fix or a restore rather than a promotion.
How is this guide?
Last updated on