สารบัญ
Back to Expense Buddy

Expense Buddy Source Code Setup Guide

เอกสารคู่มือสำหรับ ตั้งค่า และการ deploy สำหรับ Expense Buddy ด้วยตัวเอง

ใช้ AI ช่วย setup ได้เลย

ถ้าไม่ถนัด Terminal หรืออยากให้ AI พาทำทีละขั้นตอน copy prompt ด้านล่างไปวางได้เลย:

ภาษาไทย

อ่านไฟล์ SETUP.md แล้วช่วย setup และ deploy โปรเจกต์นี้ทีละขั้นตอน
ทำทีละ step — ถามหรือรอให้ฉันทำเสร็จก่อนไป step ถัดไปเสมอ

ภาษาอังกฤษ

Read SETUP.md and help me set up and deploy this project step by step.
Guide me one step at a time — ask for my input or confirmation before moving to the next step.

0) สิ่งที่ต้องมีล่วงหน้า

  • บัญชี Cloudflare (Workers + D1)
  • LINE Official Account (OA) ที่ใช้งานอยู่
  • บัญชี LINE Developers Console และ LINE OA Manager
  • บัญชี Stripe (สำหรับ Pro/ชำระเงิน) (Optional: หากใช้คนเดียว ไม่ต้องเปิด feature pro ก็ได้)
  • Bun หรือ Node.js
  • โดเมน HTTPS สำหรับ Production (หรือใช้ *.workers.dev ที่ได้จาก Cloudflare ฟรี)

อธิบายเกี่ยวกับ link ของ LINE:

  • LINE Developers Console - เอาไว้สร้าง Provider, Channel
  • LINE OA Manager - จัดการ LINE OA ของคุณ เช่น Chat, Rich Menu เพื่อเปิด LIFF

1) โครงสร้างที่ต้องเข้าใจก่อน

ระบบนี้ใช้ LINE 2 ช่องทางแยกกัน:

  1. Messaging API channel (สร้างผ่าน https://entry.line.biz/form/entry/unverified)
  • ใช้กับ OA, webhook, bot message
  1. LINE Login channel
  • ใช้กับ LIFF และ token ของ LIFF

สรุป: OA/Bot กับ LIFF เป็นคนละ channel แต่ใช้งานร่วมกันได้

2) ติดตั้งโปรเจกต์

Terminal window
bun install

3) ตั้งค่า Build-time env (ฝั่งหน้าเว็บ)

สร้าง/แก้ .env สำหรับ local หรือใส่ใน Cloudflare Build Variables:

Terminal window
PUBLIC_LIFF_ID=REPLACE_WITH_LIFF_ID
PUBLIC_LINE_OA_URL=https://line.me/R/ti/p/@YOUR_OA_BASIC_ID

หมายเหตุ:

  • PUBLIC_LIFF_ID ใช้ค่า LIFF ID เท่านั้น เช่น 200925xxxx-Udad3xxx ไม่ใช่ URL เต็ม (วิธีการสร้าง ดู Step 5.2)
  • PUBLIC_LINE_OA_URL เอาจาก OA Basic ID ที่ LINE OA Manager
  • ถ้ายังไม่มีค่าเหล่านี้ ให้ข้ามไป Step 5 ก่อน แล้วกลับมาเติมที่นี่ทีหลัง

4) ตั้งค่า Cloudflare D1, Queue และ Wrangler

ก่อนรัน wrangler command ใดๆ ต้อง login ก่อน:

Terminal window
wrangler login
# สามารถใช้ bun ได้ หากไม่ได้ตั้งค่า PATH
# เช่น bun wrangler login

4.1 สร้าง D1

Terminal window
wrangler d1 create expense_buddy
# สามารถใช้ bun ได้ หากไม่ได้ตั้งค่า PATH
# เช่น bun wrangler d1 create expense_buddy

นำ database_id ที่ได้ไปใส่ใน wrangler.jsonc:

"d1_databases": [
{
"binding": "DB",
"database_name": "expense_buddy",
"database_id": "<REAL_DATABASE_ID>",
"migrations_dir": "drizzle/migrations"
}
]

4.2 รัน migration

Terminal window
bun run db:migrate:local
bun run db:migrate:remote

