796 lines
17 KiB
Markdown
796 lines
17 KiB
Markdown
# Dokumentasi Folder `prisma/`
|
|
|
|
## Gambaran Umum
|
|
|
|
Folder `prisma/` berisi semua konfigurasi dan migration files untuk database management menggunakan Prisma ORM. Folder ini adalah "blueprint" dari database schema aplikasi.
|
|
|
|
---
|
|
|
|
## 📁 Struktur Direktori
|
|
|
|
```
|
|
prisma/
|
|
├── migrations/ # Database Migration History
|
|
│ ├── 20251228085634_add_users_model/
|
|
│ │ └── migration.sql # Initial database schema
|
|
│ ├── 20251228091450_add_menu_client_assignment/
|
|
│ │ └── migration.sql # Menu assignment feature
|
|
│ └── migration_lock.toml # Migration lock file
|
|
├── schema.prisma # Database Schema Definition
|
|
└── seed.ts # Database Seeder Script
|
|
```
|
|
|
|
---
|
|
|
|
## 📄 File Utama
|
|
|
|
### **`schema.prisma`** ⭐
|
|
|
|
**Fungsi**: Schema definition file - Blueprint utama database
|
|
|
|
**Location**: `/prisma/schema.prisma`
|
|
|
|
**Size**: ~2.6 KB
|
|
|
|
#### **Generator Configuration**:
|
|
|
|
```prisma
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../app/generated/client"
|
|
}
|
|
```
|
|
|
|
- Generate Prisma Client TypeScript
|
|
- Output ke `app/generated/client/` untuk import di aplikasi
|
|
|
|
#### **Datasource Configuration**:
|
|
|
|
```prisma
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
```
|
|
|
|
- Database: PostgreSQL
|
|
- Connection string dari environment variable `DATABASE_URL`
|
|
|
|
---
|
|
|
|
## 📊 Database Models
|
|
|
|
### **1. Model `users`** 👥
|
|
|
|
**Fungsi**: Menyimpan data pengguna (Coach & Client)
|
|
|
|
**Fields**:
|
|
| Field | Type | Description | Constraint |
|
|
|-------|------|-------------|------------|
|
|
| `id` | String | User ID unik | Primary Key, default: cuid() |
|
|
| `name` | String | Nama lengkap | VARCHAR, NOT NULL |
|
|
| `role` | String | Role pengguna | VARCHAR, "COACH" atau "CLIENT" |
|
|
| `coach_id` | String? | ID coach (untuk client) | Foreign Key → users.id |
|
|
| `created_at` | DateTime? | Waktu registrasi | Default: now() |
|
|
|
|
**Relations**:
|
|
|
|
- `coach` → Self-relation ke `users` (Many-to-One)
|
|
- Client memiliki 1 coach
|
|
- `clients` → Self-relation ke `users` (One-to-Many)
|
|
- Coach memiliki banyak clients
|
|
- `created_menus` → `training_menus[]` (One-to-Many)
|
|
- Coach dapat membuat banyak menu
|
|
- `assigned_menus` → `training_menus[]` (One-to-Many)
|
|
- Client dapat memiliki banyak assigned menus
|
|
- `recaps` → `user_recaps[]` (One-to-Many)
|
|
- User dapat memiliki banyak workout recaps
|
|
- `activity_logs` → `activity_logs[]` (One-to-Many)
|
|
- User dapat memiliki banyak activity logs
|
|
|
|
**Indexes**:
|
|
|
|
- `ix_users_id` on `id` (Primary)
|
|
- `ix_users_coach_id` on `coach_id` (Query optimization)
|
|
|
|
**ID Format**:
|
|
|
|
- Coach: `C00001`, `C00002`, etc.
|
|
- Client: `U00001`, `U00002`, etc.
|
|
|
|
**Business Logic**:
|
|
|
|
```typescript
|
|
// Coach can have multiple clients
|
|
Coach (C00001)
|
|
└─> Clients: [U00001, U00002]
|
|
|
|
// Client belongs to one coach
|
|
Client (U00001)
|
|
└─> Coach: C00001
|
|
```
|
|
|
|
---
|
|
|
|
### **2. Model `activity_logs`** 📝
|
|
|
|
**Fungsi**: Log aktivitas real-time (Standing/Sitting/Fall detection)
|
|
|
|
**Fields**:
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | Int | Auto-increment ID (Primary Key) |
|
|
| `timestamp` | DateTime? | Waktu log dibuat |
|
|
| `status` | String? | "Standing", "Sitting", "Fall Detected" |
|
|
| `confidence` | String? | Confidence score dari XGBoost |
|
|
| `details` | Json? | Additional metadata |
|
|
| `user_id` | String? | Foreign Key → users.id |
|
|
|
|
**Relations**:
|
|
|
|
- `user` → `users` (Many-to-One)
|
|
|
|
**Indexes**:
|
|
|
|
- `ix_activity_logs_id` on `id`
|
|
- `ix_activity_logs_user_id` on `user_id`
|
|
|
|
**Use Case**:
|
|
|
|
- Real-time monitoring di `/client/monitor`
|
|
- Historical activity tracking
|
|
- Fall detection alerts
|
|
|
|
**Example Data**:
|
|
|
|
```json
|
|
{
|
|
"id": 1,
|
|
"timestamp": "2025-12-28T10:30:00Z",
|
|
"status": "Standing",
|
|
"confidence": "0.95",
|
|
"details": {
|
|
"exercise": "bicep_curl",
|
|
"reps": 5
|
|
},
|
|
"user_id": "U00001"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### **3. Model `training_menus`** 📋
|
|
|
|
**Fungsi**: Menyimpan workout programs/menus yang dibuat coach
|
|
|
|
**Fields**:
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | Int | Auto-increment ID (Primary Key) |
|
|
| `name` | String? | Nama menu (e.g., "Upper Body Day 1") |
|
|
| `exercises` | Json? | Array of exercise objects |
|
|
| `created_at` | DateTime? | Waktu pembuatan |
|
|
| `author_id` | String? | Foreign Key → users.id (Coach yang buat) |
|
|
| `client_id` | String? | Foreign Key → users.id (Client assigned) |
|
|
|
|
**Relations**:
|
|
|
|
- `author` → `users` (Many-to-One, relation: "CreatedMenus")
|
|
- Menu dibuat oleh 1 coach
|
|
- `assigned_client` → `users` (Many-to-One, relation: "AssignedMenus")
|
|
- Menu ditugaskan kepada 1 client
|
|
- `user_recaps` → `user_recaps[]` (One-to-Many)
|
|
- Menu bisa memiliki banyak recap results
|
|
|
|
**Indexes**:
|
|
|
|
- `ix_training_menus_id` on `id`
|
|
- `ix_training_menus_name` on `name`
|
|
- `ix_training_menus_author_id` on `author_id`
|
|
|
|
**JSON Structure untuk `exercises` field**:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"name": "Bicep Curl",
|
|
"set_index": 1,
|
|
"reps": 10,
|
|
"weight": 15,
|
|
"rest": 60
|
|
},
|
|
{
|
|
"name": "Hammer Curl",
|
|
"set_index": 1,
|
|
"reps": 12,
|
|
"weight": 12,
|
|
"rest": 60
|
|
}
|
|
]
|
|
```
|
|
|
|
**Business Logic**:
|
|
|
|
```typescript
|
|
// Coach creates menu
|
|
Coach (C00001)
|
|
└─> Creates Menu (id: 1, "Upper Body")
|
|
└─> Assigns to Client (U00001)
|
|
|
|
// Client sees assigned menu
|
|
Client (U00001)
|
|
└─> Assigned Menu: "Upper Body" (id: 1)
|
|
```
|
|
|
|
---
|
|
|
|
### **4. Model `user_recaps`** 📈
|
|
|
|
**Fungsi**: Menyimpan hasil latihan (training recap) setelah workout selesai
|
|
|
|
**Fields**:
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | Int | Auto-increment ID (Primary Key) |
|
|
| `menu_id` | Int? | Foreign Key → training_menus.id |
|
|
| `user_id` | String? | Foreign Key → users.id |
|
|
| `summary` | Json? | Workout summary data |
|
|
| `completed_at` | DateTime? | Waktu workout selesai |
|
|
|
|
**Relations**:
|
|
|
|
- `training_menus` → `training_menus` (Many-to-One)
|
|
- Recap terkait dengan 1 menu
|
|
- `user` → `users` (Many-to-One)
|
|
- Recap milik 1 user
|
|
|
|
**Indexes**:
|
|
|
|
- `ix_user_recaps_id` on `id`
|
|
- `ix_user_recaps_user_id` on `user_id`
|
|
|
|
**JSON Structure untuk `summary` field**:
|
|
|
|
```json
|
|
{
|
|
"completed": true,
|
|
"exercises": [
|
|
{
|
|
"name": "Bicep Curl",
|
|
"set_index": 1,
|
|
"reps": 10,
|
|
"weight": 15,
|
|
"rest": 60
|
|
}
|
|
],
|
|
"timestamp": "2025-12-28T11:00:00Z",
|
|
"results": [
|
|
{
|
|
"name": "Bicep Curl",
|
|
"set": 1,
|
|
"reps": 10,
|
|
"weight": 15,
|
|
"score": 12.5,
|
|
"repDetails": [
|
|
{
|
|
"rep": 1,
|
|
"score": 8.2,
|
|
"feedback": "Perfect"
|
|
},
|
|
{
|
|
"rep": 2,
|
|
"score": 14.5,
|
|
"feedback": "Elbow moving forward"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Key Features di Summary**:
|
|
|
|
- ✅ Overall workout stats
|
|
- ✅ Per-set average form scores
|
|
- ✅ **Per-rep breakdown** dengan:
|
|
- Rep number
|
|
- MAE score
|
|
- Specific feedback text
|
|
|
|
---
|
|
|
|
## 🔄 Database Relationships Diagram
|
|
|
|
```mermaid
|
|
erDiagram
|
|
users ||--o{ users : "coach-client"
|
|
users ||--o{ training_menus : "creates (author)"
|
|
users ||--o{ training_menus : "assigned to (client)"
|
|
users ||--o{ user_recaps : "performs workout"
|
|
users ||--o{ activity_logs : "generates logs"
|
|
training_menus ||--o{ user_recaps : "tracked in recap"
|
|
|
|
users {
|
|
string id PK
|
|
string name
|
|
string role
|
|
string coach_id FK
|
|
}
|
|
|
|
training_menus {
|
|
int id PK
|
|
string name
|
|
json exercises
|
|
string author_id FK
|
|
string client_id FK
|
|
}
|
|
|
|
user_recaps {
|
|
int id PK
|
|
int menu_id FK
|
|
string user_id FK
|
|
json summary
|
|
}
|
|
|
|
activity_logs {
|
|
int id PK
|
|
string status
|
|
string user_id FK
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📁 Folder `migrations/`
|
|
|
|
### **Gambaran Umum**
|
|
|
|
Folder yang berisi history semua perubahan database schema.
|
|
|
|
### **Migration Files**:
|
|
|
|
#### **1. `20251228085634_add_users_model/migration.sql`**
|
|
|
|
**Tanggal**: 28 Desember 2025, 08:56:34
|
|
|
|
**Fungsi**: Initial database schema creation
|
|
|
|
**Changes**:
|
|
|
|
- ✅ Create table `users` dengan self-referencing foreign key
|
|
- ✅ Create table `activity_logs` dengan JSONB details
|
|
- ✅ Create table `training_menus` dengan JSONB exercises
|
|
- ✅ Create table `user_recaps` dengan JSONB summary
|
|
- ✅ Create indexes untuk optimization:
|
|
- User ID index
|
|
- Coach ID index
|
|
- Menu name index
|
|
- Author ID index
|
|
- User recap user_id index
|
|
- ✅ Add foreign key constraints dengan proper ON DELETE/UPDATE actions
|
|
|
|
**Key SQL**:
|
|
|
|
```sql
|
|
CREATE TABLE "users" (
|
|
"id" TEXT NOT NULL,
|
|
"name" VARCHAR NOT NULL,
|
|
"role" VARCHAR NOT NULL,
|
|
"coach_id" TEXT,
|
|
"created_at" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- Self-referencing foreign key
|
|
ALTER TABLE "users"
|
|
ADD CONSTRAINT "users_coach_id_fkey"
|
|
FOREIGN KEY ("coach_id") REFERENCES "users"("id")
|
|
ON DELETE SET NULL ON UPDATE CASCADE;
|
|
```
|
|
|
|
**Size**: ~2.7 KB
|
|
|
|
---
|
|
|
|
#### **2. `20251228091450_add_menu_client_assignment/migration.sql`**
|
|
|
|
**Tanggal**: 28 Desember 2025, 09:14:50
|
|
|
|
**Fungsi**: (Merged into previous migration)
|
|
|
|
**Status**: ⚠️ Skipped/No-op migration
|
|
|
|
**Reason**:
|
|
|
|
- Original purpose: Add `client_id` to `training_menus`
|
|
- Already included in first migration to fix type mismatches
|
|
- File kept untuk preserve migration history order
|
|
|
|
**Content**:
|
|
|
|
```sql
|
|
-- This migration is skipped because its changes (adding client_id)
|
|
-- were manually merged into the previous migration to fix type mismatches
|
|
-- This file is kept to preserve migration history order.
|
|
```
|
|
|
|
---
|
|
|
|
#### **`migration_lock.toml`**
|
|
|
|
**Fungsi**: Lock file untuk ensure consistent database provider
|
|
|
|
**Content**:
|
|
|
|
```toml
|
|
# Please do not edit this file manually
|
|
# It should be added in your version-control system (e.g., Git)
|
|
provider = "postgresql"
|
|
```
|
|
|
|
**Purpose**:
|
|
|
|
- Mencegah accidental switch ke database provider lain
|
|
- Ensure semua developer/environment menggunakan PostgreSQL
|
|
- Auto-generated oleh Prisma CLI
|
|
|
|
---
|
|
|
|
## 📄 File `seed.ts`
|
|
|
|
**Fungsi**: Database seeder untuk populate initial test data
|
|
|
|
**Location**: `/prisma/seed.ts`
|
|
|
|
**Size**: ~1.6 KB
|
|
|
|
### **Seed Data**:
|
|
|
|
**2 Coaches**:
|
|
|
|
1. `C00001` - "Coach One"
|
|
2. `C00002` - "Coach Two"
|
|
|
|
**3 Clients**:
|
|
|
|
1. `U00001` - "Client One" (assigned to Coach One)
|
|
2. `U00002` - "Client Two" (assigned to Coach One)
|
|
3. `U00003` - "Client Three" (assigned to Coach Two)
|
|
|
|
### **Script Logic**:
|
|
|
|
```typescript
|
|
import { PrismaClient } from "../app/generated/client/client";
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
async function main() {
|
|
// Upsert coaches
|
|
const coach1 = await prisma.users.upsert({
|
|
where: { id: "C00001" },
|
|
update: {},
|
|
create: {
|
|
id: "C00001",
|
|
name: "Coach One",
|
|
role: "COACH",
|
|
},
|
|
});
|
|
|
|
// Upsert clients with coach_id
|
|
const client1 = await prisma.users.upsert({
|
|
where: { id: "U00001" },
|
|
update: {},
|
|
create: {
|
|
id: "U00001",
|
|
name: "Client One",
|
|
role: "CLIENT",
|
|
coach_id: coach1.id,
|
|
},
|
|
});
|
|
}
|
|
```
|
|
|
|
### **Run Seeder**:
|
|
|
|
```bash
|
|
npx prisma db seed
|
|
```
|
|
|
|
**Use Case**:
|
|
|
|
- Development/testing data
|
|
- Demo accounts
|
|
- Quick reset database dengan data awal
|
|
|
|
---
|
|
|
|
## 🛠️ Prisma Commands
|
|
|
|
### **Essential Commands**:
|
|
|
|
#### **1. Generate Prisma Client**
|
|
|
|
```bash
|
|
npx prisma generate
|
|
```
|
|
|
|
- Regenerate TypeScript client dari schema
|
|
- Run setelah setiap perubahan `schema.prisma`
|
|
- Output ke `app/generated/client/`
|
|
|
|
#### **2. Create Migration**
|
|
|
|
```bash
|
|
npx prisma migrate dev --name migration_name
|
|
```
|
|
|
|
- Create migration file baru
|
|
- Apply migration ke database
|
|
- Update Prisma Client
|
|
|
|
#### **3. Apply Migration (Production)**
|
|
|
|
```bash
|
|
npx prisma migrate deploy
|
|
```
|
|
|
|
- Apply pending migrations
|
|
- Untuk production environment
|
|
- Tidak auto-generate client
|
|
|
|
#### **4. Reset Database**
|
|
|
|
```bash
|
|
npx prisma migrate reset
|
|
```
|
|
|
|
- Drop database
|
|
- Re-run all migrations
|
|
- Run seed script
|
|
- ⚠️ **DANGER**: Deletes all data!
|
|
|
|
#### **5. Prisma Studio (GUI)**
|
|
|
|
```bash
|
|
npx prisma studio
|
|
```
|
|
|
|
- Open web GUI untuk browse/edit data
|
|
- URL: `http://localhost:5555`
|
|
- Visual database management
|
|
|
|
#### **6. Format Schema**
|
|
|
|
```bash
|
|
npx prisma format
|
|
```
|
|
|
|
- Auto-format `schema.prisma`
|
|
- Fix indentation dan spacing
|
|
|
|
#### **7. Validate Schema**
|
|
|
|
```bash
|
|
npx prisma validate
|
|
```
|
|
|
|
- Check schema for errors
|
|
- Verify relations dan syntax
|
|
|
|
---
|
|
|
|
## 🔑 Key Concepts
|
|
|
|
### **Migration Strategy**:
|
|
|
|
1. **Development**:
|
|
- Use `prisma migrate dev`
|
|
- Creates migration + applies + generates client
|
|
- Safe untuk experiments
|
|
|
|
2. **Production**:
|
|
- Use `prisma migrate deploy`
|
|
- Never use `migrate dev` in prod
|
|
- Always test migrations di staging first
|
|
|
|
### **Schema Best Practices**:
|
|
|
|
1. **Naming Conventions**:
|
|
- Tables: `snake_case` (e.g., `training_menus`)
|
|
- Fields: `snake_case` (e.g., `coach_id`)
|
|
- Relations: `camelCase` di Prisma model (e.g., `assignedClient`)
|
|
|
|
2. **Indexes**:
|
|
- Add index untuk foreign keys
|
|
- Add index untuk frequently queried fields
|
|
- Consider composite indexes untuk complex queries
|
|
|
|
3. **JSONB Usage**:
|
|
- ✅ Good for: Flexible nested data (exercises, recap summary)
|
|
- ❌ Avoid for: Searchable/filterable data
|
|
- Use `Json` type di Prisma, becomes `JSONB` di PostgreSQL
|
|
|
|
4. **ID Strategy**:
|
|
- Users: Custom strings (`C00001`, `U00001`)
|
|
- Other tables: Auto-increment integers
|
|
- Consider UUID untuk distributed systems
|
|
|
|
---
|
|
|
|
## 📊 Database Size Estimates
|
|
|
|
**Assuming Active Usage**:
|
|
|
|
| Table | Rows/Month | Storage |
|
|
| ---------------- | ---------- | --------------------------- |
|
|
| `users` | ~10 | < 1 KB |
|
|
| `training_menus` | ~50 | ~5 KB |
|
|
| `user_recaps` | ~500 | ~500 KB (with per-rep data) |
|
|
| `activity_logs` | ~10,000 | ~1 MB |
|
|
|
|
**Total**: ~2 MB/month dengan moderate usage
|
|
|
|
**Optimization Tips**:
|
|
|
|
- Archive old `activity_logs` after 30 days
|
|
- Compress old recaps
|
|
- Add pagination untuk large queries
|
|
|
|
---
|
|
|
|
## 🔐 Security Considerations
|
|
|
|
### **Implemented**:
|
|
|
|
- ✅ Foreign key constraints prevent orphaned data
|
|
- ✅ Indexes prevent slow queries (avoid DOS)
|
|
- ✅ ON DELETE SET NULL untuk soft deletes
|
|
|
|
### **TODO / Recommendations**:
|
|
|
|
- [ ] Add `email` field dengan `@unique` constraint
|
|
- [ ] Add `password_hash` field (currently not in schema!)
|
|
- [ ] Add `role` as enum type instead of string
|
|
- [ ] Add `soft_delete` timestamp instead of hard delete
|
|
- [ ] Add row-level security (RLS) di PostgreSQL
|
|
|
|
---
|
|
|
|
## 🚀 Future Enhancements
|
|
|
|
### **Planned Schema Changes**:
|
|
|
|
1. **Add Authentication Fields**:
|
|
|
|
```prisma
|
|
model users {
|
|
email String @unique
|
|
password_hash String
|
|
email_verified Boolean @default(false)
|
|
}
|
|
```
|
|
|
|
2. **Add Workout Sessions** (untuk track progress over time):
|
|
|
|
```prisma
|
|
model workout_sessions {
|
|
id Int @id @default(autoincrement())
|
|
user_id String
|
|
menu_id Int
|
|
started_at DateTime
|
|
ended_at DateTime?
|
|
status String // "IN_PROGRESS" | "COMPLETED" | "ABANDONED"
|
|
}
|
|
```
|
|
|
|
3. **Add Exercise Library** (normalize exercises):
|
|
|
|
```prisma
|
|
model exercises {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
category String // "UPPER" | "LOWER" | "CORE"
|
|
instructions Json?
|
|
}
|
|
```
|
|
|
|
4. **Add Personal Records**:
|
|
|
|
```prisma
|
|
model personal_records {
|
|
id Int @id @default(autoincrement())
|
|
user_id String
|
|
exercise String
|
|
weight_kg Float
|
|
reps Int
|
|
achieved_at DateTime
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 💡 Troubleshooting
|
|
|
|
### **Common Issues**:
|
|
|
|
#### **1. Migration Failed**
|
|
|
|
```bash
|
|
Error: Migration failed to apply
|
|
```
|
|
|
|
**Solution**:
|
|
|
|
```bash
|
|
# Reset database
|
|
npx prisma migrate reset
|
|
|
|
# Or fix manually
|
|
npx prisma db push --skip-generate
|
|
npx prisma generate
|
|
```
|
|
|
|
#### **2. Client Out of Sync**
|
|
|
|
```bash
|
|
Error: Prisma Client is out of sync with schema
|
|
```
|
|
|
|
**Solution**:
|
|
|
|
```bash
|
|
npx prisma generate
|
|
```
|
|
|
|
#### **3. Connection Error**
|
|
|
|
```bash
|
|
Error: Can't reach database server
|
|
```
|
|
|
|
**Check**:
|
|
|
|
1. PostgreSQL running? (`sudo systemctl status postgresql`)
|
|
2. `DATABASE_URL` correct di `.env`?
|
|
3. Network/firewall issues?
|
|
|
|
#### **4. Seed Script Fails**
|
|
|
|
```bash
|
|
Error: Unique constraint violation
|
|
```
|
|
|
|
**Solution**: Data already exists
|
|
|
|
```bash
|
|
# Clear database first
|
|
npx prisma migrate reset
|
|
```
|
|
|
|
---
|
|
|
|
## 📚 Resources
|
|
|
|
**Official Docs**:
|
|
|
|
- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference)
|
|
- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
|
|
- [Prisma Client API](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)
|
|
|
|
**Tutorials**:
|
|
|
|
- [Prisma Quickstart](https://www.prisma.io/docs/getting-started/quickstart)
|
|
- [PostgreSQL Best Practices](https://wiki.postgresql.org/wiki/Best_practices)
|
|
|
|
---
|
|
|
|
## ✅ Checklist Maintenance
|
|
|
|
- [x] Schema documented
|
|
- [x] Migrations tracked in Git
|
|
- [x] Seed data available
|
|
- [ ] Backup strategy defined
|
|
- [ ] Migration rollback tested
|
|
- [ ] Production deployment guide
|
|
- [ ] Performance benchmarks
|