Communityコーディング&開発github.com

Satcomx00-x00/skills-db

エージェントスキル / ローンチ、プロダクトデモ動画に関連するプロジェクト: Satcomx00-x00/skills-db

skills-db とは?

このスキルは、最新のSaaSプロダクトを構築・運用するための包括的なガイドです。マルチテナンシー(共有スキーマ、スキーマ単位、DB単位、ハイブリッド)の選択、サブスクリプション・従量課金の実装パターン、プロダクト主導成長(PLG)の戦略、AI機能統合(ストリーミング、RAG、コスト管理、プライバシー)、フィーチャーフラグやプランによる機能ゲーティング、オンボーディングのベストプラクティス、OpenTelemetryを用いた可観測性、イベント追跡、GDPR/SOC 2準拠、そしてデータ分離とセキュリティの原則をカバーします。Stripe、PostgreSQL RLS、Prisma、OpenAI APIなどの実践的なコード例を示し、提供されたMarkdownの手順とリファレンスに従うことで、エンジニアは堅牢でスケーラブルなSaaSを迅速に構築できます。

対応~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に質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

saas

SaaSの最先端 — マルチテナンシー、従量課金、PLG、AI機能統合、最新の可観測性、コンプライアンス、セキュリティを本番SaaSプロダクトに適用します。

手順

マルチテナンシー

ユースケースに最適な分離モデルを選択してください:

モデル説明使用すべきケース
共有スキーマすべてのテナントがテーブルを共有。行は tenant_id でタグ付け初期段階、コスト重視、シンプルなコンプライアンス
テナント別スキーマテナントごとに個別のDBスキーマ中程度の分離、テナントごとの移行が容易
テナント別DBテナントごとに個別のデータベースエンタープライズ、厳格なデータ所在地要件、高コンプライアンス(SOC 2、HIPAA)
ハイブリッドコアデータは共有、機密データは隔離混合層サービスを提供する規制産業

共有スキーマテナンシーのルール:

  • すべてのクエリは 必ず tenant_id でフィルタリング — ベースリポジトリまたは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呼び出し、シート数、消費トークン、ストレージGB):
    await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
      quantity: tokensUsed,
      timestamp: Math.floor(Date.now() / 1000),
      action: 'increment',
    });
    
  • 顧客にリアルタイムの使用量ダッシュボードを公開 — 請求書発行日に驚かれると解約の原因になります
  • 課金期間ごとにソフトリミット(80%で警告)とハードリミット(100%でブロック)を実装
  • プロダクト主導成長(PLG)の入り口として、無料枠+従量課金アップセルを検討

プロダクト主導成長(PLG)

  • 無料枠 / フリーミアム — 支払いを要求する前にユーザーが価値を体験できるようにする。有料プランが明らかに価値があると分かる制限を設定
  • タイムトゥバリュー(TTV) — サインアップから最初の有意義なアクション(「ahaモーメント」)までの時間を測定し最小化する
  • アプリ内アップグレードプロンプト — プラン制限に達した瞬間にコンテキストに応じたアップグレードCTAを表示。一般的な設定ページではない
  • プロダクト認定リード(PQL) — 単なるデモリクエストではなく、プロダクトの使用状況シグナルでユーザーをスコアリングし、営業にルーティング
  • バイラル / コラボ機能 — チームメイトの招待、レポートの共有、公開ページ — 各アクションが配信チャネルになる

AI機能統合

最新のSaaSプロダクトにはAIネイティブ機能が期待されます。以下のパターンを適用:

AIアシスタントとコパイロット:

  • ストリーミングレスポンス(SSE / ReadableStream)を使用して、ユーザーがトークンを順次確認できるように — 完全な応答を待たせない
  • AIの出力は常に編集可能にする — ユーザーが修正、洗練、出力を所有できるようにする
  • 信頼を構築するために信頼度シグナルとソース引用(RAG)を表示