4.3 สร้าง Queue สำหรับวิเคราะห์รูป

ระบบใช้ Queue สำหรับงานวิเคราะห์รูป เพื่อให้ webhook ตอบกลับได้เร็ว และส่งผลลัพธ์ภายหลังด้วย pushMessage

Terminal window
wrangler queues create expense-buddy-image-analysis

binding ใน wrangler.jsonc มีอยู่แล้ว ไม่ต้องแก้ไฟล์ แค่สร้าง resource บน Cloudflare ด้วยคำสั่งข้างบนให้ตรงชื่อ

ตัวอย่าง config ที่อยู่ในไฟล์แล้ว:

"queues": {
"producers": [
{
"binding": "IMAGE_ANALYSIS_QUEUE",
"queue": "expense-buddy-image-analysis"
}
],
"consumers": [
{
"queue": "expense-buddy-image-analysis",
"max_batch_size": 1,
"max_batch_timeout": 5,
"max_retries": 3
}
]
}

หมายเหตุ:

  • queue คือชื่อ resource จริงบน Cloudflare
  • binding คือชื่อที่โค้ดใช้ใน Worker (env.IMAGE_ANALYSIS_QUEUE)
  • ถ้าเปลี่ยนชื่อ binding ใน config ต้องแก้โค้ดให้ตรงกันด้วย

4.4 สร้าง Queue และ R2 สำหรับ export CSV แบบ background

ระบบ export CSV ใช้ Queue + R2 เพื่อไม่ให้ LIFF ค้างระหว่างสร้างไฟล์

Terminal window
wrangler queues create expense-buddy-export-csv
wrangler r2 bucket create expense-buddy-export-csv

binding ใน wrangler.jsonc มีอยู่แล้ว ไม่ต้องแก้ไฟล์ แค่สร้าง resource บน Cloudflare ด้วยคำสั่งข้างบนให้ตรงชื่อ

ตัวอย่าง config ที่อยู่ในไฟล์แล้ว:

"r2_buckets": [
{
"binding": "EXPORT_CSV_BUCKET",
"bucket_name": "expense-buddy-export-csv"
}
],
"queues": {
"producers": [
{
"binding": "IMAGE_ANALYSIS_QUEUE",
"queue": "expense-buddy-image-analysis"
},
{
"binding": "EXPORT_CSV_QUEUE",
"queue": "expense-buddy-export-csv"
}
],
"consumers": [
{
"queue": "expense-buddy-image-analysis",
"max_batch_size": 1,
"max_batch_timeout": 5,
"max_retries": 3
},
{
"queue": "expense-buddy-export-csv",
"max_batch_size": 1,
"max_batch_timeout": 5,
"max_retries": 3
}
]
}

หมายเหตุ:

  • EXPORT_CSV_QUEUE ใช้สำหรับประมวลผล export CSV แบบ background
  • EXPORT_CSV_BUCKET ใช้เก็บไฟล์ CSV ชั่วคราว ระบบตั้ง retention 24 ชั่วโมง

4.5 ตั้งค่า Stripe สำหรับ Pro (ถ้าเปิดใช้)

  1. เข้า Stripe Dashboard แล้วสร้างสินค้า/ราคาแบบ one-time เช่น THB 990
  2. คัดลอก price_... มาใช้เป็น stripePriceId
  3. สร้าง Webhook endpoint:
https://<APP_BASE_URL>/api/stripe/webhook
  1. เลือก events อย่างน้อย:
  • checkout.session.completed
  • checkout.session.async_payment_succeeded

หมายเหตุ:

  • ระบบ unlock Pro เมื่อ payment_status = paid เท่านั้น
  • แนะนำเปิด event checkout.session.async_payment_failed เพิ่มเพื่อแจ้งเตือนกรณีจ่ายไม่สำเร็จ

สร้าง runtime types (แนะนำ)

Terminal window
bun run cf-typegen

5) เชื่อม LINE OA

ยังไม่มี LINE OA? ให้สร้างก่อนที่ https://entry.line.biz/form/entry/unverified แล้วกลับมาทำ Step นี้

