PPURIPATISSUE №47
HOME/BLOG/POST_01 · DIGIVEND EP1 — PROMPTPAY STOREFRONT
↩ BACK

DigiVend EP1 — PromptPay Storefront

สร้าง MVP ร้านขายของดิจิทัล (ขายไฟล์ PDF) ที่ลูกค้าสแกน QR จ่ายเงิน อัปสลิป แล้วได้ไฟล์ทันที — build กับ Claude Code แล้วชิปขึ้น Cloudflare จริงใน episode เดียว

BY
PURIPAT
PUBLISHED
2026.06.29
READ TIME
9 MIN
WORDS
1,704
GHXIN
FIG. 01 — DigiVend, live on Cloudflare Workers.SOURCE: AUTHOR
จอ "จ่ายเสร็จ" บน URL จริงของ Cloudflare — ปุ่ม Download โผล่ + ไฟล์ PDF เด้งลงแถบดาวน์โหลด
⧉ MISSING SCREENSHOTจอ "จ่ายเสร็จ" บน URL จริงของ Cloudflare — ปุ่ม Download โผล่ + ไฟล์ PDF เด้งลงแถบดาวน์โหลดdrop file at public/images/posts/digivend-ep1/01-hero-live.png
FIG. 01 — Live on *.workers.dev, right after deploy. (also used as cover + thumbnail)

§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 Workersdeploy ง่าย, free tier ใจดี, ทุก EP ใช้ตัวเดียวกันไม่ใช้ Pages เพราะ Cloudflare ดัน Workers สำหรับโปรเจกต์ใหม่แล้ว
Vite + Reactdev เร็ว, มาตรฐาน SPA
@cloudflare/vite-pluginรัน Worker ใน dev server เดียวกัน — ไม่ต้องเจอ CORSไม่รัน wrangler แยกพอร์ตให้ปวดหัวเรื่อง CORS
HonoAPI 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 ไหนพัง ห้ามข้าม

  1. Storefront — หน้าร้านโชว์สินค้าที่ seed ไว้ เป็น grid สวยๆ
  2. สร้าง Order — กดซื้อแล้วเกิด order ใน D1 โดย ราค amount ตัดสินที่ server ไม่ใช่ client
  3. จ่ายเงิน — โชว์ PromptPay QR → อัปสลิป → SlipOK verify → order เป็น paid (สลิปซ้ำโดนปฏิเสธ)
  4. ปลดล็อกไฟล์ — จ่ายแล้วปุ่ม Download โผล่ และ route โหลดไฟล์เช็ค paid ก่อนเสมอ
  5. 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 route GET /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

  1. pnpm run dev แล้วเปิด / — เห็นสินค้า 4 ชิ้นเป็น grid สวยๆ
  2. คลิกสินค้า → เข้าหน้า product
  3. เปิด /api/products ตรงๆ → ใน response ต้องไม่มี r2Key
หน้าร้าน grid สินค้า 4 ชิ้น
⧉ MISSING SCREENSHOTหน้าร้าน grid สินค้า 4 ชิ้นdrop file at public/images/posts/digivend-ep1/02-catalog.png
FIG. 02 — Catalog grid, right after Checkpoint 1.

🎬 Camera moment: scroll หน้าร้านให้เห็นว่า "นี่มันร้านจริงๆ นะ" — การ์ดสวย ราคาชัด

❌ ถ้าพัง — debug ตามลำดับ

  • หน้าโล่ง / ไม่มีสินค้า → seed ยังไม่เข้า D1 local รัน seed (wrangler d1 execute digivend-db --file ./seed/seed.sql --local) ใหม่
  • /api/products error → เช็ค binding DB ใน 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 status pending amount = price ส่งกลับ { orderId } ห้ามเชื่อ amount จาก body. เพิ่ม GET /api/orders/:id คืน { id, status, amount, productName }. ปุ่ม Buy ในหน้า product เรียก API นี้แล้ว route ไป /order/:id ที่โชว์ "รอชำระเงิน — ฿{amount}"

