nirva|deploy
2026-05-17·14 min read·NirvaDeploy team
#LINE#bot#production#Thai-SME

LINE OA Bot ระดับ production — ครบทุก feature ที่ Thai SME ต้องการ

Echo bot ใช้ลอง concept ได้ — แต่ production ต้องการอะไรมากกว่านั้น

ก่อนหน้านี้เราเขียน tutorial 5-นาทีสำหรับ LINE bot starter. บทความนี้ deep dive — ทุก feature ที่ Thai SME bot ต้องมีตอน launch จริง

Architecture pattern

LINE Platform ─────► Webhook ─────► NirvaDeploy service (FastAPI)
                                          │
                                          ├─► Database (Postgres)
                                          ├─► Redis (rate limit + session)
                                          ├─► Job queue (Celery / BullMQ)
                                          ├─► File storage (Cloudflare R2)
                                          └─► LLM (Claude API for smart reply)

Admin panel ──► /dashboard/[bot-id] (Next.js)
                  ├─► Broadcast composer
                  ├─► User list + segment
                  ├─► Analytics dashboard
                  └─► Rich menu designer

NirvaDeploy เหมาะเพราะ:

  • Auto-deploy from GitHub push (Phase 1 ทำได้แล้ว)
  • Postgres + Redis add-ons (Phase 2.x)
  • ใบกำกับภาษีอัตโนมัติสำหรับ LINE Messaging API monthly fee

1. Rich Menu (เปลี่ยน UX ทันที)

Rich menu = bottom-of-chat menu (6 หรือ 12 cells). Reduce friction มหาศาล — user แทนที่จะพิมพ์ "ดูสินค้า" แค่ tap

from linebot.v3.messaging import (
    MessagingApi, ApiClient, Configuration,
    RichMenuRequest, RichMenuSize, RichMenuArea,
    RichMenuBounds, MessageAction, URIAction,
)

config = Configuration(access_token=LINE_TOKEN)

rich_menu = RichMenuRequest(
    size=RichMenuSize(width=2500, height=1686),  # 2:1 ratio
    selected=True,
    name="Main menu Thai SME",
    chat_bar_text="📱 เมนู",
    areas=[
        # 6 cells, 3 cols × 2 rows
        RichMenuArea(
            bounds=RichMenuBounds(x=0, y=0, width=833, height=843),
            action=MessageAction(text="📦 ดูสินค้า"),
        ),
        RichMenuArea(
            bounds=RichMenuBounds(x=833, y=0, width=833, height=843),
            action=MessageAction(text="🛒 ตะกร้า"),
        ),
        RichMenuArea(
            bounds=RichMenuBounds(x=1666, y=0, width=834, height=843),
            action=URIAction(uri="https://liff.line.me/...", label="LIFF order"),
        ),
        # ... 3 more cells
    ],
)

with ApiClient(config) as api_client:
    api = MessagingApi(api_client)
    response = api.create_rich_menu(rich_menu)
    rich_menu_id = response.rich_menu_id

    # Upload PNG image
    with open("menu.png", "rb") as f:
        api.set_rich_menu_image(rich_menu_id, "image/png", f.read())

    # Apply as default for all users
    api.set_default_rich_menu(rich_menu_id)

2. Flex Message (ไม่ใช่ plain text)

Flex = JSON-based layout (เหมือน HTML แต่ JSON). User เห็น card สวยๆ แทน "raw text"

