~/VibeHandbook
PDF ฟรี

บท 17 · 02

กรณีศึกษาที่ 2: เครื่องมือ SaaS ขนาดเล็กที่มีระบบล็อกอินและฐานข้อมูล

ไอเดีย

นักออกแบบฟรีแลนซ์คนหนึ่งต้องการแดชบอร์ดส่วนตัวสำหรับบันทึกชั่วโมงทำงานที่เรียกเก็บเงินได้ต่อลูกค้าแต่ละราย และส่งออกเป็น CSV (Comma-Separated Values — ไฟล์สเปรดชีตแบบข้อความล้วนที่แอปสเปรดชีตไหนก็เปิดได้) รายเดือน ไม่ต้องหรูหราอะไร แต่ต้องมีระบบบัญชี (เพื่อให้ข้อมูลของเธอเป็นของเธอคนเดียว) และต้องเก็บข้อมูลถาวรได้

สเปก

เมื่อมีระบบล็อกอินและฐานข้อมูลเข้ามาเกี่ยวข้อง เดิมพันก็สูงขึ้น สเปกจึงต้องระบุ ขอบเขต ให้ชัดเจนกว่าเดิม:

A logged-in web app where a user can: sign up / log in with email,
create clients, log time entries (date, client, hours, note),
see a table of entries filtered by month, and download that month
as CSV. Each user only ever sees their own data. Mobile-friendly.

ประโยค "Each user only ever sees their own data" (ผู้ใช้แต่ละคนเห็นได้เฉพาะข้อมูลของตัวเองเท่านั้น) ดูเหมือนจะเป็นแค่เรื่องประสบการณ์ผู้ใช้ (UX) แต่จริง ๆ แล้วมันคือแบบจำลองความปลอดภัยของทั้งแอปที่ถูกอัดแน่นอยู่ในเก้าคำ การระบุมันไว้ในสเปกทำให้เราสามารถชี้กลับไปที่ประโยคนี้ได้ทุกครั้งที่ AI เริ่มหลุดออกนอกเส้นทาง

สแต็กที่เลือกใช้

สำหรับคนที่สร้างแอปคนเดียว ท่าที่ชนะคือการเลือกสแต็กที่ระบบล็อกอินและฐานข้อมูลเป็นบริการที่มีคนดูแลให้ ไม่ใช่โค้ดที่คุณต้องเขียนเอง เราเลือกแอป Next.js ที่ดีพลอยบนโฮสต์แบบ พร้อมฐานข้อมูล Postgres แบบมีคนดูแล และผู้ให้บริการล็อกอินสำเร็จรูปที่จัดการเรื่องล็อกอินด้วยอีเมล เซสชัน และการรีเซ็ตรหัสผ่านให้เราทั้งหมด โค้ดที่ต้องเขียนน้อยลงก็หมายถึงโค้ดที่ AI จะทำพลาดแทนเราได้น้อยลงตามไปด้วย ระบบล็อกอินโดยเฉพาะเป็นหมวดที่แทบไม่ควรให้ AI เขียนเองตั้งแต่ต้นเลย เพราะรูปแบบความล้มเหลวมันเงียบเชียบ รัศมีความเสียหายคือบัญชีของทุกคน และผู้ให้บริการแบบมีคนดูแลก็ผ่านการเจอกรณีสุดขั้วมานับล้าน ๆ ครั้งของการล็อกอินมาแล้ว

ส่วนที่มีคนดูแลอยู่รอบนอก ส่วนกฎเดียวที่เราเป็นเจ้าของอยู่ตรงกลาง นั่นคือทุกคิวรีต้องกรองด้วย user_id เพื่อไม่ให้ผู้ใช้คนหนึ่งอ่านข้อมูลของอีกคนได้เด็ดขาด:

 ┌──────┐          ┌─────────────┐         ┌──────────────────┐
 │ USER │─ login ─▶│ AUTH        │─ user ─▶│  NEXT.JS APP     │
 └──────┘          │ (managed)   │  id     │                  │
                   └─────────────┘         │  every query:    │
                                           │  WHERE user_id=? ─┼──┐
                                           └──────────────────┘  │
                                                                 ▼
                                                       ┌──────────────────┐
                                                       │ POSTGRES (managed)│
                                                       │ clients · entries │
                                                       └──────────────────┘
            ↑ managed = less to get wrong   ↑ the one boundary YOU enforce

พรอมต์สำคัญ

เราให้เทมเพลตของผู้ให้บริการล็อกอินทำงานหนักส่วนใหญ่ให้ แล้วสั่งให้ AI เพิ่มตรรกะเฉพาะทางธุรกิจลงไปทับ:

We're using [auth provider]'s Next.js starter. Add a Postgres
schema with two tables: clients (id, user_id, name) and
time_entries (id, user_id, client_id, date, hours, note). Every
query MUST filter by the logged-in user's id from the session.
Generate the migration and the typed data-access functions.

บรรทัด "MUST filter by user_id" คือประโยคที่สำคัญที่สุดในโปรเจกต์ทั้งหมด เราย้ำเงื่อนไขนี้ในแทบทุกพรอมต์ที่แตะต้องข้อมูล เพราะบั๊กที่น่ากลัวที่สุดในแอปหลายผู้ใช้เพียงหนึ่งเดียวคือการที่ผู้ใช้คนหนึ่งเห็นแถวข้อมูลของอีกคน การพูดซ้ำ ๆ อาจรู้สึกว่าฟุ่มเฟือยตอนพิมพ์ แต่นั่นแหละคือความฟุ่มเฟือยที่ช่วยชีวิตคุณไว้ เพราะโมเดลไม่มีความจำว่าเงื่อนไขนี้สำคัญแค่ไหนข้ามพรอมต์แต่ละครั้ง

สำหรับการส่งออกข้อมูล:

Add a /api/export route that takes a month (YYYY-MM), pulls the
logged-in user's time_entries for that month joined to client
names, and streams a CSV download. Reject the request if there's
no valid session.

อุปสรรค

ระหว่างการทดสอบ เราสร้างสองบัญชีขึ้นมา และพบว่าบัญชี B สามารถเห็นรายชื่อลูกค้าของบัญชี A ในดรอปดาวน์ได้ นี่คือบั๊กที่เรากลัวมากที่สุดพอดี แทนที่จะขอให้ AI "แก้ให้หน่อย" เราให้มันพิสูจน์ปัญหาก่อน:

Account B is seeing Account A's clients. Show me every database
query in the codebase that reads the clients table, and for each
one tell me whether it filters by the session user_id. Don't fix
anything yet — just audit.

การตรวจสอบพบคิวรีหนึ่งตัว คือตัวโหลดดรอปดาวน์ ที่ถูกเขียนขึ้นก่อนที่เราจะเพิ่มเงื่อนไขนั้น และหลุดรอดผ่านมาได้ เราให้ AI เพิ่มฟิลเตอร์ที่ขาดหายไป แล้วขอให้มันสร้างตัวป้องกันเพิ่มเติม:

Add a single helper that every read goes through, which takes the
session and injects the user_id filter, so no future query can
forget it. Refactor the existing queries to use it.

นั่นเปลี่ยนการแก้แบบครั้งเดียวจบให้กลายเป็นหลักประกันเชิงโครงสร้าง จากนั้นเราทำให้หลักประกันนี้ทดสอบได้จริง เพราะการ์ดที่ตรวจสอบไม่ได้ก็เป็นแค่ความหวังลม ๆ แล้ง ๆ:

Write a test that creates two users, has each create a client,
then asserts that user A's session can never read user B's client
through any of the data-access functions.

บทเรียน: เมื่อ AI สร้างบั๊กด้านความปลอดภัยขึ้นมา อย่าแค่แก้จุดนั้นจุดเดียว แต่สั่งให้มันกำจัด หมวดหมู่ ของความผิดพลาดนั้นทั้งหมด แล้วล็อกหมวดหมู่นั้นไว้ด้วยเทสต์ที่จะพังเสียงดังทันทีถ้าใครเผลอเปิดช่องนั้นขึ้นมาใหม่

การเปิดตัว

เราใส่ข้อมูลทดสอบของหนึ่งเดือน ส่งออกเป็น CSV แล้วเปิดในสเปรดชีตเพื่อยืนยันว่าตัวเลขและการเข้ารหัสถูกต้อง จากนั้นตั้งรหัสผ่านฐานข้อมูลให้แข็งแรง และหมุนเวียนข้อมูลรับรองออกจากไฟล์ในเครื่องทุกไฟล์ เราดีพลอยไปที่โฮสต์แบบ serverless เพิ่มตัวแปรสภาพแวดล้อมสำหรับใช้งานจริงในแดชบอร์ดของมัน แล้วส่ง ให้เธอ เธอสมัครใช้งานเองด้วยการล็อกอินจริง โปรเจกต์ทั้งหมดใช้เวลาแค่หนึ่งสุดสัปดาห์

อยากได้แบบออฟไลน์?

ดาวน์โหลดหนังสือทั้งเล่มเป็น PDF หรือ EPUB — ฟรี