検索拡張生成(RAG):

  • テナントごとのベクトル埋め込みを分離された名前空間(Pinecone、pgvector、Qdrant)に保存 — テナントデータを混在させない
  • 書き込み時にコンテンツを埋め込み、読み取り時にコサイン類似度でクエリ
  • ドキュメントをインテリジェントにチャンク分割(512~1024トークン、オーバーラップあり)。ソースコンテンツが変更されたら再埋め込み

使用量とコスト管理:

  • テナントごとのLLMトークン消費を追跡 — それに応じて課金またはレート制限
  • 決定論的なプロンプト(同じ入力→同じ出力)をキャッシュしてコスト削減
  • 分類/トリアージには安価なモデルを使用。生成タスクには最先端モデルを確保

データプライバシー:

  • テナントデータがモデルトレーニングに使用されるかどうかを明示するデータ処理追補(DPA)を提供
  • オプトアウト手段を提供。エンタープライズ顧客はこれを要求する

フィーチャーフラグとプランゲーティング

  • 機能ゲートは サービス層 で行う — クライアントサイドでの非表示はUIの見栄えであり、セキュリティではない
  • エンタイトルメントロジックを単一の can(tenant, feature) ヘルパーに集約:
    function can(tenant: Tenant, feature: Feature): boolean {
      return tenant.plan.features.includes(feature);
    }
    
  • 機能の デプロイ と機能の リリース をフラグシステム(LaunchDarkly、Unleash、GrowthBook、または単純なDBテーブル)で分離
  • フラグの用途: 段階的ロールアウト(テナントの割合)、ベータプログラム、A/Bテスト、キルスイッチ

オンボーディング

  • TTVを最小化: 5分以内にユーザーを最初の有意義なアクションに導く
  • 明確な進捗インジケーターを持つチェックリスト駆動のオンボーディングフローを使用
  • 初回使用時にプロダクトが空っぽに見えないように、サンプル/デモデータを事前に入力
  • 最初に収集する情報は最小限に抑え、その他は後回しにする
  • オンボーディングメールは プロダクトイベント(時間だけでなく)に基づいてトリガー: サインアップ時に歓迎、24時間後にステップ2未完了ならナッジ、最初のマイルストーンを祝福
  • インタラクティブなプロダクトツアー(ビデオではない)を提供して、ユーザーが実際に操作しながら学べるようにする

可観測性と運用

構造化ログ — すべてのログ行は以下を含むこと:

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

OpenTelemetryによる分散トレーシング:

  • フレームワークレベルでインストルメント化(Next.js instrumentation.ts、Expressミドルウェア)
  • サービス境界を越えてトレースコンテキストを伝播(traceparent ヘッダー)
  • 任意のバックエンド(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)またはCDP(Segment、PostHog、RudderStack)に出力
  • これらのイベントを解約予測、コホート分析、PLGスコアリングに使用

GDPR / プライバシー:

  • データエクスポートエンドポイントとアカウント削除エンドポイントを提供 — GDPRで必須、エンタープライズバイヤーにも期待される
  • データ保持ポリシーを文書化し、自動パージを実装
  • マーケティングCookieと分析には同意管理を使用

SOC 2 / コンプライアンス衛生(エンタープライズ対応):

  • 状態を変更するすべてのユーザーアクションの監査ログを維持(誰が、何を、いつ、どのIPから)
  • 最小権限をデフォルトとするロールベースアクセス制御(RBAC)を実装
  • すべての管理アカウントに多要素認証(MFA)を強制。エンタープライズテナントにSSO(SAML/OIDC)を提供
  • 定期的な依存関係の脆弱性スキャンと侵入テストを実施
  • 脆弱性開示ポリシーを維持

データ分離とセキュリティ

  • すべてのAPIコールで tenant_id を 認証済みセッション に対して検証 — リクエストボディを決して信頼しない
  • テナントごとにレート制限をかけ、騒がしい隣人問題を防止
  • DB内のAPIキーはハッシュ化(bcryptまたはArgon2)。平文で保存しない
  • HTTPSを必須にし、長い max-age で Strict-Transport-Security を設定
  • セッション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);
});

参考

関連スキル