5.1 ตั้ง Messaging API channel ให้ OA

  1. เข้า https://manager.line.biz/ เลือก Settings ด้านขวาบน -> Messaging API
  • 1.1 กรณียังไม่เคยมี ให้กด Enable Messaging API
  • 1.2 หากมี Messaging API อยู่แล้ว จะแสดง
Terminal window
Messaging API
Messaging API is an advanced feature for developers. It allows accounts to promote more interactive communication by sending and receiving messages and actions via the API.
What is Messaging API?
LINE Developers API documentation
Status Enabled
Channel info Channel ID xxxxxxxxxx
Channel secret xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Webhook URL https://{DOMAIN_NAME}/api/line/webhook

ค่าที่ต้อง copy จาก Messaging API channel:

LINE_CHANNEL_SECRET — copy จากหน้านี้ที่ช่อง Channel secret

LINE_CHANNEL_ACCESS_TOKEN — ค่านี้อยู่ใน LINE Developers Console (ไม่ใช่ OA Manager):

  1. เข้า https://developers.line.biz/console/
  2. เลือก Provider → เลือก Messaging API channel (ไม่ใช่ LINE Login)
  3. ไปที่แท็ป Messaging API
  4. เลื่อนลงหาหัวข้อ “Channel access token (long-lived)”
  5. กด Issue → copy token ที่ได้

หมายเหตุ: Channel ID ของ Messaging API ไม่ได้ใช้ในโปรเจกต์นี้ ไม่ต้อง copy

การตั้ง Webhook URL — ข้ามไปก่อน ยังไม่มี domain ตอนนี้ กลับมาตั้งหลัง deploy เสร็จใน Step 9 รูปแบบที่ต้องใส่คือ:

https://<APP_BASE_URL>/api/line/webhook

5.2 สร้าง LINE Login channel (แยกจาก Messaging API)

  1. เข้า developers.line.biz
  2. เลือก Provider เดียวกับ OA
  3. สร้าง LINE Login channel ใหม่ (Create a new channel)
  4. เลือกเป็น LINE Login 4.1 เลือก Region to provide the service เป็น Thailand 4.2 เลือก Company or owner’s country or region ใส่ Thailand 4.3 ทำการตั้งชื่อ Channel กำหนด icon 4.4 App types เลือกเป็น Web Apps
  5. เมื่อสร้างเสร็จ ที่แท็ป Basic Settings จะได้ Channel ID
    • copy ค่านี้ → ใช้เป็น LIFF_CHANNEL_ID
    • (Channel secret ของ LINE Login ไม่ได้ใช้ในโปรเจกต์นี้)
  6. ไปที่แท็ป LIFF กด Add เพื่อสร้าง LIFF ใหม่
  • 6.1 กำหนด LIFF Name ของเรา เลือกขนาด (Size) เป็น Full หรือใครชอบแบบ ไม่เต็ม หรือครึ่งจอ ก็เลือก Tall หรือ Compact ก็ได้
  • 6.2 ตั้ง Endpoint URL เป็น https://<APP_BASE_URL>/liff
  • 6.3 Scope เลือก profile และ openid (ต้องเลือกทั้งสองตัว)
  1. เมื่อสร้าง LIFF เสร็จ จะได้ LIFF ID เช่น 2009250992-Udad3StM
    • copy ค่านี้ → ใช้เป็นทั้ง PUBLIC_LIFF_ID (ใน .env) และ LIFF_ID (runtime var)

สรุปค่าที่ได้จาก Step 5 ทั้งหมด:

ค่าที่ได้ มาจากไหน ใช้เป็น variable
Channel Secret (Messaging API) OA Manager → Messaging API LINE_CHANNEL_SECRET
Channel Access Token (Messaging API) LINE Developers → Messaging API tab → Issue LINE_CHANNEL_ACCESS_TOKEN
Channel ID (LINE Login) LINE Developers → LINE Login channel → Basic Settings LIFF_CHANNEL_ID
LIFF ID LINE Developers → LINE Login channel → LIFF tab LIFF_ID และ PUBLIC_LIFF_ID

6) ตั้งค่า runtime vars และ secrets

ใน Worker ต้องมีค่าหลักดังนี้:

vars (ไม่ลับ):

  • AI_MODEL
  • LIFF_CHANNEL_ID เป็นค่าที่ไม่ลับ สามารถเก็บเป็น vars ได้
  • LIFF_ID ค่าเดียวกับ PUBLIC_LIFF_ID เช่น 2009250992-Udad3StM

secrets (ลับ):

  • LINE_CHANNEL_SECRET — มาจาก Messaging API channel (Step 5.1)
  • LINE_CHANNEL_ACCESS_TOKEN — มาจาก Messaging API channel → Issue access token (Step 5.1)
  • SESSION_SECRET — random string สำหรับ sign session cookie สร้างได้ด้วย: openssl rand -base64 32
  • STRIPE_SECRET_KEY จำเป็นเมื่อเปิด Pro
  • STRIPE_WEBHOOK_SECRET จำเป็นเมื่อเปิด Pro

6.1 ตั้ง secret แบบปกติ

Terminal window
wrangler secret put LINE_CHANNEL_SECRET
wrangler secret put LINE_CHANNEL_ACCESS_TOKEN
wrangler secret put SESSION_SECRET
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET

6.2 ถ้าใช้ Workers Versions/Deployments flow

ถ้าเจอข้อความว่าห้าม secret put ตรงๆ ให้ใช้:

Terminal window
wrangler versions secret put LINE_CHANNEL_SECRET
wrangler versions secret put LINE_CHANNEL_ACCESS_TOKEN
wrangler versions secret put SESSION_SECRET
wrangler versions secret put STRIPE_SECRET_KEY
wrangler versions secret put STRIPE_WEBHOOK_SECRET
wrangler versions deploy

7) Local dev flow

Terminal window
bun run db:generate
bun run db:migrate:local
bun run dev:seed -- --count 20
bun run dev

หน้า dev login:

http://127.0.0.1:4321/dev/login

8) เพิ่ม LIFF URL เข้า LINE OA

เมื่อ Endpoint URL เป็น https://<APP_BASE_URL>/liff ให้ใช้ลิงก์แบบ endpoint-relative:

https://liff.line.me/<LIFF_ID>/dashboard

โดย <LIFF_ID> คือรหัสอย่างเดียว เช่น 2009250000-Udad3StM

ไม่ต้องใส่ https://liff.line.me/ ลงในตัวแปร PUBLIC_LIFF_ID หรือ LIFF_ID

ตัวอย่างลิงก์ที่ใช้ใน Rich Menu / Flex:

https://liff.line.me/<LIFF_ID>/dashboard
https://liff.line.me/<LIFF_ID>/settings
https://liff.line.me/<LIFF_ID>/history
https://liff.line.me/<LIFF_ID>/add

ใส่ลิงก์นี้ใน:

  • Rich Menu
  • ปุ่มในข้อความ
  • เมนู OA

ไม่จำเป็นต้องสร้างหลาย LIFF app สำหรับหน้า add/history/dashboard/settings เพราะในแอปมี navigation อยู่แล้ว

ตัวอย่าง การตั้งค่า

ไปที่ LINE OA Manager -> Chat Screen -> Rich Menu -> Create New

// เลือก template และ link เช่น
Link - https://liff.line.me/{LIFF_ID}/dashboard
Link - https://liff.line.me/{LIFF_ID}/settings

9) การ Deploy

มี 2 วิธี เลือกอย่างใดอย่างหนึ่ง:

วิธีที่ 1 — Deploy จากเครื่องตัวเองด้วย Wrangler

ก่อน deploy ต้องเตรียม 3 ส่วน:

ส่วนที่ 1 — Build-time env (อ่านจาก .env ตอน bun run build)

ไฟล์ .env ในโปรเจกต์:

Terminal window
PUBLIC_LIFF_ID=2009250992-Udad3StM
PUBLIC_LINE_OA_URL=https://line.me/R/ti/p/@YOUR_OA_BASIC_ID

ส่วนที่ 2 — Runtime vars (อยู่ใน wrangler.jsonc แล้ว deploy ขึ้นไปพร้อมกัน)

แก้ค่าใน wrangler.jsonc:

"vars": {
"AI_MODEL": "@cf/google/gemma-4-26b-a4b-it",
"LIFF_ID": "YOUR_LIFF_ID",
"LIFF_CHANNEL_ID": "YOUR_LINE_LOGIN_CHANNEL_ID"
}

