Tenant A — Tokyo

cffs.norimiso.net

Cloudflare for SaaS + Workers for Platforms demo — powered by Hono

Architecture

USERS cffs.norimiso.net Tenant A cffs.nori-labo.com Tenant B (Cloudflare for SaaS) CLOUDFLARE EDGE Cloudflare for SaaS Custom Hostname cffs-dispatcher Hono + Cache API Workers for Platforms + R2 + D1 cffs-contents D1 (Content API) Cache Edge PoP TENANT WORKERS cffs-tenant-norimiso Worker A cffs-tenant-norilabo Worker B R2 STORAGE norimiso/ (karaage) norilabo/ (sushi) content.html style.css hero.jpg Active: cffs.norimiso.net Cache: max-age=3600 (R2) / 300 (page)

Domain-specific Content (D1) — ドメインに応じたデータを Workers + D1 から取得

東京都

品川区

品川駅周辺の再開発が進む。天王洲アイルや戸越銀座商店街が人気。

東京都

港区

東京タワー、六本木ヒルズ、赤坂サカス。国際色豊かなビジネス街。

東京都

世田谷区

都内最大の人口を誇る住宅地。下北沢や三軒茶屋など個性的な街が点在。

Pages

Presentation

Source Code

cffs-dispatcher / src/index.ts — Hono ルーティング
import { Hono } from "hono";

// ホスト名 → テナント設定のマッピング
const TENANTS: Record<string, TenantConfig> = {
  "cffs.norimiso.net": { workerName: "cffs-tenant-norimiso", r2Prefix: "norimiso" },
  "cffs.nori-labo.com": { workerName: "cffs-tenant-norilabo", r2Prefix: "norilabo" },
};

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

// テナント解決ミドルウェア
app.use("*", async (c, next) => {
  const tenant = TENANTS[new URL(c.req.url).hostname];
  c.set("tenant", tenant);
  await next();
});

// / → D1 からドメイン別データを取得して動的 HTML 生成 (キャッシュ付き)
app.get("/", async (c) => { /* ... */ });

// /content.html → R2 バケットからテナント別コンテンツを配信 (キャッシュ付き)
app.get("/content.html", serveR2Content);
app.get("/content/*", serveR2Content);

// パージ API (Bearer 認証)
app.post("/api/purge", async (c) => { /* ... */ });

// その他 → Workers for Platforms でテナント Worker を呼び出し
app.all("*", async (c) => {
  const worker = c.env.DISPATCHER.get(tenant.workerName);
  return worker.fetch(c.req.raw);
});

export default app;
Cache API — エッジキャッシュの読み書き
// Workers Cache API でエッジにレスポンスをキャッシュ
// テナント別の Cache-Tag でタグベースのパージが可能 (Enterprise)

// キャッシュ読み取り
const cache = caches.default;
const cached = await cache.match(new Request(url));
if (cached) return cached;  // X-Cache: HIT

// キャッシュ書き込み (Cache-Control で TTL、Cache-Tag でタグ制御)
const response = new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=3600",
    "Cache-Tag": `tenant:${tenant.r2Prefix},type:r2`,
  },
});
c.executionCtx.waitUntil(cache.put(cacheKey, response.clone()));

// Cloudflare for SaaS ではキャッシュは SaaS プロバイダーのゾーンで管理
// → cffs.nori-labo.com のリクエストも norimiso.net ゾーンのキャッシュに保存
Cache Purge API — POST /api/purge
// Bearer 認証付きパージエンドポイント
app.post("/api/purge", async (c) => {
  const auth = c.req.header("Authorization");
  if (auth !== `Bearer ${c.env.PURGE_TOKEN}`) return c.json({ error: "Unauthorized" }, 401);

  const { paths, hosts } = await c.req.json();
  const cache = caches.default;

  for (const host of hosts) {
    for (const path of paths) {
      await cache.delete(new Request(`https://${host}${path}`));
    }
  }
  return c.json({ purged: paths.length * hosts.length });
});

// Cache Tags でテナント単位パージ (Enterprise Zone):
// curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
//   -H "Authorization: Bearer {token}" -d '{"tags":["tenant:norimiso"]}'
cffs-tenant-norimiso / src/index.ts
// Tenant Worker: テナント固有の Static Assets と API を提供
// Workers for Platforms の Dispatch Namespace にデプロイされる
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // /api/info → テナント情報を JSON で返却
    if (url.pathname === "/api/info") {
      return Response.json({
        worker: "cffs-tenant-norimiso",
        hostname: url.hostname,
        message: "This response is from the norimiso.net tenant worker",
      });
    }

    // それ以外 → Static Assets (public/index.html) を返却
    return env.ASSETS.fetch(request);
  },
};
cffs-contents / schema.sql (D1)
CREATE TABLE locations (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  domain      TEXT NOT NULL,     -- ホスト名でフィルタ
  prefecture  TEXT NOT NULL,     -- 都道府県
  ward        TEXT NOT NULL,     -- 区名
  description TEXT NOT NULL      -- 説明文
);

-- norimiso.net → 東京都の区、nori-labo.com → 大阪府の区
INSERT INTO locations (domain, prefecture, ward, description) VALUES
  ('cffs.norimiso.net', '東京都', '渋谷区', 'スクランブル交差点で知られる...'),
  ('cffs.nori-labo.com', '大阪府', '北区', '梅田を擁する大阪の玄関口...');

-- Dispatcher が domain でフィルタして 3 件ランダム取得:
-- SELECT * FROM locations WHERE domain = ? ORDER BY RANDOM() LIMIT 3
cffs-dispatcher / wrangler.jsonc
{
  "name": "cffs-dispatcher",
  // Workers for Platforms の Dispatch Namespace バインディング
  "dispatch_namespaces": [{ "binding": "DISPATCHER", "namespace": "cffs-production" }],
  // Content API Worker への Service Binding
  "services": [{ "binding": "CMS", "service": "cffs-contents" }],
  // テナント別コンテンツ用 R2 バケット
  "r2_buckets": [{ "binding": "R2_CONTENT", "bucket_name": "cffs" }]
}