Community程式設計與開發github.com

Satcomx00-x00/skills-db

技能/專案,與產品發布或示範影片相關:Satcomx00-x00/skills-db

skills-db 是什麼?

這是一份針對生產級「軟體即服務 (SaaS)」產品的全面性技能指南,涵蓋了多租戶架構、用量計費、產品驅動成長 (PLG)、AI 功能整合、現代可觀測性、合規性與安全性等關鍵主題。技能詳細說明了如何根據不同情境選擇適當的租戶隔離模型(共享 schema、每個租戶獨立 schema、每個租戶獨立資料庫或混合模式),並提供了使用 PostgreSQL 資料列層級安全 (RLS) 進行強制隔離的具體規則與程式碼範例。 在計費與成長方面,此技能建議委託 Stripe 等第三方服務處理付款,並透過 webhook 同步訂閱狀態,同時也深入探討了用量計費、功能開關、以及透過免費試用與產品內提示來驅動 PLG 策略。針對 AI 功能整合,它提供了串流回應、檢索增強生成 (RAG)、每個租戶的 token 用量追蹤與快取的最佳實踐,並強調了資料隱私的重要性。 最後,技能涵蓋了營運面,包括結構化日誌、使用 OpenTelemetry 的分散式追蹤、基於每個租戶的監控指標與警報,以及用於支援的內部管理工具。它也整理了 GDPR、SOC 2 等合規要求的檢查清單,並提供了租戶儲存庫模式、AI 串流端點與 Stripe webhook 處理的 TypeScript 範例程式碼。

相容平台~Claude Code✓Codex CLI~Cursor
npx skills add Satcomx00-x00/skills-db

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

SaaS

SaaS 的最新技術 — 多租戶、用量計費、產品驅動成長 (PLG)、AI 功能整合、現代可觀測性、合規性與安全性,專為生產級「軟體即服務 (SaaS)」產品而設計。

指示

多租戶

為你的使用情境選擇合適的隔離模型:

模型描述使用時機
共享 schema所有租戶共用資料表,透過 tenant_id 欄位區分資料列早期階段、注重成本、法規要求單純
每個租戶獨立 schema每個租戶擁有獨立的資料庫 schema中等隔離需求、便於每個租戶的遷移
每個租戶獨立資料庫每個租戶擁有獨立的資料庫企業級、嚴格的資料駐留要求、高合規性 (SOC 2, HIPAA)
混合模式核心資料共享,敏感資料隔離受監管行業,提供混合服務層級

共享 schema 租戶的規則:

  • 每個查詢必須根據 tenant_id 進行過濾 — 應透過基礎儲存庫 (Repository) 或 ORM 中介層強制執行,而非依賴開發人員的自律性
  • 使用 PostgreSQL 資料列層級安全 (RLS) 作為第二層防禦:
    ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
    CREATE POLICY tenant_isolation ON orders
      USING (tenant_id = current_setting('app.current_tenant')::uuid);
    
  • 在每個租戶範圍的資料表上為 tenant_id 建立索引
  • 撰寫整合測試來驗證跨租戶資料隔離
  • 所有物件儲存路徑都應有範圍限制:s3://bucket/{tenantId}/uploads/{fileId}
  • 絕不暴露可預測的循序 ID — 必須使用 UUID(v7 以利排序)

訂閱與計費

固定費率訂閱

  • 將計費業務委託給供應商 (Stripe, Paddle, LemonSqueezy) — 永遠不要自行實作支付處理
  • 資料庫中只儲存 customerId 和 subscriptionId;絕不儲存信用卡資料
  • 透過 Webhook 同步狀態 — 將你的資料庫視為計費供應商最終狀態的「最終一致性唯讀副本」:
    customer.subscription.created  → 啟用方案
    customer.subscription.updated  → 升級 / 降級 / 席位變更
    customer.subscription.deleted  → 取消 + 開始寬限期
    invoice.payment_failed         → 催款流程:通知 → 限制 → 取消
    
  • 使用 webhook 事件的 id 作為冪等鍵 — 在處理前先儲存;若已處理則跳過
  • 儲存原始 webhook 負載以供稽核與重播

