Mental model 3 tầng binding: Request, Identity, Storage

Khung tư duy chung cho mọi Worker: Request là cửa vào, Identity là ai đang gọi, Storage là đọc ghi đâu. Áp dụng vào Worker đang chạy blog này và cách chọn storage primitive đúng.

· 8 phút đọc · Read in English
Mental model 3 tầng binding cho Worker — Request (fetch/scheduled/queue), Identity (Access/JWT/service token), Storage (D1/KV/R2/DO) — áp dụng vào kiến trúc blog full-stack chạy trên Cloudflare

TL;DR

Mọi Worker, từ API 50 dòng tới blog full-stack 5000 dòng, đều có thể tách ra thành 3 tầng binding:

  1. Request, vào từ đâu (fetch, scheduled, queue, request.cf metadata).
  2. Identity, ai đang gọi (Access JWT, session cookie, API key, mTLS, OIDC federation).
  3. Storage, đọc ghi đâu (D1, KV, R2, Queues, Durable Objects, Vectorize, Cache API, Workers AI).

Luận điểm chính:

Khi debug hoặc thiết kế, hỏi câu “tầng nào?” trước khi hỏi “cái gì sai?”. 80% bug nằm ở ranh giới giữa 2 tầng, ví dụ identity verify nhầm trước storage read, hoặc storage trả về dữ liệu cũ khi identity đã đổi.

Bài này định nghĩa 3 tầng, ánh xạ chúng vào Worker đang chạy blog này, và dựng cây quyết định để chọn storage primitive đúng. Từ Part 5-8 đi sâu từng storage; Parts 13-16 AI + DO.


Dành cho ai

  • Dev đã biết viết handler fetch đầu tiên, giờ đang mở rộng lên ứng dụng full-stack.
  • Người cần quyết định kiến trúc trước khi code: storage nào, identity nào, có cần Durable Object không.
  • Ai đọc code Worker của người khác và muốn tách lớp nhanh.

Nên đọc trước: Part 1 (tổng quan nền tảng), Part 2 (vòng đời môi trường chạy).

Sau bài này bạn sẽ:

  • Ánh xạ một Worker thực tế thành 3 tầng.
  • Biết chọn D1 vs KV vs R2 vs Durable Object vs Cache API.
  • Phân biệt các cơ chế identity phổ biến và khi nào dùng cái nào.

Bài này không nói về gì

  • Chi tiết từng storage primitive: Part 5 (KV), Part 6 (D1), Part 7 (R2), Part 8 (Queues + DO).
  • Router framework (Hono, Itty): Part 9.
  • Workers AI / Vectorize cụ thể: Part 13-14.

Nguyên tắc: 3 tầng, không hơn

Mental model 3 tầng binding của Worker: tầng Request chứa fetch handler, scheduled cron, queue consumer, request.cf metadata; tầng Identity chứa Cloudflare Access JWT, session cookie, API key, OIDC federation; tầng Storage chứa D1, KV, R2, Queues, Durable Objects, Vectorize, Workers AI, Cache API. Request đi qua 3 tầng theo thứ tự.

① Request

Tầng Request trả lời: sự kiện gì đã kích hoạt Worker này chạy?

Có 3 nguồn kích hoạt:

  • HTTP request: fetch(request, env, ctx). 99% traffic đi qua đây.
  • Cron: scheduled(event, env, ctx). Kích hoạt theo crons trong wrangler.jsonc.
  • Queue message: queue(batch, env, ctx). Consumer cho Queues binding.

Ngoài ra còn có tail handler (log consumer), email handler (Email Workers), nhưng ít gặp.

Từ tầng Request bạn có:

  • request.url, request.method, request.headers, request.body.
  • request.cf: metadata edge (country, colo, botScore, tlsVersion, tlsClientAuth). Miễn phí, không cần dịch vụ bên ngoài.
  • event.cron (cho scheduled): chuỗi cron đã kích hoạt.
  • batch.messages (cho queue): mảng message với id, body, ack(), retry().

