Workers + Hono + D1 + Stripe

了解。**そのままコピペで動く最小構成(実用レベル)**を出します。
構成は👇

  • Cloudflare Workers

  • Hono

  • Cloudflare D1

  • Stripe


■ 完成構成

project/
 ├ wrangler.toml
 ├ package.json
 └ src/
     └ index.ts

■ ① package.json

{
  "name": "workers-hono-stripe",
  "type": "module",
  "dependencies": {
    "hono": "^4.0.0",
    "stripe": "^14.0.0"
  }
}

■ ② wrangler.toml

name = "hono-stripe-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

[vars]
STRIPE_SECRET_KEY = "sk_test_xxx"
STRIPE_WEBHOOK_SECRET = "whsec_xxx"

■ ③ D1 初期テーブル

CREATE TABLE users (
  id TEXT PRIMARY KEY,
  email TEXT,
  stripe_customer_id TEXT,
  created_at TEXT
);

CREATE TABLE subscriptions (
  id TEXT PRIMARY KEY,
  user_id TEXT,
  status TEXT,
  price_id TEXT,
  current_period_end INTEGER
);

■ ④ src/index.ts(コア)

import { Hono } from 'hono'
import Stripe from 'stripe'

type Bindings = {
  DB: D1Database
  STRIPE_SECRET_KEY: string
  STRIPE_WEBHOOK_SECRET: string
}

const app = new Hono<{ Bindings: Bindings }>()

/* -----------------------------
   Stripe 初期化
----------------------------- */
const getStripe = (env: Bindings) => {
  return new Stripe(env.STRIPE_SECRET_KEY, {
    apiVersion: '2023-10-16'
  })
}

/* -----------------------------
   ヘルスチェック
----------------------------- */
app.get('/', (c) => {
  return c.text('OK')
})

/* -----------------------------
   Checkout作成
----------------------------- */
app.post('/create-checkout', async (c) => {
  const stripe = getStripe(c.env)

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [
      {
        price: 'price_xxx', // ← StripeのPrice ID
        quantity: 1
      }
    ],
    success_url: 'https://example.com/success',
    cancel_url: 'https://example.com/cancel'
  })

  return c.json({ url: session.url })
})

/* -----------------------------
   Webhook(超重要)
----------------------------- */
app.post('/webhook', async (c) => {
  const stripe = getStripe(c.env)

  const sig = c.req.header('stripe-signature')
  const body = await c.req.text()

  let event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig!,
      c.env.STRIPE_WEBHOOK_SECRET
    )
  } catch (err) {
    return c.text('Webhook Error', 400)
  }

  switch (event.type) {
    case 'checkout.session.completed':
      const session = event.data.object as any

      await c.env.DB.prepare(
        `INSERT INTO subscriptions (id, user_id, status)
         VALUES (?, ?, ?)`
      )
        .bind(session.id, session.customer, 'active')
        .run()
      break

    case 'customer.subscription.deleted':
      const sub = event.data.object as any

      await c.env.DB.prepare(
        `UPDATE subscriptions SET status = ? WHERE id = ?`
      )
        .bind('canceled', sub.id)
        .run()
      break
  }

  return c.text('ok')
})

/* -----------------------------
   課金チェック(SaaSガード)
----------------------------- */
app.get('/protected', async (c) => {
  const userId = 'test_user' // ← 本来は認証から取得

  const result = await c.env.DB.prepare(
    `SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active'`
  )
    .bind(userId)
    .first()

  if (!result) {
    return c.text('Not subscribed', 403)
  }

  return c.text('Welcome premium user')
})

export default app

■ ⑤ デプロイ手順

npm install
npx wrangler d1 create app-db
npx wrangler publish

■ ⑥ Stripe側設定

やること👇

  1. 商品作成

  2. Price ID取得

  3. Webhook設定

    • URL:

      https://あなたのworker/workhook
      
    • イベント:

      • checkout.session.completed

      • customer.subscription.deleted


■ ここが“実務で強いポイント”

このテンプレは👇

  • 課金 → Stripe

  • DB → D1

  • API → Hono

  • 実行 → Workers

👉 完全にSaaSの基本構造


■ 次にやるべき拡張(重要)

このままだと「荒い」ので👇

  1. 認証追加(Kinde or Auth0)

  2. user_idをStripeと紐付け

  3. middlewareで課金ガード


■ ワンランク上(欲しければ出せる)

  • 「未課金ユーザー完全遮断middleware」

  • 「Stripe Customer自動作成」

  • 「Kindeログイン連携」

  • 「管理画面(管理者だけ見れる)」


必要なら👇
👉 “本番レベル(課金漏れゼロ設計)”テンプレも出せます