用量計費(現代預設方案)

  • 即時追蹤用量事件(API 呼叫、席位、消耗的 Token、儲存 GB):
    await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
      quantity: tokensUsed,
      timestamp: Math.floor(Date.now() / 1000),
      action: 'increment',
    });
    
  • 向客戶提供即時用量儀表板 — 帳單日出現意外會導致客戶流失
  • 在每個計費週期內實作「軟限制」(用量達 80% 時警告)和「硬限制」(用量達 100% 時封鎖)
  • 考慮將「免費層級 + 用量計費」作為產品驅動成長 (PLG) 的入門方案

產品驅動成長 (PLG)

  • 免費層級 / 免費增值 — 讓使用者先體驗價值,再要求付費;設定限制,讓付費方案顯得物超所值
  • 價值實現時間 (TTV) — 衡量並最小化從註冊到首次有意義操作(頓悟時刻)的時間
  • 應用程式內升級提示 — 在用戶觸及方案限制的當下,於情境中顯示升級呼籲,而非放在一般的設定頁面
  • 產品合格線索 (PQL) — 根據產品使用行為(而非僅是 Demo 請求)為用戶評分,以轉導給銷售團隊
  • 病毒式 / 協作功能 — 邀請團隊成員、分享報告、公開頁面 — 每個動作都是一個散佈管道

AI 功能整合

現代 SaaS 產品被期望具備 AI 原生能力。請應用以下模式:

AI 助手與 Copilot:

  • 使用串流回應 (SSE / ReadableStream),讓用戶即時看到 Token 出現 — 絕不讓他們等待完整回應
  • 務必讓 AI 輸出內容可編輯 — 用戶需要能夠修正、改寫並擁有最終成果
  • 顯示信心指標並引用來源 (RAG),以建立信任

檢索增強生成 (RAG):

  • 將每個租戶的向量嵌入儲存在隔離的名稱空間中 (Pinecone, pgvector, Qdrant) — 絕不混合不同租戶的資料
  • 在寫入時嵌入內容,在讀取時使用餘弦相似度進行查詢
  • 智慧地切割文件(512–1024 個 token,並有重疊);當來源內容變更時重新嵌入

用量與成本控制:

  • 追蹤每個租戶的 LLM Token 消耗 — 據此計費或限制速率
  • 快取確定性提示(相同輸入 → 相同輸出)以降低成本
  • 使用較便宜的模型進行分類/分流處理;將頂尖模型保留給生成任務

資料隱私:

  • 提供資料處理附錄 (DPA),明確說明租戶資料是否用於模型訓練
  • 提供退出(Opt-out)選項;企業客戶會要求此功能

功能開關與方案門控

  • 在服務層進行功能門控 — 客戶端隱藏僅是 UI 美化,而非安全機制
  • 將權限邏輯集中至單一的 can(tenant, feature) 輔助函式:
    function can(tenant: Tenant, feature: Feature): boolean {
      return tenant.plan.features.includes(feature);
    }
    
  • 使用旗標系統 (LaunchDarkly, Unleash, GrowthBook, 或簡單的 DB-backed 資料表),將功能的部署與發布解耦
  • 旗標可用於:漸進式推出(% 的租戶)、Beta 計劃、A/B 測試、緊急關閉開關

入門引導

  • 最小化 TTV:引導用戶在 < 5 分鐘內完成首次有意義操作
  • 使用清單驅動的引導流程,搭配清晰的進度指示
  • 預先填充範例 / 示範資料,讓產品初次使用時感覺不空虛
  • 前端僅收集最少必要資料;其餘事項延後處理
  • 根據產品事件(而非僅是時間)觸發引導電子郵件:註冊時發送歡迎信,若 24 小時後用戶未完成第二步則發送提醒,達成第一個里程碑時慶祝
  • 提供互動式產品導覽(而非影片),讓用戶邊做邊學

可觀測性與營運

結構化日誌 — 每行日誌必須包含:

{ "level": "info", "msg": "...", "tenantId": "...", "userId": "...", "requestId": "...", "timestamp": "..." }

使用 OpenTelemetry 進行分散式追蹤:

  • 在框架層級進行插樁 (Next.js instrumentation.ts, Express 中介層)
  • 在服務邊界傳遞追蹤上下文 (traceparent header)
  • 匯出到你選擇的後端 (Jaeger, Honeycomb, Datadog, Grafana Tempo)

需要追蹤的每個租戶指標:

  • 活躍用戶 (DAU/MAU)、API 呼叫量、錯誤率、p50/p95/p99 延遲、功能採用率、已消耗儲存空間

