A LINE chatbot service that lets Thai lottery players record their ticket numbers and get automatically notified via LINE when any of their tickets win a prize.
Stack: Go + Fiber · PostgreSQL · LINE Messaging API
All interaction happens inside the LINE chat with the bot.
| Message sent | What happens |
|---|---|
123456 |
Records one L6 ticket for the upcoming draw |
456 |
Records one N3 ticket for the upcoming draw |
123456 x3 |
Records 3 copies of the same L6 ticket |
123456, 789012 |
Records two tickets in one message (comma or space separated) |
โพย |
Lists all tickets you have registered for the current upcoming draw |
| Follow bot | Creates your account; sends welcome message |
| Unfollow bot | Marks your account inactive; ticket history is preserved |
brew install golang-migrateRun all commands from the repo root unless noted otherwise.
Copy the example env file and fill in your values:
cp .env.example .env.localKey variables:
| Variable | Used by | Example value |
|---|---|---|
DB_USERNAME |
docker-compose | postgres |
DB_PASSWORD |
docker-compose | yourpassword |
DB_NAME |
docker-compose | lotto_journal |
DB_DSN |
Go app | postgres://postgres:yourpassword@localhost:5432/lotto_journal?sslmode=disable |
PORT |
Go app | :3000 |
LINE_CHANNEL_SECRET |
Go app | from LINE Developers console → Basic Settings |
LINE_CHANNEL_ACCESS_TOKEN |
Go app | from LINE Developers console → Messaging API |
pnpm db:startWait until the container is healthy before running migrations:
docker ps --filter name=lotto-db --format "table {{.Names}}\t{{.Status}}"You should see healthy in the status before proceeding.
pnpm migrate:uppnpm devRun all commands from the repo root.
- Install flyctl
- Login to Fly:
flyctl auth login- Ensure production infra is ready:
- Fly app name:
lotto-journal-api - Production LINE channel created (separate from dev)
- Neon production database created
- Fly app name:
Set runtime secrets (never commit these to git):
flyctl secrets set \
DB_DSN="postgres://...sslmode=require" \
LINE_CHANNEL_SECRET="..." \
LINE_CHANNEL_ACCESS_TOKEN="..." \
-a lotto-journal-apiflyctl deploy -a lotto-journal-apiUse the Neon connection string for production:
migrate -path apps/api/migrations -database "postgres://...sslmode=require" upflyctl status -a lotto-journal-api
flyctl checks list -a lotto-journal-api
curl -i https://lotto-journal-api.fly.dev/healthIn LINE Developers Console (production channel):
- Webhook URL:
https://lotto-journal-api.fly.dev/webhook - Enable webhook delivery
- Add the production bot account as friend
- Send a ticket message (example:
123456 x2) - Verify ticket rows in Neon DB
| Key | Where it lives | Notes |
|---|---|---|
DB_DSN |
Fly secrets | Neon connection string (sslmode=require) |
LINE_CHANNEL_SECRET |
Fly secrets | Production channel secret |
LINE_CHANNEL_ACCESS_TOKEN |
Fly secrets | Production channel access token |
APP_ENV |
fly.toml [env] |
Non-secret (production) |
PORT |
fly.toml [env] |
Non-secret (:8080) |
FLY_API_TOKEN |
GitHub Actions secret | Used only by CI/CD deploy workflow (not app runtime) |
Current production config keeps 1 machine running in primary region
sin.
For release process details, use:
doc/04-way-of-work/versioning-policy.md— SemVer policy (MAJOR.MINOR.PATCH)doc/04-way-of-work/release-checklist.md— step-by-step release checklist
The collection lives in trunk/bruno/. Open it in Bruno by choosing Open Collection and selecting that folder.
| Folder | Request | What it tests |
|---|---|---|
| REST | Webhook - Follow | User adds the bot as a friend → creates user record, sends welcome reply |
| REST | Webhook - Message | User sends ticket numbers (e.g. 123456 x2, 789) → parses and saves tickets |
| REST | Webhook - Unfollow | User removes the bot → marks user inactive |
| REST | Health | GET /health — liveness + DB readiness; 200 ok / 503 degraded |
| GLO | Check Result | Calls the Thai Government Lottery API directly — useful for inspecting the result payload |
1. Set the environment
In Bruno, select the dev environment (top-right dropdown). Then open Configure and set the secret variable:
| Variable | Value |
|---|---|
line_channel_secret |
Your channel secret from LINE Developers Console → Basic Settings |
This value is stored locally by Bruno and never committed to git.
2. Make sure the API is running
pnpm devThe endpoint_url in the dev environment points to http://localhost:3000 by default. Adjust if your PORT is different.
Every webhook request has a pre-request script that automatically computes the X-Line-Signature header before sending:
const crypto = require('crypto');
const secret = bru.getEnvVar('line_channel_secret');
const body = JSON.stringify(req.body);
const signature = crypto.createHmac('sha256', secret).update(body).digest('base64');
req.setHeader('X-Line-Signature', signature);You don't need to compute the signature manually — just send the request.
Each request body contains a webhookEventId. On first send, this ID is written to the webhook_events table. Sending the same request a second time will be silently skipped (the deduplication is working correctly).
To re-trigger processing, change the webhookEventId to any unique value before sending again.
| Field | What it is | Does our code use it? |
|---|---|---|
events[].source.userId |
The LINE user ID of the person who sent the event | Yes — used to find or create the user record |
destination |
Your bot's own LINE user ID | No — ignored by the handler |
The sample bodies use a fake userId (U1234567890abcdef…). Because FindOrCreate is idempotent, re-running the same request always operates on the same test user record in your local DB.
All pnpm commands run from the repo root.
All make commands run from apps/api/ — the pnpm shortcuts above call these automatically.
| pnpm (root) | make (apps/api) | What it does |
|---|---|---|
pnpm dev |
make run |
Start API with hot reload (air) |
pnpm build |
make build |
Build binary to dist/ |
| — | make clean |
Remove tmp/ and dist/ |
| pnpm (root) | make (apps/api) | What it does |
|---|---|---|
pnpm db:start |
make db-start |
Start PostgreSQL container |
pnpm db:stop |
make db-stop |
Stop PostgreSQL container (pause — data kept, restart with db:start) |
| pnpm (root) | make (apps/api) | What it does |
|---|---|---|
pnpm migrate:up |
make migrate-up |
Apply all pending migrations |
| — | make migrate-up-one |
Apply the next 1 migration only |
pnpm migrate:down |
make migrate-down |
Roll back the last migration |
| — | make migrate-down-all |
Roll back all migrations |
pnpm migrate:version |
make migrate-version |
Show current schema version |
| — | make migrate-force N=<ver> |
Force-set version (recover from dirty state) |
| Version | File | Description |
|---|---|---|
| 000001 | 000001_init_schema |
Initial schema — all tables, enums, indexes |
| 000002 | 000002_line_identity |
LINE identity redesign — replace email/password with line_user_id; rename N6→L6, n6_*→l6_* |
| 000003 | 000003_webhook_events |
Idempotency table — store processed LINE webhookEventId values (ON CONFLICT DO NOTHING) |