島島(DaoDao)是一個學習平台,讓使用者設定目標、追蹤每日實踐、建立學習社群。它的技術架構比大多數同規模的產品複雜:前端是 Turborepo monorepo 管三個 app,後端拆成 Node.js 和 Python 兩個服務,資料庫用了四種。這篇拆解各層的設計邏輯,以及這些選擇背後的取捨。
Monorepo 架構設計
前端用 Turborepo 管理整個 monorepo,底層是 pnpm workspaces。
daodao-f2e/
├── apps/
│ ├── website/ # Next.js,port 3000(行銷 / 落地頁)
│ ├── product/ # Next.js,port 3001(主應用)
│ └── mobile/ # Expo / React Native
└── packages/
├── shared/ # 共用型別、utils
├── ui/ # shadcn/ui 元件庫
├── i18n/ # 多語言
├── api/ # OpenAPI client(自動生成)
└── features/quiz/ # 測驗功能模組
website 和 product 分開是很常見的決策:行銷頁和應用程式的 deploy 頻率、快取策略、SEO 需求都不同,分開可以獨立優化,但共用 packages/ui 保持視覺一致。
Turborepo 的 pipeline 設定讓 build、lint、type-check 可以平行執行,只有真正有相依關係的 task 才會等待——product 的 build 不需要等 website 跑完,但都需要等 packages/ 先 build。
前端技術選型
三個 app 的技術底層:
- website / product:Next.js 15 App Router + React 19,TypeScript 5.7+
- mobile:Expo + React Native(跨 iOS / Android)
- UI:shadcn/ui + TailwindCSS,元件放在
packages/ui,所有 app 共用 - Linter / Formatter:Biome,取代 ESLint + Prettier
Biome 這個選擇值得特別說一下。它用 Rust 寫的,lint + format 合一,速度比 ESLint + Prettier 的組合快 10-20 倍,設定也簡單很多。對 monorepo 來說,加速效果更明顯——原本三個 app 分別跑 ESLint 的時間可以減少到一個 Biome pass 完成。缺點是部分 ESLint plugin 生態還沒有對應的 Biome rule,但對多數專案來說這不是問題。
Next.js 15 + React 19 的組合帶來 Server Components 和 use cache directive,可以細粒度控制哪些資料在伺服器端快取、哪些需要 client-side fetch,比 Next.js 13/14 的 fetch cache 選項更直覺。
後端架構分層
Node.js 後端(daodao-server)用 Express.js + TypeScript,分層清楚:
routes → controllers → services → Prisma ORM
↕
middleware(auth、rate limit、validation)
每一層職責分明:routes 只管路徑對應和 middleware 掛載,controllers 處理 HTTP request/response,services 放業務邏輯(不知道 HTTP 存在),services 內部直接透過 Prisma client 操作資料庫,沒有額外的 repository 抽象層——對目前的團隊規模來說,少一層間接層反而更直覺。
所有 API 回應遵循統一格式:
{
success: boolean,
data: T | null,
timestamp: string,
meta?: { page?: number, total?: number, ... }
}
這讓前端的 API client 可以統一處理錯誤,不需要每個 endpoint 各自判斷回應結構。
身份驗證走 JWT + Passport.js,支援 Google OAuth。設計上有一個細節:所有對外暴露的 ID 都使用 External UUID(而不是資料庫的自增 ID),防止攻擊者透過猜測 ID 枚舉資源——/api/posts/1、/api/posts/2 這種 URL 是資安漏洞,UUID 格式的 ID 讓猜測變得不可行。
資料驗證全面使用 Zod,schema 在 service 層定義,同時作為 TypeScript 型別來源和 runtime validation——型別推斷和驗證邏輯不需要寫兩份。
多資料庫策略
島島的資料層以 PostgreSQL + Redis 為核心,各有明確職責:
PostgreSQL(主資料庫,透過 Prisma ORM) 所有結構化資料的單一來源:使用者、目標、實踐記錄、社群關係、貼文、留言。Prisma 提供型別安全的查詢,schema migration 有版本管理,適合需要 ACID 保證的操作。
Redis(快取 + 任務佇列 + Session) 做三件事:一是 API 回應快取和 session 儲存,降低資料庫查詢壓力;二是 BullMQ 的底層 broker,處理非同步任務(通知發送、排程檢查到期實踐並自動標記完成);三是 OAuth state store,用於登入流程的防 CSRF 驗證。
這個組合的好處是概念清楚——PostgreSQL 負責所有持久化資料,Redis 負責所有暫態和非同步工作。不需要煩惱「這筆資料到底放哪個資料庫」的問題。
社交系統與通知系統
島島在 2025 年加入了完整的社交與通知功能,這是平台從「工具」轉型為「社群」的關鍵一步。
社交系統
社交功能涵蓋追蹤(follow)、雙向連結(connection)、練習夥伴申請(buddy request)、Reaction(表情回應)和留言 mention。這些功能的資料模型都在 PostgreSQL 中,透過 Prisma 管理 schema migration。
設計上有幾個值得說明的決策:
- Reaction 採用 upsert 模式:每個使用者對同一個目標只能有一個 Reaction,重複操作是更新而非新增,避免重複資料
- 留言 mention:前端送出
mentionedUserIds,後端在建立留言時同步觸發 P1(即時)通知,被 mention 的人會立即收到提醒 - 隱私控制:社交功能內建隱私機制,使用者可以控制自己的實踐紀錄和學習內容的可見範圍
通知系統
通知系統是社交功能的基礎設施,基於 BullMQ + Redis 建構:
使用者操作(按讚、留言、追蹤、mention)
│
▼
Notification Service(判斷通知類型與優先級)
│
├── In-App Worker ──▶ P1 個別通知 / P2 彙整通知
├── Email Worker(每 4 小時批次)──▶ 合併 P1 + P2 發送
└── Weekly Worker(每週排程)──▶ 週報摘要 Email
通知分為兩個優先級:
| 優先級 | 觸發情境 | In-App 處理 | Email 處理 |
|---|---|---|---|
| P1 | 被 mention、收到夥伴申請、夥伴打卡活動 | 即時建立個別通知 | 每 4 小時批次發送,不彙整 |
| P2 | 追蹤、按讚、留言、實踐進度 | 即時建立彙整通知 | 每 4 小時批次發送,同類合併 |
Email 採用批次發送而非即時發送,避免高頻互動造成信箱轟炸。另有獨立的週報排程 worker,每週發送包含當週完成的實踐項目、收到的互動統計,以及個人化 CTA 連結的摘要信。Email 模板使用 HTML 模板引擎,支援多版本(例如歡迎信有不同的 referral group 版本)。
這套社交 + 通知架構的設計原則是:即時性和資源消耗取平衡。不是每個互動都需要即時通知,P1/P2 分級讓重要通知不被淹沒,也避免頻繁發信造成使用者疲勞。
AI 後端
AI 服務獨立成 Python FastAPI 應用(daodao-ai-backend),與 Node.js 後端分開部署。這個決策很合理:Python 在 ML 生態的工具鏈遠優於 Node.js,獨立服務也讓 AI 功能可以單獨 scale。
架構:
- LLM 整合:推薦引擎,根據使用者的學習歷程和目標,推薦相關學習資源或社群成員
- Qdrant:向量資料庫,儲存學習內容的 embedding,支援語意搜尋——不是關鍵字搜尋「TypeScript 教學」,而是找到「跟你的學習目標語意相近的內容」
- ClickHouse:分析資料庫,記錄使用行為事件(頁面瀏覽、互動、學習進度),供推薦引擎的特徵工程使用
- Redis:LLM 回應和搜尋結果快取,避免重複推論相同的 query
- Celery:Redis-based 任務佇列,處理 AI 回饋生成等耗時的非同步任務
Node.js 後端透過 HTTP 呼叫 FastAPI 服務,兩邊各自維護自己的資料來源。
CI/CD 亮點
部署用 Docker + PM2 + GitHub Actions,有一個值得特別提的設計:TypeScript 類型感知的 Docker 層快取策略。
一般的 Dockerfile 把 npm install 和 tsc build 放在不同 layer,只有 package.json 改變才會重跑 install。但島島的 CI 還額外監控 TypeScript 型別變更:當 packages/shared 或 packages/api 的型別定義有異動時,自動觸發相關 app 的 Docker layer 重建,確保型別變更不會被舊的 build cache 遮蓋。
部署完成後透過 Webhook 送 Discord 通知,包含哪個服務部署了什麼版本、build 時間、測試結果。對小團隊來說,Discord 通知比 Slack 設置成本低,也足夠用。
整體架構
瀏覽器 / Mobile App
│
├── website (Next.js :3000)
└── product (Next.js :3001)
│
▼
daodao-server (Node.js / Express)
│ │
▼ ▼
PostgreSQL Redis
(Prisma) │ │ │
├─ 使用者 BullMQ Cache Session
├─ 社交關係 │
├─ 實踐記錄 ├── 通知 Worker(P1/P2 + Email 批次 + 週報)
└─ 通知記錄 └── 排程任務(實踐自動完成)
daodao-server ──HTTP──▶ daodao-ai-backend
(Python FastAPI + Celery)
│ │ │
▼ ▼ ▼
Qdrant ClickHouse Redis
(語意搜尋)(分析) (快取 + Celery broker)
GitHub Actions 管 CI/CD,Discord 收通知。前端 monorepo 透過 Turborepo pipeline 管 build 相依順序。
整體來說
島島這套架構的核心取捨是:用較高的技術複雜度換取每一層的最佳化空間。兩個後端服務(Node.js + Python)、多個資料庫(PostgreSQL、Redis、Qdrant、ClickHouse)、三個前端 app——對小團隊來說,這是有代價的選擇。
合理的前提是:
- 團隊對這些技術都有足夠熟悉度,維護成本可控
- 各資料庫的職責分界清楚,不會出現「這筆資料到底放哪」的混亂
- AI 功能是核心差異點,值得獨立投資
如果是從零開始的早期 MVP,這套架構可能過重——PostgreSQL 單一資料庫加上簡單的 Node.js API 通常可以撐到相當規模。但對一個已經明確需要語意搜尋、行為分析和多平台支援的學習平台,這個架構選擇是合理的。
Turborepo + Biome 的開發體驗確實好——lint 快、型別共用方便、多 app 的 build pipeline 清楚。這個部分是值得借鑑的,無論後端架構怎麼選。
參考資料
- Turborepo 官方文件
- Biome 官方網站
- Next.js 15 官方文件
- Prisma ORM 官方文件
- BullMQ 官方文件
- Zod 官方文件
- FastAPI 官方文件
- Qdrant 向量資料庫
- shadcn/ui
- Expo 官方文件
- NobodyClimb 技術架構 — 另一個使用 monorepo 架構的專案對照