Cloudflare PagesのDBはD1でいいのか?Supabaseと比較して分かった構造的な答えのサムネイル

はじめに

Cloudflare Pagesで本番サービスを作り始めて、こんな壁に当たりませんでしたか?

【パターンA】: Cloudflare WorkersとSupabaseを組み合わせようとドキュメントを読み進めたら、接続プーリングにHyperdriveが必要と分かった。設定を試みたら月額$5以上のコストが発生することに気づき、個人開発の低コスト運用という前提が崩れた。

【パターンB】: Workers + Supabaseで実装を進めたが、WorkersはリクエストごとにIPアドレスが変わるため、Supabase側でIP制限をかけられないと判明した。接続情報が推測可能な状況でIP制限が使えないと、攻撃時に対処手段がない。

どちらも「Cloudflareの速さを使いたい」という動機は同じなのに、外部DBとの組み合わせが予想外のコストと制約を生んだパターンです。

この記事をお勧めしない人

  • すでに外部DBサービスを使っており、Cloudflare以外の環境への移行も視野に入れている人。
  • Cloudflareではなく、VercelやFlyなど他のホスティング上で開発している人。
  • ORMやBaaSによる高い抽象度を優先する人。

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

外部DBに依存し続けると、問題は複合する

  • HyperdriveでIPレンジを固定しようとすると月額コストが発生し、無料枠で運用できなくなる。
  • Hyperdriveなしで外部DBを使うとIP制限がかけられず、セキュリティの穴を塞ぐ手段がなくなる。
  • 依存先が増えるほどデプロイ・ローカル開発の構成が複雑になり、AIによるコード生成の精度も落ちる。

D1はこれらの問題を構造的に回避します

  • D1はWorkers Bindingで直接アクセスするためHTTP呼び出しが不要。Hyperdriveも不要で追加コストゼロです。
  • D1はCloudflareインフラ内で完結するため、外部DBのIP制限問題そのものが存在しません。
  • SQLiteのSQLをそのまま使えるため、ORMへの依存を最小化しながら型安全なクエリが書けます。

このブログもそうです

ClaudeMixではユーザー認証・サブスクリプション状態など、リレーショナルな構造を持つデータはすべてD1に置いています。
この記事では、D1の選定根拠・Binding設定・型定義・クエリ例を持ち帰れる形でまとめました。
セッション管理(KV)やメール認証(Resend)との組み合わせに興味がある方は、それぞれの記事も合わせてご覧ください。

延命医の診断

Cloudflare D1はSQLite互換のサーバーレスデータベースです。延命医の視点で、以下の3点が5〜10年の生存根拠になります。

SQLiteの生存実績: SQLiteは2000年から開発され、現在も世界で最も広く使われているDBエンジンの一つです。1974年生まれのSQLをベースにした普遍的なインターフェースは、ORMや独自クエリAPIとは異なり、プラットフォームが変わっても継続して使えます。

CloudflareにおけるD1の位置付け: CloudflareはWorkers・D1・KV・R2を同一プラットフォームで提供し、急速にエコシステムを拡大しています。D1はその中でリレーショナルDBの唯一の選択肢として位置づけられており、Cloudflare側の投資と開発が継続中です。

Edge DB需要の構造的成長: AI・Edge Computingの普及でレイテンシの低いDBへの需要は増加しています。Workers Runtimeとバインド直結するD1は、この需要に対して構造的に正しい答えを持っています。

選定理由

独立選択の根拠

Cloudflare Pagesで個人開発を行う場合、リレーショナルデータの保存先としてD1は最初に検討すべき選択肢です。
Workers Runtimeに直接バインドできる唯一のSQLデータベースであり、ユーザーテーブル・記事データ・注文履歴・分析ログなど、構造化されたデータであればどのドメインにも適用できます。

D1を選んだ決定要因は「Workersランタイムの外に出ない」という設計原則です。KVはKey-Value構造のためリレーショナルなテーブル設計には不向きです。R2はオブジェクトストレージのためDBとしての利用は想定外です。D1はCloudflareストレージの中で唯一、SQLによる汎用的なリレーショナル操作が可能なサービスです。

なぜ代替案を選ばなかったか

不自然な構造(悪)として、以下の選択肢を検討しましたが採用しませんでした。

技術 不採用理由
PlanetScale / Supabase SupabaseはPostgreSQLベースの強力なBaaSだが、WorkersからはTCP接続経由のHTTP呼び出しが必要になる。D1はRuntime Bindingで直結するため、接続モデルが根本的に異なる。加えてWorkersはIPが固定されないためSupabase側のIP制限が使えず、Hyperdriveで解決しようとすると月額$5〜の追加コストが発生する。
Firebase Firestore Google依存が生まれる。Edge Runtimeからの接続に追加の設定が必要で、Cloudflare完結の設計原則を損なう。
Clerk ユーザーDBがClerk側に分散するリスクと、外部SaaS障害で認証全停止するリスクがある。

