April 22, 2026

From Two Repos to One: Migrating bfpinvest.com to a Monorepo

How we restructured two separate GitHub repositories into a single monorepo, reconfigured three Vercel projects, and set up path-scoped build triggers.

We recently consolidated two GitHub repositories — lockezhou18/bfp (the Next.js marketing site) and lockezhou18/bfp-invest (company ops and tooling) — into a single monorepo. Notes on what we did and what we ran into.

Why a Monorepo?

The split made sense early on. The website was a standalone Next.js project; the ops tooling (Prometheus exporter, Vercel DNS manager, credential aggregator) lived separately. But as the project grew, the friction mounted:

The tipping point was adding a backend: we wanted Supabase, API routes, and eventually authenticated tooling. That needed a shared workspace, not two separate codebases.

The Structure

The new layout:

bfp-invest/
├── web/          # Next.js 15 — www.bfpinvest.com
├── blog/         # Astro — blog.bfpinvest.com (this site)
├── services/     # Future Python workers
├── tools/        # bfp_auth.py, vercel_dns.py, exporter.py
├── ops/          # company-profile.md, DNS runbooks
└── tests/e2e/    # Future Playwright tests

web/ and blog/ are fully independent deployables — different frameworks, different package.json files, different Vercel projects. The monorepo doesn’t force them to share dependencies; it just gives them a shared git history and a single place to land PRs.

Pulling in the Old Repo

We used git subtree to bring lockezhou18/bfp history into bfp-invest/web/ without losing commits:

# In bfp-invest/
git remote add bfp https://github.com/lockezhou18/bfp.git
git fetch bfp
git merge -s ours --no-commit --allow-unrelated-histories bfp/main
git read-tree --prefix=web/ -u bfp/main
git commit -m "refactor: pull lockezhou18/bfp into web/ subtree"

This replays the full history from the old repo into the web/ subdirectory. The old repo now lives at lockezhou18/bfp-archive.

Reconfiguring Vercel

The tricky part. Three projects (bfp, bfp-mvp, bfp-work) all pointed at lockezhou18/bfp. We needed to:

  1. Reconnect each project to lockezhou18/bfp-invest
  2. Set rootDirectory: web so Vercel builds from the right subdirectory
  3. Add an ignored build step so pushes to tools/ or ops/ don’t trigger a deploy

The Vercel API doesn’t let you update link via PATCH /v9/projects/{id} — that returns a bad_request: should NOT have additional property link error. You have to disconnect first:

# Disconnect from old repo
DELETE /v9/projects/{id}/link

# Reconnect to new repo
POST /v9/projects/{id}/link
{
  "type": "github",
  "repo": "lockezhou18/bfp-invest",
  "repoId": 1217681571
}

Then set rootDirectory and commandForIgnoringBuildStep via a separate PATCH:

PATCH /v9/projects/{id}
{
  "rootDirectory": "web",
  "commandForIgnoringBuildStep": "git diff HEAD^ HEAD --quiet -- web/"
}

The counterintuitive part: git diff --quiet exits 0 when there are no changes — which Vercel treats as “skip the build.” So a push that only modifies tools/ will make git diff HEAD^ HEAD --quiet -- web/ exit 0 (no web/ changes) and Vercel skips. A push that touches web/ exits 1 (changes exist) and Vercel builds. This is exactly what you want, but the logic feels backwards at first.

We also took the opportunity to clean up: bfp-mvp (a stale project with messy deploy history from the old repo) was deleted and recreated as bfp-staging. bfp-work was deleted entirely.

What Vercel Shows as “CANCELED”

One thing that confused us: deploys that were skipped by the ignored build step show up as CANCELED in the Vercel API — not SKIPPED. This is expected behavior. Don’t panic when you see it.

Result

The monorepo isn’t magic — it’s just a better container for work that belongs together.