PPURIPAT
HOME/BLOG
↩ BACK

DigiVend สร้างร้านขายสินค้า Digital จ่าย QR ได้ไฟล์เลย

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

BY
PURIPAT
PUBLISHED
2026.06.29
READ TIME
8 MIN
WORDS
1,583
GHXIN
DigiVend สร้างร้านขายสินค้า Digital จ่าย QR ได้ไฟล์เลย
FIG. 01 — DigiVend, live on Cloudflare Workers.SOURCE: AUTHOR
จอ "จ่ายเสร็จ" บน URL จริงของ Cloudflare — ปุ่ม Download โผล่ + ไฟล์ PDF เด้งลงแถบดาวน์โหลด
จอ "จ่ายเสร็จ" บน URL จริงของ Cloudflare — ปุ่ม Download โผล่ + ไฟล์ PDF เด้งลงแถบดาวน์โหลด
FIG. 01 — Live Demo on https://digivend.puripat.workers.dev/

§01สิ่งที่เราจะสร้าง

DigiVend คือร้านขายของดิจิทัล — รอบนี้ขาย ไฟล์ PDF ก่อน (พวก ebook, template, คู่มือ) loop หลักคือ:

  • ลูกค้า เปิดดูสินค้า ในร้าน → กด ซื้อ
  • ระบบสร้าง order แล้วโชว์ PromptPay QR ตามราคาสินค้า → ลูกค้าสแกนจ่ายในแอปธนาคาร
  • ลูกค้า อัปสลิป → backend ส่งไป verify กับ SlipOK → order กลายเป็น paid
  • พอจ่ายแล้ว ปุ่ม Download ถึงจะโผล่ และโหลดไฟล์จริงได้

จุดที่ทำให้ร้าน "ดิจิทัล" ต่างจากร้านทั่วไป: ไม่มีการส่งของ — ไฟล์จะถูกปลดล็อกตอนที่ backend ยืนยันว่าจ่ายเงินแล้วเท่านั้น และต้องไม่หลุดไปถึง browser ก่อนจ่าย อันนี้คือหัวใจของ episode เลย

Demo สุดท้าย

สแกน QR → จ่ายเงินในแอปธนาคาร → อัปสลิป → ระบบ verify ผ่าน → ปุ่ม Download โผล่ ไฟล์ PDF เด้งลงมา ทั้งหมดนี้รันอยู่บน URL จริงของ Cloudflare

สิ่งที่เราตัดทิ้งโดยตั้งใจ (สำคัญ — ต้องเข้าใจ scope)

  • ระบบ login / หลังร้านของคนขาย — รอบนี้สินค้า seed ไว้ก่อน (ใส่ผ่าน script) ส่วน admin ที่ login เพิ่มสินค้าได้ไว้ทำทีหลัง
  • สินค้าแบบ "โค้ด/ไอดี/พาสเวิร์ด" (license key, account login) ที่ต้องมี stock เป็น pool — ไว้ทำใน ep ถัดๆไป (ถ้ามีคนดู)
  • ตะกร้าหลายชิ้น, ประวัติคำสั่งซื้อ, ใบเสร็จส่งอีเมล, อัปโหลดรูปสินค้า — ยังไม่ใช่ตอนนี้

💡 ทำไมต้องตัด: รอบนี้โฟกัสที่ core loop เดียว — ซื้อ → จ่ายด้วย QR จริง → ปลดล็อกไฟล์ — แล้ว deploy ขึ้นจริง การมี admin หรือ stock pool ทำให้งานซับซ้อน, เน้นโพกัสสร้างสิ่งสำคัญก่อนแล้วค่อยต่อยอด


§02Stack ที่ใช้ และเหตุผล

Toolทำไมเลือกทำไมไม่ใช่ตัวอื่น
Cloudflare Workersdeploy ง่าย, free tier ใจดีHosting ง่ายและฟรี จบในที่ัเดียวไม่ต้องไปจัดการหลายที่
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 (promptpay-qr + SlipOK)flow "สแกนจ่าย + เช็คสลิป" สำเร็จรูป เขียนครั้งเดียวใช้ซ้ำทุกแอปไม่เขียน QR/verify เองทุกครั้ง

ที่เราข้าม:

  • Better Auth — ยังไม่มี login ตอนนี้ (ลูกค้าซื้อแบบ guest) จะเพิ่มทีหลัง
  • ❌ 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

ตั้งค่า 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 ชิ้น
หน้าร้าน grid สินค้า 4 ชิ้น
FIG. 02 — Catalog grid, right after Checkpoint 1.

❌ ถ้าพัง — 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 "รอชำระเงิน" + จำนวนเงิน
หน้า order "รอชำระเงิน" + จำนวนเงิน
FIG. 03 — Order detail, 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 + ช่องอัปสลิป
QR + ช่องอัปสลิป
FIG. 04 — PromptPay QR + slip upload.
สถานะเปลี่ยนเป็น "จ่ายแล้ว ✅"
สถานะเปลี่ยนเป็น "จ่ายแล้ว ✅"
FIG. 05 — Status flips to paid.

** สแกน 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/06-download.png
FIG. 06 — The drop: file lands the instant verify passes.

** พอ verify ผ่าน ปุ่ม Download โผล่ปุ๊บ กดแล้วไฟล์ลงมาเลย

❌ ถ้าพัง

  • ยัง 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 ทำงาน)

** ปุ่ม 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 ใหม่ ก็ขึ้นทับเลย

สิ่งที่ได้เรียนจากรอบนี้:

  • server-side gate ของไฟล์ดิจิทัล — ของที่ขายต้องไม่หลุดก่อนจ่าย เช็ค paid ใน route ไม่ใช่แค่ซ่อนปุ่ม
  • amount ต้องมาจาก server เสมอ ไม่งั้นลูกค้าจ่าย 1 บาทแล้วเคลมของเต็มได้
  • เอา R2 มาเก็บไฟล์แล้วให้ Worker stream แบบมีเงื่อนไข — pattern นี้ใช้ซ้ำได้กับของดิจิทัลทุกแบบ

สิ่งที่ทำต่อได้

  • 🎯 หลังร้านคนขาย: login (Better Auth), เพิ่ม/แก้สินค้า, อัป PDF เอง, ดู order ที่จ่ายแล้ว → ขยับเป็น Tier 3 เต็มตัว (เพิ่ม auth)
  • 🎯 สินค้าแบบโค้ด/ไอดี: 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,583 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.

SHARE THIS

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

NEXT →
02 · แจกระบบจดรายรับ-รายจ่ายผ่าน LINE ฟรี ติดตั้งเองได้ใน 5 นาที
2026.07.08