SnapOtter sử dụng PostgreSQL 17 với [Drizzle ORM](https://orm.drizzle.team/) (pg-core / node-postgres) để lưu trữ dữ liệu bền vững. Lược đồ được định nghĩa trong `apps/api/src/db/schema.ts`.
Kết nối được cấu hình qua biến môi trường `DATABASE_URL` (mặc định `postgres://snapotter:snapotter@postgres:5432/snapotter`). Trong Docker Compose, container Postgres lưu dữ liệu của nó trong volume có tên `SnapOtter-pgdata`.
## Bảng {#tables}
### users {#users}
Lưu trữ các tài khoản người dùng. Được tạo tự động ở lần chạy đầu tiên từ `DEFAULT_USERNAME` và `DEFAULT_PASSWORD`.
| Cột | Kiểu | Ghi chú |
|---|---|---|
| `id` | uuid | Khóa chính |
| `username` | varchar | Duy nhất, bắt buộc |
| `passwordHash` | varchar | Băm scrypt |
| `role` | varchar | `admin`, `editor`, hoặc `user` |
| `mustChangePassword` | boolean | Cờ buộc đặt lại mật khẩu |
| `createdAt` | timestamp | Thời điểm tạo |
| `updatedAt` | timestamp | Thời điểm cập nhật gần nhất |
### sessions {#sessions}
Các phiên đăng nhập đang hoạt động. Mỗi hàng liên kết một token phiên với một người dùng.
| Cột | Kiểu | Ghi chú |
|---|---|---|
| `id` | varchar | Khóa chính (token phiên) |
| `userId` | uuid | Khóa ngoại tới `users.id` |
| `expiresAt` | timestamp | Thời điểm hết hạn |
| `createdAt` | timestamp | Thời điểm tạo |
### teams {#teams}
Các nhóm để tổ chức người dùng. Quản trị viên có thể gán người dùng vào các nhóm.
Thư viện tệp bền vững. Theo mặc định, một chỉnh sửa đã lưu được chèn vào như một hàng gốc độc lập («lưu thành tệp mới»: `version` 1, `parentId` null, nên tệp gốc vẫn được liệt kê), hoặc như một phiên bản liên kết với tệp cha khi bạn ghi đè lên tệp gốc (`parentId` được đặt, `version` tăng lên, thay thế nó). Cột `toolChain` ghi lại các công cụ đã áp dụng.
docker run --rm -v SnapOtter-pgdata:/data -v $(pwd)/backup:/backup \
alpine tar czf /backup/snapotter-pgdata.tar.gz -C /data .
```
### Di trú từ 1.x (SQLite) {#migrating-from-1-x-sqlite}
Nâng cấp từ SnapOtter 1.x có hướng dẫn riêng: xem [Nâng cấp từ 1.x lên 2.0](./upgrading). Nói ngắn gọn, hãy tái sử dụng volume `/data` hiện có của bạn và 2.0 sẽ tự động phát hiện và nhập `/data/snapotter.db` ở lần khởi động đầu tiên (hoặc đặt `SQLITE_MIGRATE_PATH` để trỏ tới nó một cách tường minh). Hãy sao lưu toàn bộ volume `/data` trước, không chỉ `snapotter.db`: 1.x dùng chế độ SQLite WAL, nên một container đã dừng thường để lại phần lớn dữ liệu của nó trong `snapotter.db-wal` bên cạnh một `snapotter.db` gần như rỗng.