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障害で認証全停止するリスクがある。 |
構造的メリット
Workers Bindingによるゼロレイテンシアクセス: D1はHTTP呼び出し不要でWorkersから直接操作できます。認証処理・記事取得・統計集計のたびに外部DBへのTCPコネクションを張る必要がなく、エッジでの応答速度が維持されます。
SQLという普遍的なインターフェース: D1はSQLiteのSQLをそのまま使えるため、ORMや独自APIへの依存が最小化されます。Claude Codeに渡すコンテキストもSQL+TypeScriptで完結するため、AIによるクエリ生成が容易で開発速度が向上します。
Cloudflare完結によるコスト予測性: ユーザーデータ(D1)・セッション(KV)・ファイル(R2)がすべてCloudflareプラットフォーム内に収まります。外部SaaSの料金体系変更に振り回されるリスクがなく、月次コストの見通しが立てやすくなります。
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つを組み合わせて初めてスタックが成立する。
公式リファレンス・更新履歴
- Cloudflare D1 公式ドキュメント
- D1 Workers Binding API リファレンス
- D1 Local Development(wrangler)
- 最終確認日: 2026-02-26
- 次回確認予定: D1のメジャーバージョンアップ時