Tất cả còn lại là logic ứng dụng, chạy trong môi trường chạy đã nêu ở Part 2.

② Identity

Tầng Identity trả lời: ai đang gọi, với quyền gì?

Tầng này thường bị gộp vào logic ứng dụng, nhưng tách ra giúp code sạch hơn và audit dễ hơn. 4 cơ chế phổ biến:

Cloudflare Access JWT

Dùng cho /admin/* hoặc endpoint nội bộ. Access đứng trước Worker, inject header Cf-Access-Jwt-Assertion. Worker verify JWT qua Access team JWKS.

import { verifyAccessJwt } from "./lib/access-jwt";

async fetch(request, env, ctx) {
  const jwt = request.headers.get("Cf-Access-Jwt-Assertion");
  if (!jwt) return new Response("Missing JWT", { status: 401 });

  const claims = await verifyAccessJwt(jwt, env.CF_ACCESS_TEAM_DOMAIN, env.CF_ACCESS_AUD);
  if (!claims) return new Response("Invalid JWT", { status: 403 });

  const adminEmails = env.ADMIN_EMAILS.split(",");
  if (!adminEmails.includes(claims.email)) {
    return new Response("Not admin", { status: 403 });
  }

  // Giờ mới vào logic ứng dụng
  return handleAdminRequest(request, env, claims);
}

Blog này dùng pattern này cho /admin/*. Hiện tại có worker/lib/access-jwt.ts verify JWKS + cache 10 phút trong bộ nhớ isolate.

Session cookie với HMAC

Cho newsletter hủy đăng ký, webmention confirm. Cookie chứa token được ký HMAC với secret. Verify bằng crypto.subtle + so sánh constant-time.

import { timingSafeEqual } from "./lib/http";

async function verifyUnsubscribeToken(token: string, email: string, secret: string) {
  const expected = await hmacSha256(secret, email);
  return timingSafeEqual(token, expected);
}

API key / Service token

Cho caller không phải con người (CI, cron bên ngoài, webhook bên thứ ba). Lưu secret qua wrangler secret put.

async fetch(request, env) {
  const key = request.headers.get("X-API-Key");
  const valid = await timingSafeEqual(key ?? "", env.API_KEY);
  if (!valid) return new Response("Unauthorized", { status: 401 });
  // ...
}

Với mTLS, Cloudflare Access làm phần nặng. Worker chỉ cần check request.cf.tlsClientAuth.certVerified === "SUCCESS".

OIDC federation

Cho xác thực giữa hai khối lượng công việc (workload) ngoài Cloudflare. Worker phát hành OIDC token → AWS STS / GCP STS → credentials tạm có phạm vi giới hạn. Không lưu access key dài hạn.

Blog này dùng để gọi AWS Bedrock (Claude Opus) cho tóm tắt AI:

  1. Worker ký JWT với private key trong secret.
  2. POST tới AWS STS AssumeRoleWithWebIdentity với JWT.
  3. STS trả credentials tạm (hiệu lực 15-60 phút).
  4. Worker cache credentials trong KV, dùng lại đến khi hết hạn.
  5. Gọi Bedrock với credentials đó.

Pattern này sẽ có bài riêng trong series này (ngoài phạm vi). Điểm cần nhớ: không có AWS access key dài hạn trong .env, trong CI secret, trong worker secret.

③ Storage

Tầng Storage trả lời: đọc / ghi ở đâu, với pattern nào?

Đây là tầng nhiều primitive nhất và dễ chọn sai nhất. Cây quyết định ở phần sau.


Áp dụng: Worker của blog này

Blog cloudsecop.net chạy trên một Worker. Tách theo 3 tầng:

Tầng Request

  • fetch: định tuyến theo path (/, /blog/*, /api/*, /admin/*, /og/*.png).
  • scheduled: cron 0 2 * * SUN chạy digest hàng tuần + retry gửi webmention.
  • queue: consumer cho build lại Vectorize index khi có bài mới.

Tầng Identity

EndpointIdentity mechanism
Trang publicKhông identity (ẩn danh)
/api/subscribeTurnstile token + email + chống abuse
/api/unsubscribe/*HMAC-signed token trong URL
/api/contactTurnstile + chống abuse
/api/webmentionValidate source URL (chống SSRF)
/api/email-inboundHMAC secret từ Resend webhook
/admin/*Cloudflare Access JWT + ADMIN_EMAILS allowlist
Bedrock callOIDC federation → AWS STS

Tầng Storage

Mục đíchPrimitive
Subscribers, page viewsD1 (khavan-subscribers)
Cache tóm tắt AID1 (ai_summaries)
Embedding bài viếtVectorize (khavan-posts)
Credentials tạm OIDCKV (OIDC_CREDS_CACHE)
Feature flag, configKV
Analytics eventAnalytics Engine
Static assetWorkers Assets (env.ASSETS)
Ảnh OG sinh raTính per-request, cache qua Cache API
Cron digestD1 (subscribers) + Resend API

Một Worker. Mỗi endpoint tách rõ ràng theo 3 tầng. Khi debug, đi theo 3 tầng theo thứ tự ngược: lỗi ở storage? identity? parse request?


Chọn storage primitive nào

Đây là câu hỏi hay sai nhất. Cây quyết định:

Storage decision tree: dữ liệu có schema cộng query hoặc join thì dùng D1. Blob lớn hoặc public URL dùng R2. Cần single-writer hoặc realtime dùng Durable Objects. Eventual consistency đủ dùng KV. Còn lại dùng Cache API cho HTTP cache.

D1 khi

  • Dữ liệu có schema cố định (users, posts, orders).
  • Cần query SQL phức tạp (JOIN, GROUP BY, window function).
  • Cần transaction (insert hàng loạt atomic, lùi phiên bản).
  • Dùng FTS cho tìm kiếm full-text.
  • Kích thước tổng < 10GB mỗi database.

KHÔNG dùng D1 khi: dữ liệu > 10GB, cần đọc dưới mili-giây toàn cầu (D1 primary ở 1 region), hoặc blob nhị phân > 1MB (dùng R2).

R2 khi

  • Object nhị phân (ảnh, video, PDF, zip).
  • File > 25MB (KV tối đa 25MB).
  • Cần URL public hoặc URL ký trước.
  • Cần egress miễn phí (không tính phí theo GB).
  • Thay S3 mà giữ client boto3/aws-sdk.

KHÔNG dùng R2 khi: cần truy vấn nội dung (dùng D1 hoặc Vectorize), hoặc cần liệt kê prefix cực nhanh (dùng metadata KV).

KV khi

  • Key-value đơn giản, đọc toàn cầu.
  • Cache metadata (feature flag, redirect map, tag alias).
  • Dữ liệu session, token auth tạm.
  • Tra cứu thường xuyên, eventual consistency OK (lan tỏa < 60s).

KHÔNG dùng KV khi: cần strong consistency (dùng D1 hoặc Durable Object), value > 25MB, hoặc tần suất ghi > 1 write/key/giây.

Durable Objects khi

  • Điều phối single-writer (counter, rate limiter, lock).
  • WebSocket server (chat room, game nhiều người, trình soạn thảo cộng tác).
  • Session với state trong bộ nhớ (giỏ hàng, form wizard).
  • Thao tác transactional trên nhiều key.

KHÔNG dùng DO khi: khối lượng công việc stateless (dùng Worker + D1), hoặc cần truy vấn toàn cầu (DO ghim ở 1 region).

Cache API khi

  • Cache HTTP response (tránh sinh lại cùng response).
  • Warmup sau cold fetch.
  • Cache response URL ký trước trong TTL ngắn.

KHÔNG dùng Cache API khi: cần chia sẻ dữ liệu giữa request ngoài HTTP (dùng KV), hoặc cần invalidate theo key pattern (dùng KV với TTL).

Queues khi

  • Job nền fire-and-forget.
  • Giới hạn tốc độ / làm mượt traffic đi ra.
  • Retry với exponential backoff.
  • Xử lý fan-out / fan-in.

Vectorize khi

  • Tìm kiếm semantic với embedding.
  • Pattern RAG (lấy context liên quan cho LLM).
  • Tìm kiếm tương đồng.

Workers AI khi

  • Chạy inference với model có sẵn trong catalog (embedding, LLM nhỏ, sinh ảnh).
  • Không cần GPU tự train.

Gotchas phổ biến

① Gộp Identity vào handler chính

// Khó test, khó audit
async fetch(request, env) {
  const jwt = request.headers.get("Cf-Access-Jwt-Assertion");
  // ... 30 dòng verify ...
  if (authorized) {
    const row = await env.DB.prepare("...").first();
    // ...
  }
}

Tách identity ra thành middleware:

async fetch(request, env) {
  const claims = await requireAdmin(request, env);
  if (claims instanceof Response) return claims; // 401/403

  return handleAdminRequest(request, env, claims);
}

Identity là tầng tách bạch, test riêng được, audit log riêng được.

② Dùng sai primitive

Hay gặp:

  • Dùng KV cho dữ liệu session cần cập nhật liên tục, chạm giới hạn 1 write/key/giây.
  • Dùng D1 cho blob nhị phân, làm row phình ra, query chậm.
  • Dùng Durable Object cho khối lượng công việc stateless, ghim ở 1 region, mất lợi thế edge.
  • Dùng R2 cho metadata nhỏ cần truy vấn, liệt kê object chậm.

Cây quyết định ở trên là hướng dẫn đầu tiên. Khi phân vân, bắt đầu với D1 + KV, mở rộng khi thấy nút thắt.

③ Storage không qua Identity

// SAI
async fetch(request, env) {
  if (url.pathname === "/api/user") {
    const id = url.searchParams.get("id");
    return Response.json(await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(id).first());
  }
}

Không có identity check, ai cũng xem được profile bất kỳ. Storage phải luôn sau Identity, không trước hoặc bên cạnh.

④ Cache kết quả Identity sai

// SAI
const cachedClaims = new Map<string, Claims>();

async function verifyJwt(jwt: string) {
  if (cachedClaims.has(jwt)) return cachedClaims.get(jwt);
  // ...
}

Map ở cấp module không tồn tại qua request (Part 2). Và cache JWT lâu là rủi ro bảo mật (revoke không hiệu quả). Dùng KV với TTL ngắn (60s) hoặc verify lại mỗi request.


Production checklist

  • Worker tách được rõ ràng thành 3 tầng khi vẽ kiến trúc.
  • Mỗi endpoint có cơ chế Identity rõ ràng (kể cả “public” phải ghi tường minh).
  • Identity verify trước khi đụng storage, không gộp chung.
  • Lựa chọn storage có lý do (không “vì KV dễ nhất”): khớp với cây quyết định.
  • Lưu secret bằng wrangler secret put, không hardcode trong repo hay env var plaintext.
  • OIDC federation được ưu tiên cho xác thực giữa hai khối lượng công việc, không key dài hạn.
  • Logging phân biệt 3 tầng (nhận request, identity verify xong, thao tác storage xong).

Kết

3 tầng binding là khung tư duy lặp đi lặp lại trong mọi bài của series. Part 4 tới đi vào vòng lặp dev cụ thể: Wrangler, Miniflare, dev local, test với vitest.

Từ Part 5 bắt đầu đi sâu tầng Storage: KV, D1, R2, Queues, Durable Objects. Mỗi bài sẽ có code thật, gotcha thật từ quá trình build blog này.


Tham khảo