Sentinel System Series Part 2: ขุมพลังหลังบ้านด้วย Bun + Elysia และการจัดการข้อมูลเชิงพื้นที่ (Backend & Spatial)
ผ่าโครงสร้าง Backend ประสิทธิภาพสูงที่รองรับ User Roles หลายระดับ และการใช้ PostGIS เพื่อจัดการข้อมูลพิกัดภูมิศาสตร์แบบเรียลไทม์
Problem
ระบบ Incident Management ทั่วไปมักมีปัญหาเรื่องการจัดการข้อมูลพิกัด (Lat/Long) และประสิทธิภาพในการรับ traffic จำนวนมากพร้อมกัน
Solution
เลือกใช้ Bun เป็น Runtime ร่วมกับ ElysiaJS เพื่อความเร็วสูงสุด และใช้ PostGIS ในการจัดการ Spatial Queries ระดับมืออาชีพ
Impact
ได้ระบบ Backend ที่เสถียร รองรับการกระจายงาน (Assignment) และการขยายตัว (Scale) ในอนาคต
Sentinel System Series Part 2: ขุมพลังหลังบ้านประสิทธิภาพสูง (The Engine)
หลังจากที่เราได้วางแผนวิเคราะห์ระบบไปใน ตอนที่ 1 แล้ว หน้าที่ของผมในฐานะ Lead Developer คือการเลือก Stack ที่ “เร็ว” และ “คุ้มค่า” ที่สุด เพื่อให้โปรเจกต์ Sentinel System (ระบบเฝ้าระวัง) รันได้อย่างราบรื่นในงบประมาณที่จำกัด
บทความนี้เราจะมาเจาะลึกที่ The Engine หรือระบหลังบ้านที่เราสร้างขึ้นครับ
1. ทำไมถึงเลือก Bun + ElysiaJS?
ในยุคที่ Node.js เริ่มช้าเกินไปสำหรับ High-throughput API ผมตัดสินใจเลือก Bun เป็น JavaScript Runtime เนื่องจาก:
- Startup Time: เยี่ยมยอด เหมาะกับการรันบน Docker
- Built-in Tools: ไม่ต้องลงเพิ่มเยอะ (Native Bundler, Test runner)
- ElysiaJS: เป็น Framework ที่เบาและเร็วที่สุดในปัจจุบัน พร้อมรองรับ Static Type-checking ตั้งแต่ต้นทางจนถึงปลายทาง (End-to-end Typesafety)
graph LR
User([User Client]) --> Elysia[ElysiaJS API]
subgraph "High Performance Backend (Bun)"
Elysia --> Auth[Better-Auth]
Elysia --> Drizzle[Drizzle ORM]
end
subgraph "Storage Layer"
Drizzle --> DB[(Postgres + PostGIS)]
Elysia --> S3[MinIO Storage]
end
2. การจัดการข้อมูลเชิงพื้นที่ (Spatial Data with PostGIS)
หัวใจของ Sentinel System (ระบบเฝ้าระวัง) คือการรู้ว่า “เหตุการณ์เกิดขึ้นที่ไหน” และ “เจ้าหน้าที่คนไหนอยู่ใกล้ที่สุด” ผมจึงเลือกใช้ PostgreSQL พร้อมส่วนขยาย PostGIS
ด้วย PostGIS เราสามารถทำ Spatial Queries ที่ซับซ้อนได้ง่ายๆ เช่น:
- PostGIS Geospatial Power: เรานำความสามารถของ PostGIS มาใช้อย่างเต็มที่ โดยเฉพาะการระบุพิกัดและการคำนวณพื้นที่
- Reverse Geocoding: ระบบสามารถเปลี่ยนพิกัด (Lat/Long) เป็นที่อยู่จริง (ตำบล, อำเภอ, จังหวัด) ได้อัตโนมัติ โดยใช้
ST_DWithinค้นหาภายในรัศมี 5 กม. และST_Distanceเพื่อหาจุดที่ใกล้ที่สุด - Spatial Area Management: รองรับทั้งจุด (Point) และรูปหลายเหลี่ยม (Polygon) เพื่อกำหนดขอบเขตความรับผิดชอบ (Area of Responsibility) โดยบันทึกข้อมูลในรูปแบบ GeoJSON และคำนวณจุดกึ่งกลาง (Centroid) เพื่อหาที่อยู่ของพื้นที่นั้นๆ
- Reverse Geocoding: ระบบสามารถเปลี่ยนพิกัด (Lat/Long) เป็นที่อยู่จริง (ตำบล, อำเภอ, จังหวัด) ได้อัตโนมัติ โดยใช้
เราใช้ Drizzle ORM ร่วมกับ Custom SQL เพื่อจัดการ Query ที่ซับซ้อนเหล่านี้ ทำให้โค้ดยังคงความเป็น TypeScript ที่พิมพ์ปลอดภัย (Type-safe)
flowchart LR
Coord[User Coordinates: Lat/Long] --> Search{PostGIS ST_DWithin}
Search -->|Radius 5km| Dist[Calculate ST_Distance]
Dist -->|Sort ASC| Result[Closest Address Found]
Result --> Prov[Province / District / Sub-district]
Prov --> Store[Save Incident with Address]
3. การทดสอบและความเสถียร (Bun Test & Unit Testing)
เพื่อให้ระบบเฝ้าระวังทำงานได้อย่างแม่นยำ ผมให้ความสำคัญกับ Unit Testing อย่างมาก โดยใช้ Test Runner ของ Bun ที่มีความเร็วสูง:
- Test Environment Isolation: แยกฐานข้อมูลสำหรับทดสอบโดยเฉพาะ (Port 5433) เพื่อไม่ให้กระทบข้อมูลจริง
- Automated Seeding: ในกระบวนการ
beforeAllระบบจะทำการ Seed ข้อมูลพื้นฐานที่จำเป็น เช่น Superadmin, Security Questions และข้อมูลที่อยู่ชุดตัวอย่างอัตโนมัติ - Comprehensive Coverage: ครอบคลุมตั้งแต่ Logic การคำนวณพิกัด ไปจนถึงระบบความปลอดภัย RBAC
4. วงจรชีวิตของเหตุการณ์ (Incident Lifecycle: Technical Flow)
เพื่อให้ระบบทำงานได้อย่างแม่นยำและป้องกันความซ้ำซ้อน ผมได้ออกแบบ Incident Lifecycle ที่มีกลไกป้องกันข้อมูลขยะ (Spam) และการแจ้งเหตุซ้ำ:
sequenceDiagram
participant U as User (App/Web)
participant API as Backend (Elysia)
participant PG as PostgreSQL (PostGIS)
participant NS as Notification Service
U->>API: Report Incident (Lat/Long + Images)
API->>API: Validate Payload (Max 3 Images)
API->>PG: Advisory Lock (Prevention of Race Conditions)
API->>PG: ST_DWithin Check (Duplicate Guard < 100m)
API->>PG: Reverse Geocode (ST_Distance to address)
API->>PG: Insert Incident (Expires in 24h)
API-->>NS: Trigger Hybrid Notification
API-->>U: Return Success (Incident ID)
- Duplicate Guard: ระบบใช้ PostgreSQL Advisory Lock เพื่อป้องกันกรณีที่มีการแจ้งเหตุพิกัดเดียวกันเข้ามาพร้อมกัน และใช้
ST_DWithinตรวจสอบในรัศมี 100 เมตร หากมีเหตุประเภทเดียวกันและยังไม่จบ (Reported/In Progress) ระบบจะปฏิเสธการสร้างเพื่อลดภาระเจ้าหน้าที่ - Address Resolution: เมื่อพิกัดถูกส่งเข้ามา Backend จะทำการหาที่อยู่ (ตำบล/อำเภอ/จังหวัด) จากฐานข้อมูลเชิงพื้นที่ทันที เพื่อใช้ในการกระจายงานตามเขตรับผิดชอบ
- Expiration Logic: เหตุการณ์ที่ไม่มีเจ้าหน้าที่รับ (Expired) จะถูกจำกัดเวลาไว้ที่ 24 ชั่วโมง เพื่อรักษาความสะอาดของข้อมูลบนแผนที่
5. ระบบแจ้งเตือนอัจฉริยะ (Hybrid Notification System)
ความเร็วในการตอบสนองคือตัวชี้วัดความสำเร็จ เราใช้ Hybrid Strategy ในการเลือกกลุ่มเป้าหมายที่จะได้รับแจ้งเตือน:
flowchart LR
New[New Incident Reported] --> Strategy{Hybrid Strategy}
Strategy -->|Primary: Proximity| Near[Officers within 5km]
Strategy -->|Secondary: Admin| Area[Area Admins & Commanders]
Near --> Combined[Deduplicate Targets]
Area --> Combined
Combined --> DualWrite[Dual Write Action]
DualWrite --> DB[Persistent Notification Table]
DualWrite --> FCM[Firebase Cloud Messaging]
- PostGIS Proximity: ระบบจะค้นหาเจ้าหน้าที่ที่ “กำลังปฏิบัติงาน” (Active) ในรัศมี 5 กม. ผ่าน
ST_DWithin - Fallback Logic: หากในรัศมี 5 กม. ไม่มีเจ้าหน้าที่ ระบบจะขยายการแจ้งเตือนไปยังเจ้าหน้าที่ทั้งหมดที่สังกัดใน “ตำบล/อำเภอ” นั้นๆ อัตโนมัติ
- Dual Write Architecture: เราส่ง Push Message ผ่าน FCM เพื่อความเร็ว และบันทึกลง Notification Table ในฐานข้อมูล เพื่อให้ผู้ใช้สามารถกลับมาดูประวัติการแจ้งเตือน (Inbox) ได้ในภายหลัง
6. ระบบความปลอดภัยและบทบาทผู้ใช้ (Better-Auth & RBAC)
ความปลอดภัยคือเรื่องสำคัญที่สุด เราเลือกใช้ Better-Auth ในการจัดการ Session และระบบ RBAC (Role-Based Access Control)
- Multi-tenant Feel: แม้ไม่ใช่ Multi-tenant 100% แต่เราออกแบบให้ Admin แต่ละคนเห็นเฉพาะข้อมูลในพื้นที่ตนเอง (Role Filtering)
- Security Questions: ระบบกู้คืนรหัสผ่านด้วยคำถามความปลอดภัย (5 เลือก 3) ซึ่งเป็นฟีเจอร์ที่ผมออกแบบมาเพื่อให้รองรับกรณีที่ผู้ใช้งานภาคสนามจำ Email ไม่ได้
graph LR
Login[User Login] --> RoleCheck{Check Role}
RoleCheck -->|Superadmin| All[Access Global Data]
RoleCheck -->|Admin| Area[Access Area Data]
RoleCheck -->|Officer| Assign[Access Assigned Data]
Area --> Filter[Apply DB Spatial Filter]
4. การจัดการไฟล์ภาพด้วย MinIO
เหตุการณ์ (Incident) สภาพจริงต้องมีรูปภาพประกอบ เราใช้ MinIO (S3-compatible storage) ในการเก็บภาพความละเอียดสูง
- รองรับการอัปโหลดได้สูงสุด 10 ภาพต่อเหตุการณ์
- ระบบจัดการ Clean-up อัตโนมัติเมื่อมีการลบเหตุการณ์ เพื่อไม่ให้มีไฟล์ขยะตกค้างใน Storage
5. การทำงานร่วมกับ Docker & Infrastructure
ผมออกแบบให้ Backend ทั้งหมดรันบน Docker ผ่าน Taskfile เพื่อให้ทีมงาน (แม้จะมีแค่ผมเป็นส่วนใหญ่ในตอนแรก) สามารถ Setup สภาพแวดล้อมได้ในคำสั่งเดียว:
task dev # รันทั้ง Postgres, MinIO และ API Server
สรุปแล้ว Backend ของ Sentinel System (ระบบเฝ้าระวัง) ถูกสร้างขึ้นมาเพื่อเป็นรากฐานที่ “นิ่ง” และ “ยืดหยุ่น” เพื่อรอรับการเชื่อมต่อจากหน้าบ้านที่เราจะพูดถึงใน Part 3: The Command Center (Frontend)
เจอกันตอนหน้าครับ!