Aplikasi web multi-user untuk mengelola tugas pribadi. Mendukung register/login, Task List, Kanban, Dashboard, kategori, pencarian, filter, pengurutan, dan drag-and-drop status dengan isolasi data per akun. Seluruh antarmuka menggunakan Bahasa Indonesia.
- Frontend: React 19 + Vite 8
- Backend: Node.js + Express 5
- Database: Neon PostgreSQL melalui Drizzle ORM dan driver
pg - Autentikasi: sesi cookie
HttpOnly, Argon2id, dan CSRF token - Drag-and-drop:
@dnd-kit/core - Test: Vitest + Supertest + Testing Library
.
├── client/ # React + Vite
│ ├── src/components/ # Task List, Kanban, Dashboard, modal, filter
│ └── vite.config.js # Proxy /api ke backend
├── server/ # Express + PostgreSQL
│ ├── drizzle/ # Drizzle migration dan metadata
│ ├── scripts/ # Importer data SQLite
│ ├── src/ # API, auth, validasi, database, migrasi, repository
│ └── test/ # Unit dan integration test
└── package.json # npm workspaces dan script root
- Node.js 20.19 atau lebih baru (disarankan Node.js 24)
- npm 10 atau lebih baru
- Build tools untuk
better-sqlite3(makedang++) bila binary prebuilt tidak tersedia; dependency ini hanya dipakai dev tool importer dan test, bukan runtime server - Binary prebuilt
argon2untuk platform deployment; bila tidak tersedia, sediakan build tools native
npm installnpm run devPerintah ini menjalankan backend dan frontend sekaligus:
- Landing page: http://localhost:5173/
- Workspace: http://localhost:5173/app
- API: http://localhost:3001
- Proxy Vite meneruskan
/apike backend.
Jalankan migration database secara eksplisit sebelum backend pertama kali dipakai (lihat Migrasi Database). Server tidak menjalankan migration atau DDL saat start.
Salin contoh yang sesuai, lalu isi kredensial dari dashboard Neon:
cp server/.env.example server/.env
cp client/.env.example client/.env| Variabel | Environment | Keterangan |
|---|---|---|
PORT |
backend | Port API/backend; default 3001 |
DATABASE_URL |
backend runtime | Koneksi Neon pooled untuk API |
DATABASE_URL_UNPOOLED |
backend migration | Koneksi langsung Neon untuk migration dan importer |
CORS_ORIGIN |
backend | Daftar origin frontend yang diizinkan, dipisahkan koma; wildcard tidak diizinkan |
SESSION_TTL_HOURS |
backend | Masa berlaku sesi dalam jam; default 168 |
AUTH_COOKIE_SECURE |
backend | true pada production (HTTPS); false untuk development/test |
AUTH_COOKIE_SAME_SITE |
backend | Lax/Strict untuk topologi first-party; None hanya fallback cross-site dengan Secure=true |
AUTH_COOKIE_NAME |
backend | Nama cookie sesi; default ptm_session |
TEST_DATABASE_URL |
test | Koneksi pooled ke database/branch test khusus |
TEST_DATABASE_URL_UNPOOLED |
test | Koneksi langsung ke database/branch test khusus |
VITE_API_URL |
frontend | Base URL API saat deployment; kosongkan untuk path relatif dan proxy development |
VITE_API_TARGET |
frontend development | Target proxy /api; default http://localhost:3001 |
VITE_PUBLIC_DEPLOYMENT |
frontend build | Menampilkan landing publik; bukan authentication atau security boundary. /app tetap memerlukan sesi valid. |
Jangan menulis kredensial nyata ke file contoh atau repository. Gunakan server/.env untuk development, server/.env.test untuk test, dan environment variables platform untuk deployment. Database test harus berupa branch/database Neon terpisah dan tidak boleh menunjuk production.
Contoh development dengan port berbeda:
PORT=4000 VITE_API_TARGET=http://localhost:4000 npm run devJika hanya backend yang dijalankan dengan port berbeda, sesuaikan juga VITE_API_TARGET agar frontend tetap terhubung.
CORS_ORIGINberisi daftar origin frontend yang diizinkan, dipisahkan koma, misalnyahttp://localhost:5173,https://aplikasi.example.com. Gunakan origin eksplisit tanpa path dan tanpa wildcard*; backend mengirim credentials (cookie sesi) hanya untuk origin tersebut dan menolak request mutasi tanpaOriginyang cocok.- Request tanpa header
Origin, termasuk yang melewati proxy Vite, tetap diproses sebagai request same-origin. VITE_API_URLopsional dan dibaca saat build. Jika kosong, frontend memanggil path relatif/api/...sehingga proxy Vite development atau backend yang menyajikanclient/distmenangani request tersebut. Jika diisi, trailing slash dinormalisasi agar URL tidak memiliki slash ganda.VITE_API_TARGEThanya dipakai Vite development sebagai target proxy/api; nilai default-nyahttp://localhost:3001. Restartnpm run devsetelah mengubahnya.- Untuk deployment terpisah, set
CORS_ORIGINke origin frontend dan gunakan topologi cookie first-party (domain same-site atau rewrite/proxy/apisame-origin). Jangan hardcode URL platform tertentu atau memakai wildcard.
Aplikasi bersifat multi-user. Setiap pengguna memiliki akun, tugas, kategori, dan statistik sendiri.
POST /api/auth/registermembuat akun, tiga kategori bawaan, dan sesi.POST /api/auth/loginmemverifikasi kredensial dan membuat sesi baru.POST /api/auth/logoutmencabut sesi aktif dan menghapus cookie.GET /api/auth/memengembalikan pengguna aktif dan token CSRF.- Password disimpan sebagai hash Argon2id dan tidak pernah dikembalikan API atau dicatat log.
- Token sesi berupa nilai acak, disimpan sebagai hash di database, dan dikirim melalui cookie
HttpOnly. SESSION_TTL_HOURS,AUTH_COOKIE_SECURE,AUTH_COOKIE_SAME_SITE, danAUTH_COOKIE_NAMEmengatur masa berlaku serta atribut cookie.AUTH_COOKIE_SECUREwajibtruepadaNODE_ENV=production.- Register/login mewajibkan
Originyang diizinkan danContent-Type: application/json. - Logout dan seluruh mutasi data terautentikasi mewajibkan header
X-CSRF-Tokenyang cocok dengan sesi. - Respons
401berarti sesi tidak valid,403berarti kegagalan CSRF/Origin, dan429berarti percobaan autentikasi dibatasi sementara. - Seluruh endpoint data memfilter berdasarkan pengguna yang login; data milik pengguna lain menghasilkan
404.
Cookie sesi wajib bersifat first-party pada deployment yang didukung:
- frontend dan backend memakai subdomain dari registrable domain yang sama (misalnya
app.example.comdanapi.example.com), atau - frontend meneruskan
/apike backend melalui rewrite/proxy same-origin.
SameSite=None; Secure hanya dipakai sebagai fallback bila cross-site tidak dapat dihindari. Safari ITP, Firefox ETP, dan pembatasan cookie pihak ketiga di Chromium dapat memblokir cookie tersebut ketika frontend dan backend berada pada situs berbeda. Konfigurasi cross-site tanpa domain same-site atau proxy same-origin dianggap tidak didukung.
# Membuat file SQL migration dari server/src/schema.js.
npm run db:generate -w server
# Menerapkan migration yang belum berjalan.
npm run db:migrate -w server
# Alias dari root workspace.
npm run migratedb:migrate dan npm run migrate memakai DATABASE_URL_UNPOOLED, yaitu koneksi langsung Neon tanpa -pooler, karena migration tidak boleh melewati PgBouncer. Migration bersifat idempotent, dicatat di drizzle.schema_migrations, dan hanya dijalankan secara eksplisit; server tidak menjalankan DDL saat start.
Untuk menerapkan migration ke Neon test branch, jalankan perintah dengan koneksi langsung test tanpa menulis kredensial ke repository:
DATABASE_URL_UNPOOLED="$TEST_DATABASE_URL_UNPOOLED" npm run db:migrate -w serverIntegration test migration menjalankan langkah ini secara otomatis dari server/.env.test.
Importer membaca server/data/tasks.db secara read-only, memakai DATABASE_URL_UNPOOLED, dan menulis kategori lalu tugas dalam satu transaksi. Sumber SQLite tidak diubah. better-sqlite3 dipertahankan hanya sebagai dev dependency untuk importer dan test; jalur runtime SQLite sudah dihapus, sehingga API selalu memakai Neon PostgreSQL.
Lakukan prosedur berikut dalam maintenance window:
-
Hentikan backend agar tidak ada tulisan baru ke sumber SQLite selama cutover.
-
Buat backup bertimestamp dan simpan checksum sumber:
mkdir -p server/data/backups cp server/data/tasks.db "server/data/backups/tasks-$(date -u +%Y%m%dT%H%M%SZ).db" sha256sum server/data/tasks.db -
Buat snapshot atau branch Neon sebagai titik rollback database target.
-
Jalankan migration ke database Neon yang disetujui, lalu periksa hasil tanpa menulis:
npm run db:migrate -w server npm run db:import -w server -- --dry-run
-
Periksa laporan dry-run: jumlah kategori/tugas, relasi, status, priority, deadline, timestamp, dan status kecocokan target. Target hasil migration berisi seed kategori bawaan, sehingga dry-run akan menyarankan
--replace. -
Jalankan impor pengganti di dalam transaksi:
npm run db:import -w server -- --replace
-
Verifikasi ulang tanpa menulis; hasil yang diharapkan adalah target sesuai sumber:
npm run db:import -w server -- --dry-run
-
Jalankan smoke test API terhadap data hasil impor sebelum mengizinkan pengguna menulis kembali. Pertahankan backup SQLite dan snapshot Neon sampai rollback window ditutup.
Gunakan --source <path> atau argumen path untuk memilih file SQLite lain. Importer menolak target non-kosong tanpa --replace; kegagalan di tengah proses membatalkan seluruh perubahan target. Jangan menaruh kredensial pada argumen command line atau dokumentasi.
Rollback: hentikan backend lalu pulihkan snapshot/branch Neon. Runtime SQLite sudah dihapus dari kode saat ini; jika benar-benar perlu kembali ke versi SQLite, checkout revisi Git terakhir yang masih mendukung SQLite (42d2d76398f4d943412ded2c4512addb1e94fd58) dan gunakan backup SQLite. Tulisan yang dibuat setelah cutover hanya dapat dipertahankan jika ada prosedur sinkronisasi terpisah; maintenance window meminimalkan risiko ini.
Retensi backup: server/data/tasks.db, checksum, dan snapshot Neon dipertahankan sampai rollback window ditutup dan disetujui. File tersebut berada di server/data/ yang di-ignore Git, jadi simpan salinannya di storage terpisah bila diperlukan untuk retensi jangka panjang.
npm testPerintah lain:
npm run test:unit # Unit test server + test client
npm run test:integration # Integration test API dan persistensiUnit test tidak membutuhkan database. Integration test membaca server/.env.test dan memakai TEST_DATABASE_URL (pooled) serta TEST_DATABASE_URL_UNPOOLED (langsung) ke Neon branch test khusus. Test akan menolak berjalan jika NODE_ENV=production, jika URL test sama dengan URL runtime/production, atau jika kedua URL test tidak berasal dari branch yang sama.
Integration test membuat schema dari nol dan mereset data dengan TRUNCATE sessions, tasks, categories, users RESTART IDENTITY CASCADE; karena itu jangan pernah mengarahkan environment test ke database production. File test dijalankan serial agar state database test tidak saling bertabrakan. Test mencakup validasi, CRUD tugas dan kategori, filter, pencarian, pengurutan, overdue, statistik dashboard, kontrak respons API, register/login/logout, session, CSRF, migration, isolasi antar pengguna, serta persistensi setelah koneksi ditutup dan dibuka kembali.
npm run build
npm startnpm run build menghasilkan client/dist. npm start menjalankan backend pada http://localhost:3001; backend juga menyajikan file statis dari client/dist, sehingga aplikasi dapat dibuka langsung dari port tersebut tanpa server frontend terpisah. Landing tersedia di /, halaman autentikasi di /login dan /register, dan workspace di /app hanya untuk sesi valid.
Untuk menjalankan versi production-like secara lokal, isi server/.env dengan konfigurasi lengkap dari server/.env.example, jalankan migration secara eksplisit, lalu start backend. Server tidak menjalankan migration atau DDL saat start. Health check tersedia di GET /api/health dan hanya mengembalikan status konektivitas database.
Script database dapat dipanggil dari root workspace:
npm run db:generate # generate migration Drizzle
npm run db:migrate # terapkan migration memakai DATABASE_URL_UNPOOLED
npm run db:migrate:status -w server # cek migration applied/pending (read-only)
npm run db:import -- --dry-run
npm run db:import -- --replace
npm run db:studio # buka Drizzle Studio
npm run migrate # alias db:migratePanduan lengkap tersedia di DEPLOYMENT.md.
Production menggunakan satu project Vercel untuk frontend dan backend:
- Frontend: Vercel, root repository, build
npm run build -w client, outputclient/dist - Backend: Vercel Function
api/[...path].jsyang mengekspor Express app tanpaapp.listen() - API: same-origin
/api/*pada domain Vercel - Database: Neon PostgreSQL production
Konfigurasi Vercel:
- Root Directory: root repository (bukan
client) - Install Command:
npm ci --include=dev - Build Command:
npm run build -w client - Output Directory:
client/dist - Framework: Vite
Environment Vercel:
NODE_ENV=productionDATABASE_URL— Neon pooledDATABASE_URL_UNPOOLED— Neon direct (diperlukan validasi konfigurasi runtime)CORS_ORIGIN=https://app-tugas.vercel.appSESSION_TTL_HOURS=168AUTH_COOKIE_SECURE=trueAUTH_COOKIE_SAME_SITE=LaxAUTH_COOKIE_NAME=ptm_sessionVITE_PUBLIC_DEPLOYMENT=trueVITE_API_URLkosong (same-origin/api)
Install Command memakai npm ci --include=dev karena Vite berada di devDependencies dan build production dapat menghilangkan devDependencies. api/[...path].js memakai pool PostgreSQL pada module scope dengan max: 1. Migration production sudah diterapkan; Function tidak menjalankan migration. Jangan menjalankan reset, drop, importer, seed, atau migration tanpa alasan.
Keamanan:
- Cookie sesi
HttpOnly,Secure,SameSite=Lax, first-party pada domain Vercel. - Tidak memakai localStorage untuk session.
- CORS credentials aktif dan
CORS_ORIGINharus origin Vercel yang sebenarnya tanpa trailing slash. - CSRF dan ownership filtering tetap berlaku.
Verifikasi setelah deploy:
GET /api/health→200 {"status":"ok"}./api/taskstanpa sesi →401, bukan HTML SPA./login,/register,/appdimuat sebagai SPA.- Register, login, logout, CRUD, dan isolasi dua akun berfungsi.
| Method | Endpoint | Fungsi |
|---|---|---|
POST |
/api/auth/register |
Membuat akun, kategori bawaan, dan sesi |
POST |
/api/auth/login |
Login dan membuat sesi |
POST |
/api/auth/logout |
Mencabut sesi aktif |
GET |
/api/auth/me |
Identitas pengguna aktif dan token CSRF |
GET |
/api/tasks |
Daftar tugas pengguna dengan pencarian, filter, dan sort |
POST |
/api/tasks |
Membuat tugas |
PATCH |
/api/tasks/:id |
Mengubah sebagian atau seluruh field tugas |
DELETE |
/api/tasks/:id |
Menghapus tugas |
GET |
/api/categories |
Daftar kategori terurut |
POST |
/api/categories |
Membuat kategori |
PATCH |
/api/categories/:id |
Mengubah nama kategori |
DELETE |
/api/categories/:id |
Menghapus kategori; tugas terkait menjadi tanpa kategori |
GET |
/api/dashboard/stats |
Statistik pengguna yang sedang login |
GET |
/api/health |
Health check konektivitas database; 200 { "status": "ok" } atau 503 { "status": "unhealthy" } |
Respons sukses memakai bentuk { "success": true, "data": ... }. Respons gagal memakai { "success": false, "message": "...", "errors": ... } dengan status HTTP 400, 401 (sesi tidak valid), 403 (CSRF/Origin gagal), 404, 409, 429 (rate limit), atau 500. Pengecualian: GET /api/health memakai body status khusus dan tidak memakai envelope API.
Aplikasi bersifat multi-user dan memerlukan login. Data disimpan di Neon PostgreSQL. Setiap tugas dan kategori memiliki user_id; API hanya menampilkan data milik pengguna yang login.
Untuk mereset data pada database development/test, jalankan migration lalu:
TRUNCATE sessions, tasks, categories, users RESTART IDENTITY CASCADE;atau buat branch Neon baru. Jangan mereset database production tanpa backup dan persetujuan operasional. Backup SQLite dan snapshot Neon dipertahankan sampai rollback window ditutup.
Data hasil migrasi yang memiliki user_id NULL dipertahankan tetapi tidak terlihat dan tidak dapat diubah oleh pengguna mana pun. Registrasi akun baru tidak mengambil data tersebut secara otomatis. Operator dapat menetapkan pemilik secara eksplisit setelah membuat akun tujuan:
UPDATE tasks SET user_id = '<USER_UUID>' WHERE user_id IS NULL;
UPDATE categories SET user_id = '<USER_UUID>' WHERE user_id IS NULL;Jalankan hanya setelah backup dan verifikasi; jangan menaruh UUID/kredensial nyata di repository.
Hasil uji otomatis, walkthrough acceptance criteria, verifikasi browser, dan pemeriksaan responsif dicatat di openspec/changes/personal-task-manager-mvp/verification.md.