โมเดลข้อมูลแชต · Chat data model (D1 + R2)
On this page
หน้านี้อธิบายว่าเราเก็บประวัติแชตระหว่างผู้ใช้กับ AI ไว้อย่างไร ตอนนี้ มีแค่ schema ใน D1 (nvx-db) ยังไม่มี endpoint หรือ UI ไหนอ่านหรือเขียนตารางพวกนี้ แผนขั้นต่อไปอยู่ท้ายหน้า
เป้าหมาย#
- เก็บบทสนทนาของผู้ใช้กับ AI ได้ครบ (ข้อความทุก role, โมเดลที่ใช้, จำนวน token) เพื่อเปิดต่อ ค้นย้อนหลัง และคิดต้นทุนได้
- ไฟล์แนบอยู่ใน R2 ส่วน D1 เก็บแค่ key และ metadata เพื่อให้ฐานข้อมูลเล็กและเร็ว
- ให้ AI หรือบริการอื่นเชื่อมต่อผ่าน API key ที่ เราออกและเพิกถอนได้เอง โดยเก็บแค่ hash ของ key
- ไม่ผูกกับผู้ให้บริการ auth รายใด คอลัมน์ auth จึงเป็น nullable ไว้ก่อน
แผนภาพ#
Mermaid source
erDiagram
users ||--o{ conversations : owns
conversations ||--o{ messages : contains
messages ||--o{ attachments : has
users {
TEXT id PK
INTEGER created_at
TEXT display_name
TEXT auth_provider
TEXT auth_subject
TEXT email
}
conversations {
TEXT id PK
TEXT user_id FK
TEXT title
TEXT model
INTEGER created_at
INTEGER updated_at
INTEGER archived
}
messages {
TEXT id PK
TEXT conversation_id FK
TEXT role
TEXT content
INTEGER tokens_in
INTEGER tokens_out
TEXT model
INTEGER created_at
}
attachments {
TEXT id PK
TEXT message_id FK
TEXT r2_key
TEXT mime
INTEGER size
INTEGER created_at
}
api_clients {
TEXT id PK
TEXT name
TEXT key_hash
TEXT scopes
INTEGER rate_limit
INTEGER created_at
INTEGER revoked_at
}
api_clients ไม่ผูก foreign key กับตารางอื่น เพราะเป็นตัวตนของ "เครื่อง" ที่เข้ามาใช้ API ไม่ใช่ผู้ใช้คน
การตัดสินใจหลัก#
| เรื่อง | ทางที่เลือก | เหตุผล |
|---|---|---|
| ชนิดของ id | TEXT (UUIDv7 หรือ ULID ที่แอปสร้าง) |
เรียงตามเวลาได้ ไม่เดาง่ายแบบเลขรัน และสร้างได้ก่อน insert (ใช้ทำ R2 key ได้ทันที) |
| เวลา | INTEGER เป็น Unix epoch มิลลิวินาที (UTC) |
เทียบและเรียงได้เร็ว ไม่มีปัญหา timezone; ค่า default มาจาก unixepoch('subsec') * 1000 |
| boolean | INTEGER 0/1 พร้อม CHECK |
SQLite ไม่มีชนิด boolean |
| ตาราง | STRICT |
SQLite บังคับชนิดคอลัมน์จริง ไม่ยอมให้ใส่ข้อความลงคอลัมน์ตัวเลข |
| role | CHECK (role IN ('user','assistant','system','tool')) |
กันค่าแปลกตั้งแต่ระดับฐานข้อมูล |
| การลบ | ON DELETE CASCADE จาก users → conversations → messages → attachments |
ลบผู้ใช้ทีเดียวได้ข้อมูลหายครบ (ตรงกับคำขอลบข้อมูลส่วนบุคคล) แต่ วัตถุใน R2 ต้องลบเองจากโค้ด เพราะ D1 สั่ง R2 ไม่ได้ |
| ไฟล์แนบ | bytes อยู่ R2, D1 เก็บ r2_key (unique), mime, size |
D1 จำกัดขนาดแถวและฐานข้อมูล ไฟล์ใหญ่จึงไม่ควรอยู่ใน D1 |
| API key | เก็บ key_hash = SHA-256 hex ของ key เต็ม (unique) |
key จริงแสดงครั้งเดียวตอนสร้าง ถ้าฐานข้อมูลรั่วก็ใช้ key ไม่ได้; key สุ่มยาวพอจึงไม่ต้องใช้ hash แบบช้า |
| scopes | JSON array ในคอลัมน์ TEXT พร้อม CHECK (json_valid(...)) |
ยืดหยุ่น เพิ่ม scope ใหม่ได้โดยไม่ต้อง migrate |
updated_at ของบทสนทนา |
แอปอัปเดตเองทุกครั้งที่เพิ่มข้อความ (ไม่ใช้ trigger) | ควบคุมได้ชัดและอยู่ใน batch เดียวกับ insert ข้อความ |
Index และ query ที่รองรับ#
| Query | Index |
|---|---|
| รายการบทสนทนาของผู้ใช้ เรียงล่าสุดก่อน ซ่อนที่เก็บถาวร | conversations_user_updated_idx (user_id, archived, updated_at DESC) |
โหลดข้อความในบทสนทนาตามลำดับ และแบ่งหน้าด้วย created_at |
messages_conversation_created_idx (conversation_id, created_at, id) |
| ไฟล์แนบของข้อความ | attachments_message_idx (message_id) |
| หา API client จาก key | unique index บน key_hash |
| รายการ API client ที่ยังใช้งานได้ | api_clients_active_created_idx (created_at DESC) WHERE revoked_at IS NULL |
| หาผู้ใช้จากตัวตนของผู้ให้บริการ auth | users_auth_identity_uq (auth_provider, auth_subject) (unique) |
Migration#
ไฟล์อยู่ในโฟลเดอร์ migrations/ และรันตามลำดับชื่อ:
0001_schema_meta.sql: ตารางschema_meta(key/value) พร้อมschema_version = 10002_chat_history.sql: ทั้ง 5 ตารางและ index แล้วตั้งschema_version = 2
wrangler บันทึกว่ารันไฟล์ไหนไปแล้วในตาราง d1_migrations ห้ามแก้ไฟล์ที่ apply ไปแล้ว ถ้าจะเปลี่ยน schema ให้สร้างไฟล์ใหม่ (0003_...sql) ขั้นตอนดูที่ operations/d1-database.md รายละเอียดคอลัมน์ดูที่ reference/d1-schema.md
สถานะ R2#
bucket nvx-assets (binding ASSETS_BUCKET) ยังไม่ได้สร้าง เพราะบัญชียังไม่เปิดใช้ R2 (API ตอบ error 10042 "Please enable R2 through the Cloudflare Dashboard") การเปิด R2 ต้องยอมรับเงื่อนไขการใช้งานและการคิดเงินใน dashboard ซึ่งเจ้าของบัญชีต้องตัดสินใจเอง ตาราง attachments ออกแบบรอไว้แล้ว และ binding จะเพิ่มใน wrangler.jsonc หลัง bucket มีจริงเท่านั้น (ถ้าใส่ก่อน deploy จะล้ม)
แผนขั้นต่อไป (ยังไม่ได้สร้าง)#
API routes (ทั้งหมดอยู่ใต้ /api, ใช้ zod ตรวจ input และ rate limit แบบเดียวกับ /api/plan):
| Route | ทำอะไร |
|---|---|
GET /api/chat/conversations |
รายการบทสนทนาของผู้ใช้ (cursor ด้วย updated_at) |
POST /api/chat/conversations |
สร้างบทสนทนา |
GET/PATCH/DELETE /api/chat/conversations/:id |
อ่าน, เปลี่ยนชื่อ/เก็บถาวร, ลบ (ลบวัตถุ R2 ด้วย) |
GET /api/chat/conversations/:id/messages |
ข้อความ แบ่งหน้าด้วย created_at |
POST /api/chat/conversations/:id/messages |
ส่งข้อความผู้ใช้แล้ว stream คำตอบ AI (SSE) จากนั้นบันทึกข้อความ assistant พร้อม tokens_in/out ใน batch เดียว |
POST /api/chat/attachments |
อัปโหลดผ่าน Worker เข้า R2 (จำกัดชนิดและขนาด) หรือออก presigned URL |
/api/v1/... |
API เดียวกันสำหรับ AI ภายนอก ใช้ Authorization: Bearer <key> |
Auth:
- ผู้ใช้คน: เริ่มด้วย OAuth (GitHub/Google) หรือ passkey ผ่าน session cookie แบบ
HttpOnly; Secure; SameSite=Lax; ทางเลือกที่เร็วกว่าสำหรับใช้ภายในคือ Cloudflare Access - AI ภายนอก: key รูปแบบ
nvx_live_<random 32 bytes>แสดงครั้งเดียว เก็บ SHA-256 ในapi_clients.key_hashตรวจrevoked_at IS NULLและ scope (เช่นchat:read,chat:write) ทุกคำขอ - Rate limit ต่อ client ตาม
api_clients.rate_limitด้วย Workers Rate Limiting (ต้องเพิ่ม binding และ namespace ใหม่)
UI:
- หน้า
/chatใช้ shell และ component ของ NVX ที่มีอยู่ (ไม่เปลี่ยน layout หน้าเดิม): sidebar รายการบทสนทนา, หน้าต่างข้อความที่ render Markdown ด้วย pipeline ที่ sanitize แล้ว, ช่องพิมพ์พร้อมแนบไฟล์, เลือกโมเดล - สองภาษา (ไทย/อังกฤษ) และผ่าน WCAG AA ทั้งสองธีม ตามมาตรฐานเดิมของโปรเจกต์
ดูเพิ่ม#
reference/d1-schema.md, operations/d1-database.md, reference/config-env.md, cloudflare-runtime.md
Source: docs/architecture/chat-data-model.md · /docs/architecture/chat-data-model.md