ค่าเหล่านี้ไม่ใช่ความลับ เก็บใน wrangler.jsonc ได้เลย

ส่วนที่ 3 — Secrets (ต้อง wrangler secret put แยกต่างหาก)

.env และ .dev.vars ไม่ได้ ถูก upload ขึ้น Cloudflare — secrets ต้องตั้งด้วยคำสั่งนี้ (ทำครั้งเดียว):

Terminal window
wrangler secret put LINE_CHANNEL_SECRET
wrangler secret put LINE_CHANNEL_ACCESS_TOKEN
wrangler secret put SESSION_SECRET
# ถ้าเปิด Pro:
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET

Deploy:

Terminal window
bun run build
bun run deploy

วิธีที่ 2 — Deploy ผ่าน Cloudflare Git Integration (PR merge → auto deploy)

เตรียม GitHub repo ก่อน (ถ้ายังไม่มี)

โค้ดต้องอยู่บน GitHub ก่อน — ตั้งเป็น private repo ได้:

Terminal window
git init
git add .
git commit -m "initial commit"
# สร้าง repo ที่ github.com แล้ว:
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main

สร้าง Worker ใน Cloudflare Dashboard

  1. เข้า Cloudflare DashboardWorkers & Pages
  2. กด Create application
  3. เลือก Pages แท็ป → กด Connect to Git
  4. เชื่อม GitHub account → เลือก repo ของคุณ
  5. ตั้งค่า build:
    • Framework preset: None (หรือ Astro ถ้ามีให้เลือก)
    • Build command: bun run build
    • Build output directory: dist
  6. กด Save and Deploy

ส่วนที่ 1 — Build-time env ตั้งใน Cloudflare Dashboard:

Workers & Pages → โปรเจกต์ → Settings → Environment Variables → Add variable (ไม่ต้อง encrypt):

  • PUBLIC_LIFF_ID
  • PUBLIC_LINE_OA_URL

ส่วนที่ 2 — Runtime vars อยู่ใน wrangler.jsonc แล้ว deploy ขึ้นไปพร้อม commit อัตโนมัติ ไม่ต้องตั้งใน Dashboard

ส่วนที่ 3 — Secrets ตั้งผ่าน wrangler secret put จากเครื่องของคุณ (ทำครั้งเดียว):

Terminal window
wrangler secret put LINE_CHANNEL_SECRET
wrangler secret put LINE_CHANNEL_ACCESS_TOKEN
wrangler secret put SESSION_SECRET
# ถ้าเปิด Pro:
wrangler secret put STRIPE_SECRET_KEY
wrangler secret put STRIPE_WEBHOOK_SECRET

หรือตั้งใน Dashboard → Settings → Environment Variables → Add variable → เลือก Encrypt

checklist ก่อน deploy (ทั้ง 2 วิธี):

  • Queue expense-buddy-image-analysis สร้างแล้ว
  • Queue expense-buddy-export-csv สร้างแล้ว
  • R2 bucket expense-buddy-export-csv สร้างแล้ว
  • wrangler.jsonc มี binding IMAGE_ANALYSIS_QUEUE, EXPORT_CSV_QUEUE, EXPORT_CSV_BUCKET
  • ถ้าเปิด Pro: ตั้ง STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET และ Stripe webhook endpoint /api/stripe/webhook

หลัง deploy เสร็จ — กลับไปตั้งค่าที่ข้ามไว้:

Deploy สำเร็จแล้วจะได้ domain จาก Cloudflare เช่น https://your-worker.workers.dev (หรือ custom domain ถ้าตั้งไว้)

  1. ตั้ง Webhook URL ใน LINE OA Manager (Step 5.1 ที่ข้ามไว้):

    • เข้า https://manager.line.biz/ → Settings → Messaging API
    • ใส่ Webhook URL: https://<APP_BASE_URL>/api/line/webhook
    • กด Verify เพื่อทดสอบ และเปิด Use webhook
  2. ตั้ง LIFF Endpoint URL (ถ้ายังไม่ได้ตั้งตอนสร้าง LIFF ใน Step 5.2):

    • LINE Developers Console → LINE Login channel → LIFF → แก้ Endpoint URL เป็น https://<APP_BASE_URL>/liff

