Troubleshooting
Quick reference for common production issues.
Bot not responding
Symptoms
User sends a message in LINE, no reply.
Diagnose
-
Check LINE webhook delivery:
- LINE Developers Console → Messaging API channel → Webhook
- “Last delivery” should show recent attempts with HTTP status
- If 200 but no reply → backend bug (check logs)
- If non-200 → backend not reachable or returning error
-
Check backend
/healthz:Terminal window curl https://api.tinadiet.com/healthzShould return
{"status":"ok",...}. If timeout or 500 → backend down. -
Check Railway logs:
- Railway dashboard → backend → Logs tab
- Filter for
webhook.lineorlevel":"error" - Look for stack traces around the time of the message
Common causes
- LINE_CHANNEL_SECRET / ACCESS_TOKEN rotated in console but Railway env not updated → signature verification fails → 401
- OpenAI API key invalid (key revoked, budget hit, etc.) → food parsing throws → handler crashes → no reply
- DB locked (rare with better-sqlite3, but possible if a previous write hung) → restart backend to release
Fix
- Rotate offending secret, update Railway env, redeploy
- If DB issue: restart backend service via Railway dashboard
Payment doesn’t apply
Symptoms
User completed payment in PromptPay/TrueMoney but premium not active in LIFF.
Diagnose
-
Check
paymentstable for the charge:Terminal window node -e 'const db=require("better-sqlite3")("/data/app.db");const rows=db.prepare("SELECT * FROM payments WHERE user_id = <USER_ID> ORDER BY id DESC LIMIT 5").all();console.table(rows);'- Status
pending→ webhook didn’t arrive or failed - Status
successfulbut no premium → bug in grant application - No row → charge never created
- Status
-
Check Omise dashboard Recent Deliveries:
dashboard.omise.co/test/webhooks(or/webhooksfor LIVE)- Look for the most recent delivery, check status
- Status 200 → backend received OK; bug somewhere downstream
- Status 400 BAD_SIGNATURE →
OMISE_WEBHOOK_SECRETmismatch - Status 4xx/5xx other → backend logged the error
-
Check Railway logs for
webhook.omise:webhook.omise.handledshows event processedwebhook.omise.handle_failedshows handler errorwebhook.omise.bad_signatureshows sig mismatch
Common causes
OMISE_WEBHOOK_SECRETwas rolled in dashboard but Railway not updated- Webhook URL changed (now wrong) — verify
https://api.tinadiet.com/webhooks/omiseis the registered URL tinadiet_user_idmetadata missing from charge → backend can’t find user- charge
metadata.tinadiet_user_iddoesn’t match an existing user (created in TEST mode with placeholder, etc.)
Fix
- Verify
OMISE_WEBHOOK_SECRETmatches dashboard (re-roll if uncertain) - Manually sync via API:
curl -X GET -H "Authorization: Bearer <JWT>" https://api.tinadiet.com/api/v1/billing/omise/charge/<charge_id>— forces sync and applies grant if eligible - Manual grant via Railway Console (last resort) — see Manual grants
LIFF blank/stuck screen
Symptoms
User opens LIFF, sees mascot loading forever, or blank white screen.
Diagnose
-
In LINE webview, open dev tools:
- LINE Android: tap
︙(top right) → “Webview tools” → Inspect - LINE iOS: connect to Safari debugger via USB cable
- Check Console for errors
- LINE Android: tap
-
Common error patterns:
LiffInitError→ LIFF SDK init failed → likely wrongVITE_LIFF_IDin Cloudflare env, or LIFF channel “Status” is “Developing” (must be Public for non-admin users)Failed to fetch→ backend CORS or network issue401 Unauthorized→ session JWT exchange failed; backend may not have validLINE_LOGIN_CHANNEL_ID
-
Check Cloudflare deploy status:
- Dashboard → Workers → tinadiet-liff → Deployments
- Most recent deploy succeeded?
Common causes
- LIFF channel set to “Developing” instead of “Public” (only admin users can use Developing-status LIFF)
VITE_API_BASE_URLbuild env wrong → LIFF can’t reach backend- Backend session JWT secret rotated but old JWTs cached in LIFF SDK → exchange fails until next session
Fix
- Set LIFF Status to Public in LINE Developers Console
- Verify Cloudflare env vars on Workers Build
- Rotate
SESSION_JWT_SECRETand push to Railway → all sessions invalidate, next LIFF init re-exchanges
Premium expired but still showing premium UI
Symptoms
User’s premium expired (date passed) but LIFF still shows Premium badge.
Diagnose
-
Check users.plan and premium_expires_at:
Terminal window node -e 'const db=require("better-sqlite3")("/data/app.db");const row=db.prepare("SELECT id, plan, premium_expires_at FROM users WHERE id = ?").get(<USER_ID>);console.log(row);' -
Verify
expire_premiumcron is running:- Railway logs → search
jobs.expire_premium.tick - Should fire daily at 02:00 ICT
- If never fired → cron not scheduled (env
CRON_ENABLED=false?)
- Railway logs → search
Fix
- Run
expire_premiummanually:Returns count of users reverted to free.Terminal window curl -X POST -H "x-jobs-secret: $JOBS_TRIGGER_SECRET" \https://api.tinadiet.com/internal/jobs/expire-premium - If cron disabled: set
CRON_ENABLED=truein Railway env
OpenAI errors
Symptoms
Bot replies with “ขออภัยค่ะ Tina มีปัญหาเล็กน้อย” or similar fallback.
Diagnose
- Check Railway logs for
food_parserorcoacherrors - Check OpenAI dashboard usage:
- platform.openai.com → Usage tab
- Hit monthly budget cap?
- API key revoked?
Common causes
- Budget cap hit (project key has $35/month limit)
- API key rotated but Railway env not updated
- OpenAI outage (rare but does happen — status.openai.com)
- gpt-4o-mini deprecated/renamed — model env points to wrong name
Fix
- Increase budget cap in OpenAI dashboard, or rotate key, or wait for upstream recovery
Cloudflare DNS issues
Symptoms
tinadiet.com or api.tinadiet.com not resolving, or wrong target.
Diagnose
dig tinadiet.comdig api.tinadiet.comdig app.tinadiet.comCommon causes
- DNS record deleted accidentally
- TTL changed but cache hasn’t refreshed
- Cloudflare proxy disabled (gray cloud instead of orange) for
api— TLS may not work
Fix
- Re-add missing DNS record in Cloudflare dashboard
- Ensure orange cloud (proxy on) for all records
See also
- Architecture overview
- Railway deployment
- Cloudflare LIFF deployment
- DB queries — full diagnostic snippets