Sentinel System Series Part 2: ขุมพลังหลังบ้านด้วย Bun + Elysia และการจัดการข้อมูลเชิงพื้นที่ (Backend & Spatial)

ผ่าโครงสร้าง Backend ประสิทธิภาพสูงที่รองรับ User Roles หลายระดับ และการใช้ PostGIS เพื่อจัดการข้อมูลพิกัดภูมิศาสตร์แบบเรียลไทม์

· 9 min read

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) เพื่อหาที่อยู่ของพื้นที่นั้นๆ

เราใช้ 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)
  1. Duplicate Guard: ระบบใช้ PostgreSQL Advisory Lock เพื่อป้องกันกรณีที่มีการแจ้งเหตุพิกัดเดียวกันเข้ามาพร้อมกัน และใช้ ST_DWithin ตรวจสอบในรัศมี 100 เมตร หากมีเหตุประเภทเดียวกันและยังไม่จบ (Reported/In Progress) ระบบจะปฏิเสธการสร้างเพื่อลดภาระเจ้าหน้าที่
  2. Address Resolution: เมื่อพิกัดถูกส่งเข้ามา Backend จะทำการหาที่อยู่ (ตำบล/อำเภอ/จังหวัด) จากฐานข้อมูลเชิงพื้นที่ทันที เพื่อใช้ในการกระจายงานตามเขตรับผิดชอบ
  3. 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)

เจอกันตอนหน้าครับ!