Solo founder 的 monorepo — 4 個 app + 5 個 package 的設計取捨
為什麼一個人的公司需要 monorepo
Solo founder 通常被建議走相反方向:一個產品、一個 repo、一個 deploy target。少維護、少複雜度、少踩坑。
但 Moriwise 從第一天就是 monorepo。理由不是「我以後會擴大團隊」這種憧憬,而是當下的實際痛點:
- 4 個產品共用同一套 LINE OA / ECPay / Resend 整合
- 同一個 brand identity 想跨產品保持一致
- 一個人沒辦法在 4 個 repo 之間切上下文還記得每個的版本
換句話說,monorepo 不是規模問題,是上下文成本問題。
現況快照
Moriwise/├── apps/│ ├── amibon/ ← SaaS 任務派遣(LIVE,2167 tests)│ ├── web/ ← 公司官網 + 文章系統│ ├── amibon_crm_ins/ ← 保險業 CRM(LIVE,B2B sales-ready)│ └── discord-bot/ ← 內部 DevOps platform└── packages/ ├── ui/ ← shadcn-based 共用元件 ├── article-kit/ ← 文章排版系統(Phase 2 新增) ├── notion-cms/ ← Notion API client ├── line/ ← LINE Messaging 包裝 ├── ecpay/ ← ECPay 金流 └── email/ ← Resend client4 個 app 走 npm workspaces,部署到 3 個 Vercel project + 1 個 pm2 (WSL2)。
第一個取捨:npm workspaces vs pnpm vs Turborepo
很多 monorepo 教學會推 pnpm + Turborepo + Nx 的全家桶。我們刻意不用。
選擇 npm workspaces 的原因,是工具的可解釋性比效能重要:
- 一個人 debug 時,不想跟 pnpm 的 symlink 結構打架
- 不想跟 Turborepo 的 cache invalidation 戰鬥
- Vercel 對 npm workspaces 支援相對直白
- Phase 2 才會評估切 pnpm(地基都還沒蓋穩)
第二個取捨:app 分還是合?
apps/amibon 跟 apps/amibon_crm_ins 都是租戶系統、都用 LINE 推播、都接 ECPay。要不要合成一個 app?
我們故意分開。三個原因:
- 業務週期不同步。Amibon 在 closed beta,CRM 已經是 B2B sales-ready。同個 deploy 滾動會互相牽扯
- 資安隔離。保險業客戶的 PII 不能跟一般 SaaS 用戶共用 session middleware
- 故障爆炸範圍。app A 推壞,不該讓 app B 也下線
但不分開的部分是 packages/。LINE 怎麼推、ECPay 怎麼簽,那邊的邏輯只能有一份。
第三個取捨:packages 怎麼切
最早的 packages/line 包了所有 LINE 操作 — flex message builder、reply、push、webhook 驗證。看起來合理,直到 Vercel build fail。
問題出在 --workspaces=false:Vercel install consumer app 的時候,不會去裝 packages/line 自己的 deps。如果 packages/line import 了 @line/bot-sdk,build 階段就找不到型別。
修法是讓每個 package 對「自己的 node_modules 是空的」這件事 robust:
- 第三方型別重新匯出時,type-only import + re-export
- 不要 import 其他 workspace package(會打開遞迴依賴)
- 視覺元件 inline 化,不依賴 packages/ui
對比:理想化的設計 vs 我們實際的取捨
對照組是「教科書 monorepo」。我們的實際選擇刻意不漂亮:
教科書 monorepo
看起來很對,實際上養不起
- pnpm + Turborepo + Nx 全家桶
- 每個 package 都有自己的 README + CHANGELOG
- 所有 app 共用同一個 design system 一致到像素
- CI matrix 跑全 package 全 app build
- 嚴格 semver,內部 package 也照升
我們的取捨
刻意省事,留 debug 預算給真問題
- 純 npm workspaces,Vercel 直白支援
- package README 有就好,CHANGELOG 等真有 consumer 再寫
- Amibon 跟 Moriwise 的 design system 故意分開
- Vercel ignoreCommand 只 build 改動到的 app
- 內部 package 用 file: link,不走 semver
Vercel 的 ignoreCommand 也是同個概念
每個 app 有自己的 vercel.json,宣告它依賴哪些 packages:
git fetch --depth=500 origin $VERCEL_GIT_PREVIOUS_SHA 2>/dev/null || exit 1git diff --quiet $VERCEL_GIT_PREVIOUS_SHA HEAD -- \ apps/<name>/ <relevant packages> || exit 1只有 watch path 內的檔案有變才會觸發 build。
效果:docs-only commit 不會觸發 4 個 project 重 build;只改 amibon 不會浪費 web build minute。對 Vercel Pro plan(usage-based billing 2026-04-24+ 上線)這條省下的不是時間,是錢。
一個人的 monorepo 該記住的事
最好的架構是一個人能維護的架構,不是教科書最漂亮的架構。
把這句話打在牆上。每次想引入新工具、切新 package、立新 boundary 之前都默念一次。
接下來
Phase 3 packages/ 第二批整理計畫
- 把 amibon 跟 CRM 共用的 tenant routing 抽成 packages/tenant-shell
- 原因不是 DRY — 是兩邊現在的 middleware 已經 drift 出 bug
- 決策準則:這份重複是不是 bug 的溫床?是 → 抽。不是 → 留著。
每個抽 package 的決策都該回到同一個問題:這份重複是不是 bug 的溫床?是 → 抽。不是 → 留著重複,沒事。
