A lightweight, high-performance microservice written in Go (Golang). It acts as a local relay queue to forward push notifications to the NotiPush API (or any other API) with a strict 2 TPS (Transactions Per Second) rate limit.
-
Strict 2 TPS Rate Limiting: Enforces a hard limit of 2 requests per second using Go's background worker and precise
time.Sleep. -
Persistent Queue: Uses SQLite (
modernc.org/sqlite- pure Go, no CGO required) to store the queue. Prevents data loss during unexpected container restarts or crashes. -
Database Optimization (Row Recycling): Reuses rows where
is_sent = 1instead of inserting new ones indefinitely. This Object Pooling concept guarantees the.dbfile size will not bloat over time. -
Smart Retry Mechanism:
-
Immediate Drop: Drops the payload immediately on 4xx Client Errors (Except 400) like
422 Unprocessable Entityfor duplicate messages. -
Delayed Retry (5 Minutes): Specifically for
400 Bad Requesterrors, the payload is pushed back to the queue and paused for 5 minutes before retrying (Up to 3 times) to prevent spamming the endpoint. -
Fast Retry: Retries up to 3 times on 5xx Server Errors or Network Timeouts before dropping the payload.
-
Extremely Lightweight: Compiled as a single static binary inside an Alpine Docker image (~15MB total size).
- Bearer or IP Access Control: Accepts inbound requests when the caller sends a valid bearer token in
Authorization: Bearer ...or when the source IP matchesALLOWED_IPS - Inbound Bearer Token: Configure
INBOUND_BEARER_TOKENfor callers that run behind dynamic IP addresses and cannot be safely allowlisted - Separate Inbound/Outbound Tokens:
INBOUND_BEARER_TOKENprotectsPOST /send-push, whileNOTIPUSH_TOKENis forwarded to the downstream NotiPush API as an outboundAuthorizationheader - CIDR Subnet Support: Supports network ranges using CIDR notation (e.g.,
192.168.1.0/24,10.99.0.0/16) for flexible network access control - Proxy-aware IP Detection: Supports
X-Forwarded-ForandX-Real-IPheaders to correctly identify client IPs behind load balancers or proxies - Flexible IP Configuration: Supports individual IPs, hostnames (localhost), IPv6 addresses, and CIDR subnets with comma-separated values as a backward-compatible fallback
- Docker and Docker Compose
- Clone the repository.
- Update your
.envordocker-compose.ymlvalues before starting the container:
NOTIPUSH_URL: Downstream API endpoint (Current default:https://notipush.app/api/send-push)NOTIPUSH_PORT: Port exposed by the microservice. The example.envin this repository uses8280INBOUND_BEARER_TOKEN: Bearer token for inbound requests toPOST /send-push. Use a long random secret when the caller has a dynamic IPALLOWED_IPS: Comma-separated allowlist for fallback access when the caller does not send a bearer token. Supports single IPs, hostnames, IPv6, and CIDR blocksNOTIPUSH_TOKEN: Optional outboundAuthorizationheader sent from this microservice to the downstream NotiPush APIDEBUG: Set totrueto log incoming payloads for troubleshooting
- Recommended auth setup:
- Keep
INBOUND_BEARER_TOKENset for callers behind dynamic IPs - Keep
ALLOWED_IPSfor trusted fixed-IP callers or as a migration fallback - Avoid committing real secrets from
.envinto version control
- Start the service:
docker compose up -d --build
(Note: The SQLite database file will be safely persisted in the ./push_data folder on your host machine).
- Verify locally:
curl -X POST http://localhost:8280/send-push \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_INBOUND_BEARER_TOKEN" \
-d '{"title":"hello","body":"test"}'The microservice exposes one internal endpoint. Send your raw JSON payload here, and it will be queued immediately.
Endpoint: POST http://localhost:8280/send-push when using the example .env in this repository, or POST http://localhost:<NOTIPUSH_PORT>/send-push for your own configuration.
Inbound authentication accepts either of these:
Authorization: Bearer <INBOUND_BEARER_TOKEN>- A source IP/hostname/subnet that matches
ALLOWED_IPS
Authorization behavior is:
- Valid bearer token: request is accepted even if the caller IP changes
- No bearer token but IP is allowlisted: request is accepted
- Invalid bearer token and IP not allowlisted: request is rejected with
401 Unauthorized
Example cURL:
curl -X POST http://localhost:8280/send-push \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_INBOUND_BEARER_TOKEN" \
-d '{
"token": "YOUR_NOTIPUSH_API_TOKEN",
"device_token": "your_device_token_here",
"title": "Hello World",
"body": "This message is rate-limited to 2 TPS"
}'
Response:
{"status":"queued"}
If authorization fails, the service responds with 401 Unauthorized and returns a WWW-Authenticate: Bearer realm="send-push" header.
GitHub Actions is configured in .github/workflows/ci.yml to run on every push and pull request:
gofmt -l .to catch formatting driftgo test ./...go build ./...
ไมโครเซอร์วิสขนาดเล็กและทำงานได้รวดเร็ว เขียนด้วยภาษา Go (Golang) ทำหน้าที่เป็นคิวตัวกลาง (Relay Queue) เพื่อส่ง Push Notification ไปยัง NotiPush API (หรือประยุกต์ใช้กับ API อื่นๆ ได้) โดยมีการควบคุมอัตราการส่งที่ 2 TPS (Transactions Per Second) อย่างเด็ดขาด
-
คุม 2 TPS เด็ดขาด: ควบคุมอัตราการส่งไม่ให้เกิน 2 ครั้งต่อวินาทีโดยใช้ Worker และการหน่วงเวลาของ Go
-
คิวไม่สูญหายเมื่อไฟดับ: ใช้ SQLite (
modernc.org/sqlite- แบบ Pure Go ไม่ต้องพึ่งพา CGO) บันทึกคิวลงดิสก์ ข้อมูลที่รอส่งจะไม่หายไปแม้ Docker Container จะถูกลบหรือรีสตาร์ท -
ฐานข้อมูลไม่บวม (Row Recycling): ใช้หลักการ Object Pooling โดยนำแถวข้อมูลที่ส่งสำเร็จแล้ว (
is_sent = 1) กลับมาเขียนทับใหม่แทนการเพิ่มแถวใหม่ไปเรื่อยๆ ทำให้ขนาดไฟล์.dbคงที่และไม่กินพื้นที่ดิสก์ -
ระบบ Retry อัจฉริยะแบบแยกประเภท:
-
ทิ้งทันที (Drop): หากตอบกลับเป็น 4xx Error (ยกเว้น 400) เช่น
422ส่งข้อความซ้ำ ระบบจะไม่นำมาส่งซ้ำให้เสียโควต้า -
หน่วงเวลา 5 นาที (Delayed Retry): เฉพาะกรณีเกิด Error
400ระบบจะนำคิวไปต่อท้ายและตั้งเวลาล็อกไว้ไม่ให้ดึงมาทำซ้ำภายใน 5 นาที (สูงสุด 3 ครั้ง) -
ลองใหม่ (Fast Retry): หากเป็น 5xx Error หรือเน็ตเวิร์กพัง ระบบจะนำคิวไปต่อท้ายเพื่อรอส่งใหม่ทันที (สูงสุด 3 ครั้ง)
-
เบาและกินทรัพยากรน้อยมาก: คอมไพล์เป็น Binary ไฟล์เดียว รันบน Alpine Linux Image ขนาดเพียงประมาณ 15MB
- ควบคุมการเข้าถึงแบบ Bearer หรือ IP: อนุญาตให้ส่ง Push ได้หากมี
Authorization: Bearer ...ที่ตรงกับINBOUND_BEARER_TOKENหรือมี IP/Hostname/Subnet ตรงกับALLOWED_IPS - รองรับ Inbound Bearer Token: ใช้
INBOUND_BEARER_TOKENสำหรับเครื่องต้นทางที่มี Dynamic IP และไม่สะดวก allowlist IP - แยก Token ขาเข้าและขาออกชัดเจน:
INBOUND_BEARER_TOKENใช้ป้องกันPOST /send-pushส่วนNOTIPUSH_TOKENใช้เป็นAuthorizationheader ตอน service นี้ยิงต่อไปยัง NotiPush ปลายทาง - รองรับ Subnet แบบ CIDR: รองรับช่วงเครือข่ายโดยใช้ CIDR notation (เช่น
192.168.1.0/24,10.99.0.0/16) สำหรับควบคุมการเข้าถึงแบบยืดหยุ่น - ตรวจจับ IP ข้างหลัง Proxy: รองรับ Header
X-Forwarded-ForและX-Real-IPเพื่อระบุ Client IP ที่แท้จริงเมื่ออยู่หลัง Load Balancer หรือ Proxy - การตั้งค่า IP ยืดหยุ่น: รองรับทั้ง IPv4, IPv6, Hostname (localhost) และ CIDR Subnets โดยคั่นรายการด้วย comma และยังใช้เป็น fallback ได้
- Docker และ Docker Compose
- Clone โปรเจกต์นี้
- แก้ค่าที่
.envหรือdocker-compose.ymlให้ตรงกับระบบจริงก่อนรัน
NOTIPUSH_URL: URL ของ API ปลายทาง (ค่าปัจจุบัน:https://notipush.app/api/send-push)NOTIPUSH_PORT: Port ที่ต้องการให้ Microservice รัน โดยตัวอย่าง.envใน repo นี้ใช้8280INBOUND_BEARER_TOKEN: Bearer token สำหรับ request ที่ยิงเข้าPOST /send-pushแนะนำให้ตั้งเป็นค่ายาวและสุ่มเมื่อ client ใช้ Dynamic IPALLOWED_IPS: รายการ IP/Hostname/Subnet ที่อนุญาตให้ส่ง Push คั่นด้วย comma รองรับ CIDR notation และยังใช้เป็น fallback ได้NOTIPUSH_TOKEN: (Optional) Token สำหรับขาออก ถ้า API ปลายทางบังคับให้ใส่AuthorizationheaderDEBUG: ตั้งเป็นtrueเมื่อต้องการ log payload สำหรับ debug
- แนวทางการตั้งค่า auth ที่แนะนำ:
- ตั้ง
INBOUND_BEARER_TOKENไว้สำหรับ client ที่ IP เปลี่ยนบ่อย - เก็บ
ALLOWED_IPSไว้สำหรับเครื่องที่มี IP คงที่ หรือใช้เป็น fallback ช่วง migration - หลีกเลี่ยงการ commit secret จริงจาก
.envเข้า git
- สั่งรันระบบด้วยคำสั่ง:
docker compose up -d --build
(หมายเหตุ: ไฟล์ฐานข้อมูล SQLite จะถูกบันทึกไว้อย่างปลอดภัยที่โฟลเดอร์ ./push_data ในเครื่องโฮสต์)
- ทดสอบแบบเร็วในเครื่อง:
curl -X POST http://localhost:8280/send-push \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_INBOUND_BEARER_TOKEN" \
-d '{"title":"hello","body":"test"}'Microservice ตัวนี้เปิดรับ Request เพียง 1 Endpoint คุณสามารถส่ง JSON รูปแบบใดก็ได้เข้ามา ระบบจะนำไปเข้าคิวและส่งต่อให้ตามรูปแบบนั้น 100%
Endpoint: POST http://localhost:8280/send-push หากใช้ .env ตัวอย่างใน repo นี้ หรือ POST http://localhost:<NOTIPUSH_PORT>/send-push ตามค่าที่คุณตั้งเอง
การยืนยันตัวตนฝั่ง inbound ใช้ได้ 2 แบบ:
- ใส่
Authorization: Bearer <INBOUND_BEARER_TOKEN> - ยิงมาจาก IP/Hostname/Subnet ที่อยู่ใน
ALLOWED_IPS
พฤติกรรมการอนุญาตมีดังนี้:
- Bearer token ถูกต้อง: อนุญาต แม้ IP ต้นทางจะเปลี่ยน
- ไม่มี bearer token แต่ IP อยู่ใน allowlist: อนุญาต
- Bearer token ไม่ถูกต้องและ IP ไม่อยู่ใน allowlist: ปฏิเสธด้วย
401 Unauthorized
ตัวอย่างการยิงด้วย cURL:
curl -X POST http://localhost:8280/send-push \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_INBOUND_BEARER_TOKEN" \
-d '{
"token": "YOUR_NOTIPUSH_API_TOKEN",
"device_token": "your_device_token_here",
"title": "Hello World",
"body": "This message is rate-limited to 2 TPS"
}'
Response:
{"status":"queued"}
ถ้า auth ไม่ผ่าน ระบบจะตอบ 401 Unauthorized และส่ง header WWW-Authenticate: Bearer realm="send-push" กลับไป
มี GitHub Actions ที่ .github/workflows/ci.yml ให้รันอัตโนมัติทุกครั้งที่ push หรือเปิด pull request:
gofmt -l .สำหรับจับไฟล์ที่ format ไม่ตรงgo test ./...go build ./...