flex_msg = {
    "type": "bubble",
    "hero": {
        "type": "image",
        "url": "https://yourcdn.com/product.jpg",
        "size": "full",
        "aspectRatio": "20:13",
        "aspectMode": "cover",
    },
    "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
            {"type": "text", "text": "เสื้อยืดผ้าฝ้าย 100%", "weight": "bold", "size": "xl"},
            {
                "type": "box",
                "layout": "baseline",
                "margin": "md",
                "contents": [
                    {"type": "text", "text": "฿299", "size": "xl", "color": "#0EA5A4", "weight": "bold"},
                    {"type": "text", "text": "(ลด 40%)", "size": "sm", "color": "#aaaaaa", "margin": "md"},
                ],
            },
        ],
    },
    "footer": {
        "type": "box",
        "layout": "vertical",
        "spacing": "sm",
        "contents": [
            {
                "type": "button",
                "style": "primary",
                "color": "#1E3A5F",
                "action": {"type": "postback", "label": "เพิ่มในตะกร้า", "data": "action=add&id=42"},
            },
            {
                "type": "button",
                "style": "secondary",
                "action": {"type": "uri", "label": "ดูเพิ่ม", "uri": "https://liff.line.me/.../product/42"},
            },
        ],
    },
}

api.push_message(PushMessageRequest(
    to=user_id,
    messages=[FlexMessage(alt_text="เสื้อยืดผ้าฝ้าย ฿299", contents=FlexContainer.from_dict(flex_msg))],
))

Best practices:

  • ใช้ Flex สำหรับ product cards, order confirmation, notifications
  • ใช้ plain text สำหรับ FAQ replies, errors
  • alt_text สำคัญ — แสดงใน notification + accessibility

3. Postback events (Button → action)

@app.post("/webhook")
async def webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-Line-Signature")
    # ... verify signature ...

    events = parser.parse(body.decode(), signature)
    for event in events:
        if isinstance(event, PostbackEvent):
            data = parse_qs(event.postback.data)
            action = data.get("action", [""])[0]
            if action == "add":
                product_id = data.get("id", [""])[0]
                await cart_add(event.source.user_id, product_id)
                api.reply_message(...)  # confirmation

4. LIFF (LINE Front-end Framework)

LIFF = web app ที่ run inside LINE — got user ID + access token auto-injected. ใช้สำหรับ checkout, profile edit, complex forms.

<!DOCTYPE html>
<html>
<head>
  <script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
</head>
<body>
  <script>
    liff.init({ liffId: "1234567890-AbCdEfGh" }).then(() => {
      if (!liff.isLoggedIn()) liff.login();
      liff.getProfile().then(profile => {
        document.body.innerHTML = `<h1>สวัสดี ${profile.displayName}</h1>`;
      });
    });
  </script>
</body>
</html>

Deploy LIFF web app บน NirvaDeploy:

# Claude:
"deploy template thai-liff-starter ตั้ง LIFF_ID เป็น xxxxx"

5. Broadcast + Multi-cast

  • Broadcast = ส่งให้ทุกคน (ใช้ quota ทุก message × user count)
  • Multi-cast = ส่งให้ list (max 500/call)
  • Narrowcast = ส่งตาม audience (age, gender, region — pre-built segment)
# Broadcast (ระวัง quota!)
api.broadcast(BroadcastRequest(
    messages=[TextMessage(text="🎉 Sale 11.11 เริ่มแล้ว!")],
))

# Multi-cast — ปลอดภัยกว่า เพราะระบุ user IDs ชัด
api.multicast(MulticastRequest(
    to=user_ids[:500],
    messages=[flex_message],
))

Quota math (LINE OA pricing 2026):

  • Premium plan: 45,000 messages/mo free, ฿0.04/msg ส่วนเกิน
  • หาก user 10K → ส่ง 1 broadcast = 10K messages = ฿0 (within quota)
  • ส่ง 5 broadcasts/เดือน @ 10K user = 50K messages = ฿200 ส่วนเกิน

Best practice:

  • เก็บ user_id ทุกครั้งที่ user ทัก bot (follow event)
  • Segment ก่อน broadcast — ไม่ส่งให้คนที่ unfollow แล้ว
  • Schedule broadcast ที่ peak hours (11am-12pm, 6pm-8pm ของไทย)

6. Webhook signature verification (security)

ห้ามข้าม — ใครก็ส่ง webhook ได้