✅ วิธีทดสอบ Checkpoint 2

  1. กด Buy → เด้งไปหน้า order เห็นราคาถูกต้อง
  2. เช็ค D1 มี row order pending amount = ราคาสินค้า
  3. ลองยิง POST /api/orders ใส่ amount มั่วๆ ใน body → amount ที่เก็บยังเท่าราคาจริง (server ไม่สนใจ body)
หน้า order "รอชำระเงิน" + จำนวนเงิน
⧉ MISSING SCREENSHOTหน้า order "รอชำระเงิน" + จำนวนเงินdrop file at public/images/posts/digivend-ep1/03-order-pending.png
FIG. 03 — Order pending, amount shown.

❌ ถ้าพัง

  • 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) แล้ว mount app.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

  1. หน้า order โชว์ QR PromptPay ตามยอด
  2. จ่ายเงินจริงให้ตัวเองสัก ฿49 แล้วอัปสลิป → order เป็น paid
  3. อัป สลิปเดิมซ้ำ → โดนปฏิเสธ (transRef ซ้ำ)
  4. อัปสลิปที่ยอด ไม่ตรง → โดนปฏิเสธ (amount ไม่ match)
QR + ช่องอัปสลิป
⧉ MISSING SCREENSHOTQR + ช่องอัปสลิปdrop file at public/images/posts/digivend-ep1/04-qr.png
FIG. 04 — PromptPay QR + slip upload.
สถานะเปลี่ยนเป็น "จ่ายแล้ว ✅"
⧉ MISSING SCREENSHOTสถานะเปลี่ยนเป็น "จ่ายแล้ว ✅"drop file at public/images/posts/digivend-ep1/05-paid.png
FIG. 05 — Status flips to paid.

🎬 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) แล้ว stream object.body พร้อม header Content-Type: application/pdf และ Content-Disposition: attachment. ถ้า object เป็น null ตอบ 404 (อย่า 500). หน้า order พอ paid ให้สลับ PaymentPanel เป็น success state + ปุ่ม Download ที่ยิง route นี้

✅ วิธีทดสอบ Checkpoint 4

  1. หลังจ่าย → ปุ่ม Download โผล่ → โหลดได้ไฟล์ PDF จริง
  2. เอา order ที่ยัง pending มายิง /api/orders/{id}/download ตรงๆ → ได้ 403 ไม่มีไฟล์หลุด
ปุ่ม Download + ไฟล์เด้งลงแถบดาวน์โหลด
⧉ MISSING SCREENSHOTปุ่ม Download + ไฟล์เด้งลงแถบดาวน์โหลดdrop file at public/images/posts/digivend-ep1/06-download.png
FIG. 06 — The drop: file lands the instant verify passes.

🎬 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

✅ วิธีทดสอบ

  1. เปิด URL *.workers.dev ที่ deploy ให้
  2. รัน loop เต็มๆ บน URL จริง: เปิดร้าน → ซื้อ → สแกน QR → จ่าย → อัปสลิป → โหลดไฟล์
  3. refresh หน้า /order/:id บน URL จริง → ยังเปิดได้ (SPA fallback ทำงาน)
loop จบบน URL จริง — address bar เห็น workers.dev ตอนปุ่ม Download โผล่
⧉ MISSING SCREENSHOTloop จบบน URL จริง — address bar เห็น workers.dev ตอนปุ่ม Download โผล่drop file at public/images/posts/digivend-ep1/07-live-shot.png
FIG. 07 — ⭐ The money shot: full loop, live on workers.dev.

🎬 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 ครับ

— FIN —P. 01 / 01 · 1,704 WORDS
WRITTEN BY
P
Puripat
@PURIPAT · ENG / WRITER

Engineer, writer, occasional GPU-melter. Writes every Sunday about applied AI — agents, evals, and whatever broke that week.

ALL POSTS →
SHARE THIS

If you got something out of this, share it with someone who's trying to ship their first agent.