警報:

  • 針對租戶級別異常發出警報 — 突然的錯誤峰值、用量下降、計費失敗
  • 為每個服務層級定義 SLO:免費層級(盡力而為)、專業版 (99.5%)、企業版 (99.9%)

管理工具(內部用):

  • 模擬某個租戶身份以進行支援(每次模擬都需稽核)
  • 檢視稽核日誌、管理訂閱、覆蓋功能開關、觸發重新引導
  • 建立健康狀態儀表板:每個租戶的狀態、近期錯誤、用量趨勢

資料、分析與合規性

事件追蹤:

  • 將結構化產品事件 (user.signed_up, feature.used, subscription.upgraded) 發送到資料倉儲 (Snowflake, BigQuery) 或客戶資料平台 (Segment, PostHog, RudderStack)
  • 使用這些事件進行流失預測、同期群分析以及 PLG 評分

GDPR / 隱私:

  • 提供資料匯出端點和帳戶刪除端點 — 這是 GDPR 的要求,也是企業採購者的期望
  • 記錄你的資料保留政策;實作自動清除機制
  • 對行銷 Cookie 和分析工具使用同意管理

SOC 2 / 合規衛生(企業級準備):

  • 維護所有變更狀態的使用者動作的稽核日誌(誰、做了什麼、何時、從哪個 IP)
  • 實作基於角色的存取控制 (RBAC),預設採用最小權限原則
  • 對所有管理員帳戶強制使用 MFA;為企業租戶提供 SSO (SAML/OIDC)
  • 定期進行依賴項漏洞掃描與滲透測試
  • 維護一個漏洞揭露政策

資料隔離與安全

  • 在每次 API 呼叫時,根據已認證的工作階段驗證 tenant_id — 永遠不要信任請求體的內容
  • 對每個租戶進行速率限制,以防止吵鄰效應
  • 資料庫中 API 金鑰應進行雜湊處理 (bcrypt 或 Argon2);絕不儲存純文字
  • 要求使用 HTTPS;設定 Strict-Transport-Security,並使用較長的 max-age
  • 工作階段 Cookie 應使用 HttpOnly; Secure; SameSite=Lax 屬性

範例

// 具有 RLS 上下文的租戶範圍儲存庫
class TenantRepository<T> {
  constructor(
    private readonly tenantId: string,
    private readonly db: PrismaClient,
  ) {}

  protected async withTenantContext<R>(fn: () => Promise<R>): Promise<R> {
    return this.db.$transaction(async (tx) => {
      await tx.$executeRaw`SELECT set_config('app.current_tenant', ${this.tenantId}, true)`;
      return fn();
    });
  }

  async findById(id: string): Promise<T | null> {
    return this.withTenantContext(() =>
      (this.db as any).findFirst({ where: { id, tenantId: this.tenantId } })
    );
  }
}

// 具有每個租戶用量計量的 AI 串流端點
export async function POST(req: Request) {
  const session = await getSession(req);
  const { prompt } = await req.json();

  const stream = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: prompt }],
    stream: true,
  });

  let tokensUsed = 0;
  const readable = new ReadableStream({
    async start(controller) {
      for await (const chunk of stream) {
        const text = chunk.choices[0]?.delta?.content ?? '';
        controller.enqueue(new TextEncoder().encode(text));
        tokensUsed += chunk.usage?.completion_tokens ?? 0;
      }
      controller.close();
      // 串流完成後記錄用量
      await meteringService.record(session.tenantId, 'ai_tokens', tokensUsed);
    },
  });

  return new Response(readable, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}

// 具有冪等性的 Stripe Webhook
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const event = stripe.webhooks.constructEvent(
    req.body, req.headers['stripe-signature']!, process.env.STRIPE_WEBHOOK_SECRET!
  );

  const seen = await db.webhookEvent.findUnique({ where: { stripeEventId: event.id } });
  if (seen) return res.sendStatus(200);
  await db.webhookEvent.create({ data: { stripeEventId: event.id, payload: event } });

  switch (event.type) {
    case 'customer.subscription.updated':
      await subscriptionService.sync(event.data.object);
      break;
    case 'customer.subscription.deleted':
      await subscriptionService.cancel(event.data.object.metadata.tenantId);
      break;
  }

  res.sendStatus(200);
});

參考資料

相關技能