構造的メリット

  1. Workers Bindingによるゼロレイテンシアクセス: D1はHTTP呼び出し不要でWorkersから直接操作できます。認証処理・記事取得・統計集計のたびに外部DBへのTCPコネクションを張る必要がなく、エッジでの応答速度が維持されます。

  2. SQLという普遍的なインターフェース: D1はSQLiteのSQLをそのまま使えるため、ORMや独自APIへの依存が最小化されます。Claude Codeに渡すコンテキストもSQL+TypeScriptで完結するため、AIによるクエリ生成が容易で開発速度が向上します。

  3. Cloudflare完結によるコスト予測性: ユーザーデータ(D1)・セッション(KV)・ファイル(R2)がすべてCloudflareプラットフォーム内に収まります。外部SaaSの料金体系変更に振り回されるリスクがなく、月次コストの見通しが立てやすくなります。

  4. SQLのAI親和性: D1はSQLiteのSQLを直接記述するため、ORMの抽象レイヤーがありません。Claude Codeはスキーマ定義とSQLクエリをそのまま読めるため、テーブル設計・クエリ生成・マイグレーション計画をコンテキスト最小で正確に実行できます。独自クエリAPIを持つNoSQLと比べて、AIが生成するコードの精度が構造的に高くなります。

ClaudeMixでの活用例

以下はD1 Bindingをwrangler.tomlで設定し、WorkersアクションからD1を型安全に操作するボイラープレートです。

# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "claudemix-db"
database_id = "your-database-id"
// app/types/cloudflare.ts
export interface Env {
  DB: D1Database;
  KV: KVNamespace;
}

ユーザーデータのCRUD例:

// app/data-io/account/auth/user.server.ts
import type { D1Database } from "@cloudflare/workers-types";

export type User = {
  id: number;
  email: string;
  passwordHash: string;
  createdAt: string;
};

export async function findUserByEmail(
  db: D1Database,
  email: string
): Promise<User | null> {
  const result = await db
    .prepare(
      "SELECT id, email, password_hash, created_at FROM users WHERE email = ?"
    )
    .bind(email)
    .first<{ id: number; email: string; password_hash: string; created_at: string }>();

  if (!result) return null;

  return {
    id: result.id,
    email: result.email,
    passwordHash: result.password_hash,
    createdAt: result.created_at,
  };
}

サブスクリプション状態の取得例:

// app/data-io/account/subscription/getSubscriptionByUserId.server.ts
import type { Subscription } from '~/specs/account/types';
import { getEnv, CloudflareLoadContext } from '../common/getEnv.server';

export async function getSubscriptionByUserId(
  userId: string,
  context: CloudflareLoadContext
): Promise<Subscription | null> {
  const env = getEnv(context);
  if (!userId) return null;

  const subscription = await env.DB
    .prepare(`
      SELECT
        id,
        user_id AS userId,
        stripe_subscription_id AS stripeSubscriptionId,
        plan_id AS planId,
        status,
        current_period_end AS currentPeriodEnd
      FROM subscriptions
      WHERE user_id = ?
      ORDER BY created_at DESC
      LIMIT 1
    `)
    .bind(userId)
    .first<Subscription>();

  return subscription ?? null;
}

サブスクリプション状態の管理においてD1はStripeの「ローカルミラー」として機能します。Stripeが source of truth であり、D1はwebhook経由で同期された高速参照用のコピーです。記事アクセスのたびにStripe APIを叩くとレイテンシが増しレートリミットにも当たるため、D1にミラーしてWorkersから直接参照する設計がベストプラクティスです。ClaudeMixではisWebhookEventProcessedによる冪等性チェックとrecordWebhookEventによる処理済み記録を組み合わせ、webhookの二重処理を防いでいます。

エラーハンドリングの思想として、D1のクエリエラーは「外部サービス障害」と同じカテゴリで扱います。D1はCloudflareインフラに依存しているため、稼働率はCloudflare全体の信頼性に連動します。個々のクエリではtry/catchでエラーをキャッチし、ユーザーには「しばらくお待ちください」という汎用メッセージを返す設計が適切です。

D1でデータは保存できる。しかし本番サービスを動かすにはこれだけでは足りない。ログインセッションの管理・メール認証・課金状態の同期——これらがなければサービスは完成しない。Cloudflare WorkersでD1を選んだなら、次はセッションをKVでどう管理するか、メール認証をResendでどう実装するかという問いが待っている。この3つを組み合わせて初めてスタックが成立する。

公式リファレンス・更新履歴

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SEPARATION|責務を分離

D1で管理するデータとKVで管理するデータの責務境界が守られているか確認する

調査:

  1. セッションデータがD1ではなくKVで管理されているか確認せよ
  2. 永続的なユーザーデータ・記事データがD1に正しく配置されているか確認せよ
  3. TTLが必要なデータがD1のcronではなくKVのTTL機能で管理されているか確認せよ

この内容をClaude Codeに貼り付けて実行します

PERFORMANCE|パフォーマンス改善

Workers BindingによるゼロレイテンシD1アクセスが設計通りに機能しているか確認する

調査:

  1. D1へのアクセスがWorkers Bindingを経由しているか確認せよ(外部HTTP呼び出しになっていないか)
  2. Cloudflare Pagesのwrangler設定でD1 Bindingが正しく定義されているか確認せよ
  3. ローカル開発環境でD1 Bindingが機能しているか確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。