import hmac
import hashlib
import base64

def verify_signature(body: bytes, signature: str, channel_secret: str) -> bool:
    expected = base64.b64encode(
        hmac.new(channel_secret.encode(), body, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(expected, signature)

ใส่เป็น middleware:

@app.middleware("http")
async def line_signature_middleware(request: Request, call_next):
    if request.url.path == "/webhook":
        body = await request.body()
        sig = request.headers.get("X-Line-Signature", "")
        if not verify_signature(body, sig, CHANNEL_SECRET):
            return JSONResponse({"error": "invalid signature"}, status_code=403)
    return await call_next(request)

7. Rate limit + abuse protection

ใช้ Redis สำหรับ token bucket:

import redis

r = redis.from_url(REDIS_URL)

async def check_rate_limit(user_id: str, limit: int = 10, window: int = 60) -> bool:
    key = f"rl:{user_id}"
    count = await r.incr(key)
    if count == 1:
        await r.expire(key, window)
    return count <= limit

@app.post("/webhook")
async def webhook(...):
    user_id = event.source.user_id
    if not await check_rate_limit(user_id, limit=30, window=60):
        api.reply_message(... "⚠️ ส่งข้อความถี่เกินไป รอสักครู่")
        return

8. Analytics dashboard

ใช้ event log แบบนี้:

CREATE TABLE bot_events (
    id BIGSERIAL PRIMARY KEY,
    user_id VARCHAR(64),
    event_type VARCHAR(32),  -- 'message', 'postback', 'follow', 'unfollow'
    payload JSONB,
    created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_events_user_time ON bot_events(user_id, created_at DESC);
CREATE INDEX idx_events_type_time ON bot_events(event_type, created_at DESC);

Aggregate ใน NirvaDeploy dashboard:

  • Daily Active Users (DAU)
  • Most popular postback actions
  • Conversion funnel (browse → add → checkout)
  • Unfollow rate trend

9. Smart reply ด้วย Claude API

แทนที่จะ FAQ keyword match ใช้ Claude:

import anthropic

client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

async def smart_reply(user_message: str, user_context: dict) -> str:
    response = await client.messages.create(
        model="claude-3-5-haiku-20251022",  # cheap + fast
        max_tokens=500,
        system=(
            "คุณเป็น customer service bot ของร้าน Thai SME ขายเสื้อผ้า. "
            f"ลูกค้า: {user_context.get('name')}. "
            "ตอบเป็นภาษาไทยสุภาพ ใส่ emoji ถ้าเหมาะสม. "
            "ถ้าไม่รู้คำตอบ — แนะนำให้รอ admin ตอบ"
        ),
        messages=[{"role": "user", "content": user_message}],
    )
    return response.content[0].text

Cost: Claude Haiku ~$0.25/1M input tokens. 1 message ≈ 500 tokens. = ~$0.000125 per reply = แสนข้อความ $12.50

ถูกกว่าจ้าง admin ตอบเอง 🤖

Deploy LINE bot ระดับ production บน NirvaDeploy

"Deploy template nirvadeploy/template-line-bot-production บน NirvaDeploy
ใช้ env:
  LINE_CHANNEL_ACCESS_TOKEN=xxx
  LINE_CHANNEL_SECRET=yyy
  ANTHROPIC_API_KEY=zzz
ตั้ง add-ons: postgres, redis
scale เป็น 2 replicas"

Claude เรียก:

  1. nirvadeploy_deploy_template
  2. Multiple nirvadeploy_set_variable
  3. nirvadeploy_scale_service(replicas=2)

🎉 Production-ready LINE bot in 3 minutes


Resources

คำถาม? Line OA `@nirvadeploy` · email [email protected]


ชอบบทความนี้?

ลอง NirvaDeploy ฟรี

Free tier ตลอดชีพ · ไม่ต้องใส่บัตร · Sign in ด้วย Line/Google

เริ่มเลย →