§01สิ่งที่เราจะสร้าง
DigiVend คือร้านขายของดิจิทัล — EP นี้ขาย ไฟล์ PDF ก่อน (พวก ebook, template, คู่มือ) loop หลักคือ:
- ลูกค้า เปิดดูสินค้า ในร้าน → กด ซื้อ
- ระบบสร้าง order แล้วโชว์ PromptPay QR ตามราคาสินค้า → ลูกค้าสแกนจ่ายในแอปธนาคาร
- ลูกค้า อัปสลิป → backend ส่งไป verify กับ SlipOK → order กลายเป็น paid
- พอจ่ายแล้ว ปุ่ม Download ถึงจะโผล่ และโหลดไฟล์จริงได้
จุดที่ทำให้ร้าน "ดิจิทัล" ต่างจากร้านทั่วไป: ไม่มีการส่งของ — ไฟล์จะถูกปลดล็อกตอนที่ backend ยืนยันว่าจ่ายเงินแล้วเท่านั้น และต้องไม่หลุดไปถึง browser ก่อนจ่าย อันนี้คือหัวใจของ episode เลย
Demo สุดท้าย
สแกน QR → จ่ายเงินในแอปธนาคาร → อัปสลิป → ระบบ verify ผ่าน → ปุ่ม Download โผล่ ไฟล์ PDF เด้งลงมา ทั้งหมดนี้รันอยู่บน URL จริงของ Cloudflare ไม่ใช่ localhost
สิ่งที่เราตัดทิ้งโดยตั้งใจ (สำคัญ — ต้องเข้าใจ scope)
- ❌ ระบบ login / หลังร้านของคนขาย — EP1 สินค้า seed ไว้ก่อน (ใส่ผ่าน script) ส่วน admin ที่ login เพิ่มสินค้าได้ ไว้ EP2
- ❌ สินค้าแบบ "โค้ด/ไอดี/พาสเวิร์ด" (license key, account login) ที่ต้องมี stock เป็น pool — ไว้ EP3
- ❌ ตะกร้าหลายชิ้น, ประวัติคำสั่งซื้อ, ใบเสร็จส่งอีเมล, อัปโหลดรูปสินค้า — ยังไม่ใช่ตอนนี้
💡 ทำไมต้องตัด: EP นี้โฟกัสที่ core loop เดียว — ซื้อ → จ่ายด้วย QR จริง → ปลดล็อกไฟล์ — แล้ว deploy ขึ้นจริง การมี admin หรือ stock pool ตั้งแต่ EP แรกจะทำให้ build ไม่จบใน sitting เดียว เราค่อยต่อยอดทีละ EP
§02Stack ที่ใช้ และเหตุผล
| Tool | ทำไมเลือก | ทำไมไม่ใช่ตัวอื่น |
|---|---|---|
| Cloudflare Workers | deploy ง่าย, free tier ใจดี, ทุก EP ใช้ตัวเดียวกัน | ไม่ใช้ Pages เพราะ Cloudflare ดัน Workers สำหรับโปรเจกต์ใหม่แล้ว |
| Vite + React | dev เร็ว, มาตรฐาน SPA | — |
@cloudflare/vite-plugin | รัน Worker ใน dev server เดียวกัน — ไม่ต้องเจอ CORS | ไม่รัน wrangler แยกพอร์ตให้ปวดหัวเรื่อง CORS |
| Hono | API framework เล็กๆ บน Worker, โตไป full-stack ได้ | ไม่เขียน fetch handler ดิบเพราะ routing ลำบาก |
| D1 + Drizzle | เก็บ products / orders, กัน slip ซ้ำด้วย UNIQUE index | ไม่ใช้ Supabase/Postgres ภายนอก — D1 อยู่ใน Cloudflare เลย |
| R2 | เก็บไฟล์ PDF, แล้วให้ Worker stream เฉพาะ order ที่จ่ายแล้ว | ไม่ยัดไฟล์ลง D1 (เละ) ไม่วางเป็น static asset (จะโหลดฟรีโดยไม่จ่าย) |
slipok skill (promptpay-qr + SlipOK) | flow "สแกนจ่าย + เช็คสลิป" สำเร็จรูป เขียนครั้งเดียวใช้ซ้ำทุกแอป | ไม่เขียน QR/verify เองทุกครั้ง |
ที่เราข้าม:
- ❌ Better Auth — ยังไม่มี login ใน EP1 (ลูกค้าซื้อแบบ guest) จะมาตอน EP2
- ❌ Pages, Next.js, Supabase — เราอยู่บน Cloudflare Workers + Vite
§03เตรียมตัวก่อนเริ่ม (และ Prebuild)
ก่อนเริ่ม ให้แน่ใจว่ามี:
- Node.js 20+ (
node -v) - pnpm
- Cloudflare account (free ก็พอ) +
wrangler loginแล้ว - บัญชี SlipOK (สมัครที่ slipok.com) เอา
API_KEYกับBRANCH_IDมา + PromptPay ID ของตัวเอง (เบอร์/เลขบัตรปชช./e-wallet) ไว้รับเงิน
Prebuild — สิ่งที่ทำไว้ก่อนหน้าแล้ว (นอกกล้อง):
# 1) scaffold โปรเจกต์ (React + Hono API + Vite บน Workers)
pnpm create cloudflare@latest digivend --framework=react
cd digivend
# 2) สร้าง resource บน Cloudflare
wrangler d1 create digivend-db
wrangler r2 bucket create digivend-files
# 3) ลง Drizzle + slipok deps
pnpm add drizzle-orm
pnpm add -D drizzle-kit
pnpm add promptpay-qr qrcode
# 4) deploy ทิ้งครั้งแรกเพื่อเช็ค toolchain ว่าขึ้นได้จริง
pnpm run deploy🎬 เราเริ่มถ่ายตรงจุดที่ scaffold เสร็จ + ผูก D1/R2 + ก๊อปโมดูล slipok เข้ามา + seed สินค้าตัวอย่าง (4 ชิ้น) พร้อมอัป PDF ขึ้น R2 + วาง base style จาก Claude Design เรียบร้อยแล้ว เพราะส่วนนี้คือ setup น่าเบื่อที่ไม่ใช่หัวใจของ EP ใครอยากตามให้รันคำสั่งข้างบนก่อน แล้วมาเริ่มที่ Checkpoint 1 พร้อมกัน
ตั้งค่า wrangler.jsonc ให้มี D1 + R2 binding + SPA fallback:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "digivend",
"compatibility_date": "2026-06-26",
"main": "./worker/index.ts",
"assets": { "not_found_handling": "single-page-application" },
"d1_databases": [
{ "binding": "DB", "database_name": "digivend-db", "database_id": "<ใส่ id จากตอน create>", "migrations_dir": "drizzle" }
],
"r2_buckets": [
{ "binding": "PRODUCTS_BUCKET", "bucket_name": "digivend-files" }
]
}secret ฝั่ง dev ใส่ใน .dev.vars (อย่า commit):
PROMPTPAY_ID=0812345678
SLIPOK_API_KEY=xxxxxxxx
SLIPOK_BRANCH_ID=12345
§04โครงเรื่อง: 5 Checkpoint
ทุก checkpoint จบด้วย "ของที่เห็นได้" และทดสอบได้ ถ้า checkpoint ไหนพัง ห้ามข้าม
- ✅ Storefront — หน้าร้านโชว์สินค้าที่ seed ไว้ เป็น grid สวยๆ
- ✅ สร้าง Order — กดซื้อแล้วเกิด order ใน D1 โดย ราค amount ตัดสินที่ server ไม่ใช่ client
- ✅ จ่ายเงิน — โชว์ PromptPay QR → อัปสลิป → SlipOK verify → order เป็น paid (สลิปซ้ำโดนปฏิเสธ)
- ✅ ปลดล็อกไฟล์ — จ่ายแล้วปุ่ม Download โผล่ และ route โหลดไฟล์เช็ค paid ก่อนเสมอ
- ✅ Deploy — แอปขึ้น URL จริง (🎬 the money shot)
§05Checkpoint 1: Storefront
เป้าหมาย
มี API /api/products กับหน้าร้านที่โชว์สินค้าที่ seed ไว้ — การ์ดสินค้าเรียงเป็น grid ตาม DESIGN.md
ขั้นตอน
1.1 เปิด Claude Code แล้วให้มันทำงาน
ใน Claude Code prompt:
อ่าน
CLAUDE.md,EP1-SPEC.md,DESIGN.mdก่อน แล้วทำ Checkpoint 1 (Storefront): สร้าง Hono routeGET /api/productsกับGET /api/products/:slugอ่านจาก D1 ผ่าน Drizzle ส่งกลับเฉพาะ field ปลอดภัย (id, slug, name, description, price, coverEmoji) ห้ามส่งr2Keyเด็ดขาด แล้วทำหน้า catalog (/) เป็น grid การ์ดสินค้า + หน้า product (/p/:slug) ตาม DESIGN.md
✅ วิธีทดสอบ Checkpoint 1
pnpm run devแล้วเปิด/— เห็นสินค้า 4 ชิ้นเป็น grid สวยๆ- คลิกสินค้า → เข้าหน้า product
- เปิด
/api/productsตรงๆ → ใน response ต้องไม่มีr2Key
🎬 Camera moment: scroll หน้าร้านให้เห็นว่า "นี่มันร้านจริงๆ นะ" — การ์ดสวย ราคาชัด
❌ ถ้าพัง — debug ตามลำดับ
- หน้าโล่ง / ไม่มีสินค้า → seed ยังไม่เข้า D1 local รัน seed (
wrangler d1 execute digivend-db --file ./seed/seed.sql --local) ใหม่ /api/productserror → เช็ค bindingDBในwrangler.jsoncกับdatabase_id
§06Checkpoint 2: สร้าง Order (amount ตัดสินที่ server)
เป้าหมาย
กดปุ่มซื้อแล้วเกิด order pending ใน D1 โดย amount ก๊อปมาจาก products.price ฝั่ง server — ส่งไปที่หน้า /order/:id
ขั้นตอน
2.1 ใน Claude Code:
ทำ Checkpoint 2: สร้าง
POST /api/ordersรับแค่{ productId }แล้ว server ไปอ่าน price ของสินค้านั้นเอง สร้าง order statuspendingamount = price ส่งกลับ{ orderId }ห้ามเชื่อ amount จาก body. เพิ่มGET /api/orders/:idคืน{ id, status, amount, productName }. ปุ่ม Buy ในหน้า product เรียก API นี้แล้ว route ไป/order/:idที่โชว์"รอชำระเงิน — ฿{amount}"
✅ วิธีทดสอบ Checkpoint 2
- กด Buy → เด้งไปหน้า order เห็นราคาถูกต้อง
- เช็ค D1 มี row order
pendingamount = ราคาสินค้า - ลองยิง
POST /api/ordersใส่amountมั่วๆ ใน body → amount ที่เก็บยังเท่าราคาจริง (server ไม่สนใจ body)
❌ ถ้าพัง
- amount เพี้ยน → แปลว่ายังอ่าน amount จาก client อยู่ ให้ตัดออก อ่านจาก product row อย่างเดียว
§07Checkpoint 3: จ่ายเงินด้วย PromptPay QR + SlipOK
เป้าหมาย
โชว์ QR ตามยอด order → อัปสลิป → verify ผ่าน SlipOK → order เป็น paid โดยใช้ slipok skill (ไม่เขียน QR/verify เอง)
ขั้นตอน
3.1 ใน Claude Code:
ทำ Checkpoint 3 ตาม slipok skill: implement
OrderProviderทับตารางorders(getOrder,reserveTransRefแบบ atomic คืน false ถ้า transRef ซ้ำ,markPaidเซ็ต paid + paidAt + transRef) แล้ว mountapp.route('/api/pay', createPaymentApp({ getProvider, getConfig }))โดยgetConfigอ่านPROMPTPAY_ID,SLIPOK_API_KEY,SLIPOK_BRANCH_IDจากc.env. หน้า order render<PaymentPanel orderId={...} />. ยอดที่ส่งเข้า QR/verify ใช้order.amountจาก server เท่านั้น
✅ วิธีทดสอบ Checkpoint 3
- หน้า order โชว์ QR PromptPay ตามยอด
- จ่ายเงินจริงให้ตัวเองสัก ฿49 แล้วอัปสลิป → order เป็น
paid - อัป สลิปเดิมซ้ำ → โดนปฏิเสธ (transRef ซ้ำ)
- อัปสลิปที่ยอด ไม่ตรง → โดนปฏิเสธ (amount ไม่ match)
🎬 Camera moment: สแกน QR ในแอปธนาคารจริง → จ่าย → อัปสลิป → สถานะเด้งเป็น paid
❌ ถ้าพัง
- verify ไม่ผ่านทั้งที่จ่ายแล้ว → เช็ค
SLIPOK_API_KEY/BRANCH_IDใน.dev.vars - สลิปซ้ำไม่โดนกัน → เช็คว่า
orders.transRefมี UNIQUE index จริง
§08Checkpoint 4: ปลดล็อกไฟล์ (the PDF unlock)
เป้าหมาย
order ที่ paid ถึงจะโหลดไฟล์ได้ — route โหลดเช็ค paid ฝั่ง server ก่อน stream จาก R2
ขั้นตอน
4.1 ใน Claude Code:
ทำ Checkpoint 4: สร้าง
GET /api/orders/:id/download— โหลด order ถ้าstatus !== 'paid'ตอบ 403; ถ้า paid ให้env.PRODUCTS_BUCKET.get(product.r2Key)แล้ว streamobject.bodyพร้อม headerContent-Type: application/pdfและContent-Disposition: attachment. ถ้า object เป็น null ตอบ 404 (อย่า 500). หน้า order พอ paid ให้สลับ PaymentPanel เป็น success state + ปุ่ม Download ที่ยิง route นี้
✅ วิธีทดสอบ Checkpoint 4
- หลังจ่าย → ปุ่ม Download โผล่ → โหลดได้ไฟล์ PDF จริง
- เอา order ที่ยัง
pendingมายิง/api/orders/{id}/downloadตรงๆ → ได้ 403 ไม่มีไฟล์หลุด
🎬 Camera moment: พอ verify ผ่าน ปุ่ม Download โผล่ปุ๊บ กดแล้วไฟล์ลงมาเลย — นี่คือ "the drop"
❌ ถ้าพัง
- ยัง pending แต่โหลดได้ → แปลว่าเช็ค paid แค่ซ่อนปุ่ม ต้องเช็คใน route ฝั่ง server ด้วย
- ไฟล์ 404 → R2 local ยังไม่มีไฟล์ อัปไฟล์ตัวอย่างเข้า bucket local ก่อน
§09Checkpoint 5: Deploy — 🏆 The Money Shot
ถึงจุดที่ทุก episode สร้างมาเพื่อสิ่งนี้: เอาแอปขึ้น internet จริง
ขั้นตอน
# ใส่ secret ฝั่ง production
wrangler secret put PROMPTPAY_ID
wrangler secret put SLIPOK_API_KEY
wrangler secret put SLIPOK_BRANCH_ID
# apply schema + seed ขึ้น remote D1
wrangler d1 migrations apply digivend-db --remote
wrangler d1 execute digivend-db --file ./seed/seed.sql --remote
# อัป PDF ตัวอย่างขึ้น remote R2 (ทำทุกไฟล์)
wrangler r2 object put digivend-files/products/notion-pack.pdf --file ./seed/notion-pack.pdf --remote
# ... อัปไฟล์ที่เหลือให้ครบ
pnpm run deploy✅ วิธีทดสอบ
- เปิด URL
*.workers.devที่ deploy ให้ - รัน loop เต็มๆ บน URL จริง: เปิดร้าน → ซื้อ → สแกน QR → จ่าย → อัปสลิป → โหลดไฟล์
- refresh หน้า
/order/:idบน URL จริง → ยังเปิดได้ (SPA fallback ทำงาน)
🎬 The money shot ของ episode: ปุ่ม Download โผล่พร้อมไฟล์เด้งลงมา โดย address bar เป็น URL จริงของ Cloudflare ไม่ใช่ localhost
§10สรุป ก้าวต่อไป และค่าใช้จ่าย
เราสร้างร้านขายของดิจิทัลที่จ่ายด้วย PromptPay จริง แล้ว deploy ขึ้น Cloudflare ได้ใน 5 checkpoint:
- ✅ หน้าร้าน + ซื้อ + จ่ายด้วย QR จริง (SlipOK verify + กันสลิปซ้ำ)
- ✅ ไฟล์ปลดล็อกเฉพาะ order ที่จ่ายแล้ว (gate ฝั่ง server)
- ✅ ขึ้น URL จริงใช้ได้เลย
ค่าใช้จ่าย / การดูแล:
- อยู่ใน free tier ของ Workers + D1 + R2 ได้สบายๆ สำหรับร้านเล็ก (R2 free tier ใจดีมาก, D1 ก็พอ)
- SlipOK มีค่าบริการตาม plan ของเขา — เช็คที่ slipok.com
- อัปเดต: แก้โค้ดแล้ว
pnpm run deployใหม่ ก็ขึ้นทับเลย
สิ่งที่ได้เรียนจาก episode นี้:
- server-side gate ของไฟล์ดิจิทัล — ของที่ขายต้องไม่หลุดก่อนจ่าย เช็ค paid ใน route ไม่ใช่แค่ซ่อนปุ่ม
- amount ต้องมาจาก server เสมอ ไม่งั้นลูกค้าจ่าย 1 บาทแล้วเคลมของเต็มได้
- เอา R2 มาเก็บไฟล์แล้วให้ Worker stream แบบมีเงื่อนไข — pattern นี้ใช้ซ้ำได้กับของดิจิทัลทุกแบบ
Episode ถัดไป
- 🎯 EP2 — หลังร้านคนขาย: login (Better Auth), เพิ่ม/แก้สินค้า, อัป PDF เอง, ดู order ที่จ่ายแล้ว → ขยับเป็น Tier 3 เต็มตัว (เพิ่ม auth)
- 🎯 EP3 — สินค้าแบบโค้ด/ไอดี: stock เป็น pool ของโค้ดไม่ซ้ำ จ่ายแล้วได้โค้ด 1 อัน หมดแล้วขึ้น "sold out"
Resources
- 🔗 Code: github.com/paepuripat/digivend
- 🔗 Cloudflare Workers + R2 + D1 docs: developers.cloudflare.com
- 🔗 SlipOK: slipok.com
- 🔗 Claude Code docs: docs.claude.com
💬 ถ้ามีคำถาม comment ใต้วิดีโอ หรือเปิด issue ใน GitHub repo ครับ
Engineer, writer, occasional GPU-melter. Writes every Sunday about applied AI — agents, evals, and whatever broke that week.
If you got something out of this, share it with someone who's trying to ship their first agent.