10 ตั้งค่า Pro Feature (Optional)

กรณีต้องการเปิด Pro feature ต้องปรับค่า src/config.json เพื่อกำหนดว่า Free และ Pro ใช้อะไรได้บ้าง

ตัวอย่างเช่น user ทุกคน สามารถใช้ image AI ได้ 10 ครั้ง และ transaction ได้ 100 ครั้ง (ต่อเดือน) แต่ถ้าเป็น Pro (จ่ายเงินผ่าน Stripe แบบ one-time) เราก็จะตั้งค่า โดยเปิด pro.enabled เป็น true และกำหนด stripePriceId และ price

// 1. default pro.enabled=false คือทุก account ไม่จำกัด image ai และ transaction
{
"imageAi": 10,
"transaction": 100,
"pro": {
"enabled": false,
"stripePriceId": "",
"price": 0
}
}
// 2. ตั่งค่าใช้ pro, user ฟรีใช้งานได้จำกัด (imageAi และ transaction จะไม่ถูกอ่าน)
{
"imageAi": 10,
"transaction": 100,
"pro": {
"enabled": true,
"stripePriceId": "YOUR_STRIPE_PRICE_ID",
"price": 99000 // price ในหน่วยสตางค์ (1000 satang = 1 บาท)
}
}

หมายเหตุ:

  • pro.enabled = false จะไม่เปิด gate
  • pro.enabled = true จะเริ่มจำกัด free user ตาม imageAi และ transaction
  • price เป็น satang สำหรับแสดงผล UI เช่น 99000 = 990 บาท

Local dev

ใช้สำหรับรัน local เพื่อเปิดดู web ไม่ผ่าน LINE (bypass AUTH) พร้อมทั้งทำการ seed database เพื่อให้มี transaction

เริ่มต้น ให้เราทำการ สร้างไฟล์ .dev.vars ใน root ของ project เนื่องจาก Wrangler อ่าน secrets จากไฟล์นี้ตอนรัน local (ไม่ใช่จาก .env):

Terminal window
# ไฟล์ .dev.vars
DEV_AUTH_BYPASS=1
SESSION_SECRET=<random string เช่น openssl rand -base64 32>
LINE_CHANNEL_SECRET=...
LINE_CHANNEL_ACCESS_TOKEN=...
LIFF_CHANNEL_ID=...
APP_BASE_URL=http://127.0.0.1:4321
AI_VISION_MODEL=@cf/google/gemma-4-26b-a4b-it

และไฟล์ .env ให้เพิ่ม เพื่อให้หน้า /dev/login แสดงผล (by pass)

Terminal window
# ไฟล์ .env
PUBLIC_DEV_AUTH_BYPASS=1

รัน (ครั้งแรก)

Terminal window
bun run db:migrate:local
bun run dev:seed -- --count 100
bun run dev
  • db:migrate:local — ทำครั้งเดียว สร้าง local DB ที่ .wrangler/state/v3/d1/ ถ้ามีแล้วไม่ต้องรันซ้ำ เว้นแต่มี migration ใหม่
  • dev:seed — inject ข้อมูลทดสอบเข้า local DB ใส่ตัวเลขได้ตามต้องการ รันซ้ำได้เพื่อเพิ่มข้อมูล รองรับ --months-back <1-24> (default: 6) เพื่อ mock transaction ย้อนหลัง
  • db:generate — ไม่ต้องรันถ้า clone repo มาแล้ว รันเฉพาะตอนแก้ Drizzle schema เพื่อ generate migration files ใหม่

รัน (ครั้งต่อไป)

Terminal window
bun run dev

Dev login (ไม่ต้องเปิด LINE)

วิธีทดสอบ เปิดเว็บ และลองเล่นดูได้ http://localhost:4321/dev/login -> กด เข้าสู่ระบบโหมดพัฒนา

ใส่ LINE User ID (default: U_LOCAL_DEV_001) และ Display Name ได้ตามต้องการ — ระบบ redirect ไป dashboard โดยไม่ผ่าน LINE OAuth