Backend API Kamus Digital Sambas-Indonesia.
Stack: Hono + Drizzle ORM + Turso (libSQL / SQLite), clean architecture
feature-based (src/modules/<fitur>/). Dokumentasi API interaktif di
GET /docs (Scalar) saat server jalan.
Kode dan dokumentasi di repo ini ditulis bersama agen AI. Manusia tetap yang memutuskan arah produk, meninjau setiap perubahan, dan menjalankan verifikasi sebelum rilis. Tidak ada agen yang punya akses commit, merge, atau deploy otomatis. Semua keputusan akhir ada di tangan pengelola.
Lokal & CI memakai file SQLite. Staging/production memakai Turso Cloud.
| Environment | DATABASE_URL |
Token |
|---|---|---|
| Development | file:./local.db |
kosong |
| Test (integration) | file:./test.db |
kosong |
| Staging / Prod | libsql://….turso.io |
DATABASE_AUTH_TOKEN (wajib) |
Tidak perlu Colima/Docker Postgres. File *.db di-gitignore.
pnpm install
# 1. Environment
cp .env.example .env
# Pastikan: DATABASE_URL=file:./local.db
# DATABASE_AUTH_TOKEN=
# 2. Generate JWT keypair dev (RS256)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out jwt_private.pem
openssl pkey -in jwt_private.pem -pubout -out jwt_public.pem
# Salin isi kedua file ke .env (JWT_PRIVATE_KEY / JWT_PUBLIC_KEY,
# bungkus dengan tanda kutip dua), lalu hapus file key-nya
# 3. Apply migration → membuat/mengisi local.db
pnpm db:migrate
# 4. Seed (lokal): akun default + referensi/user sistem
pnpm seed
# Atau terpisah: pnpm seed:accounts | pnpm seed:reference
# 5. Jalankan API (Hono + Node)
pnpm dev # http://localhost:3000 - docs di /docs
# Health: GET /health → { "status": "ok", "database": "up" }
# Deploy ID: GET / atau GET /api/v1/ping → data.gitSha (bake CI)cp .env.test.example .env.test
# DATABASE_URL=file:./test.db + JWT key (sama pola .env)
DATABASE_URL=file:./test.db pnpm db:migrate
pnpm test| Command | Fungsi |
|---|---|
pnpm dev |
Jalankan API (watch mode, Node + file SQLite) |
pnpm dev:worker |
Jalankan via wrangler (paritas Cloudflare Workers) |
pnpm deploy / pnpm deploy:staging |
Deploy ke Cloudflare Workers |
pnpm test |
Semua test (unit + integration + e2e) |
pnpm test:unit / test:integration / test:e2e |
Test per lapisan |
pnpm typecheck |
tsc --noEmit |
pnpm seed |
Lokal: accounts + reference |
pnpm seed:accounts |
Akun default (upsert; prod CI: insert-only via env) |
pnpm seed:reference |
Referensi + user sistem (insert-if-missing) |
pnpm drizzle-kit generate --name=… |
Generate migration SQL dari perubahan schema |
pnpm db:migrate |
Apply migration (libsql; error terlihat di CI) |
pnpm drizzle-kit studio |
GUI browser untuk lihat isi database |
Dua runtime berbagi satu composition root: main.ts (Node, default dev)
dan worker.ts (Workers - env dari bindings, DB via @libsql/client/web
singleton HTTP ke Turso, email via Resend).
Migration tetap dari CI Node (pnpm db:migrate), bukan dari Workers.
npx wrangler secret put DATABASE_URL --env staging
# paste: libsql://sambasku-staging-….turso.io
npx wrangler secret put DATABASE_AUTH_TOKEN --env staging
# paste: token dari `turso db tokens create sambasku-staging`
# JWT, Resend, ImageKit, OAuth, FCM - sama seperti sebelumnya
# Audio pelafalan (lihat bagian "Audio pelafalan" di bawah):
# npx wrangler secret put PRONUNCIACION_GITHUB_TOKEN --env stagingStaging (NODE_ENV=staging): email tidak dikirim via Resend (hemat free
tier). Semua OTP (verifikasi, reset password, hapus akun) tetap
111-111.
Push ke branch staging →
.github/workflows/deploy-staging.yml:
test (file SQLite) → migrate (DB_STAGING_TURSO_*) → image/deploy staging.
| Secret | Nilai |
|---|---|
CLOUDFLARE_API_TOKEN |
CF → API Tokens → Edit Cloudflare Workers |
CLOUDFLARE_ACCOUNT_ID |
547e27ab971bbd809dfd57626049a131 |
DB_STAGING_TURSO_URL |
URL libSQL staging (org secret, prioritas) |
DB_STAGING_TURSO_AUTH_TOKEN |
token staging |
DB_PRODUCTION_TURSO_URL |
URL libSQL production (org) |
DB_PRODUCTION_TURSO_AUTH_TOKEN |
token production |
TURSO_STAGING_* / TURSO_PRODUCTION_* |
fallback lama (opsional) |
Secret Worker (JWT, dll.) tidak ikut CI - wrangler deploy mempertahankan
secret yang sudah terpasang. Vars non-secret (termasuk
OAUTH_REQUIRE_AZP=false default / grace) ada di wrangler.toml
[vars] / [env.staging.vars]. Seed manual (staging):
# Aman diulang - Anonim, Pengimpor Data CSV, bahasa/dialek/kelas/kategori
gh workflow run seed-reference-staging.yml --ref staging
# Hanya jika perlu reset password akun default → pass1234
gh workflow run seed-account-staging.yml --ref staging -f confirm=trueProduction: seed-reference-production.yml / seed-account-production.yml (--ref main).
seed-account-production insert-only (SEED_ACCOUNTS_INSERT_ONLY=1) - tidak menimpa user yang sudah ada.
File audio tidak disimpan di Turso. Backend menulis file ke repo publik
sambasku/audios,
lalu menyimpan record metadata + URL di tabel word_audios.
README asset (asal audio, struktur path, URL raw):
pronunciation/README.md ·
raw di GitHub.
Record metadata + URL audio tersimpan di tabel word_audios.
PRONUNCIACION_PROVIDER=github
PRONUNCIACION_GITHUB_URL=https://github.com/sambasku/audios
PRONUNCIACION_GITHUB_TOKEN= # PAT Contents RW - secret, jangan commitStaging: PRONUNCIACION_PROVIDER + PRONUNCIACION_GITHUB_URL di
wrangler.toml [env.staging.vars]; token via
npx wrangler secret put PRONUNCIACION_GITHUB_TOKEN --env staging.
Tanpa token/URL → upload balas 503 PRONUNCIACION_UPLOAD_UNAVAILABLE.
Client (admin / mobile)
→ POST /api/v1/words/:wordId/pronunciations/audio (multipart)
→ API validasi MIME/ukuran
→ GitHub Contents API PUT assets/audio/<dialect|umum>/<lemma-slug>/<ulid>.<ext>
→ INSERT word_audios (url = cdn.jsdelivr.net/gh/…@main/<path>, …)
Field di word_audios (inti) |
Arti |
|---|---|
url |
Link asset publik (jsDelivr CDN) |
path / path di storage |
Path relatif di repo pronunciation |
word_id |
Lemma induk |
example_id |
null = audio lemma; terisi = audio contoh kalimat |
dialect_id |
Dialek opsional |
mime_type / file_size / duration_ms |
Metadata file |
speaker_name |
Nama penutur (opsional) |
is_primary |
Take pertama per target (lemma / per-example) |
status |
pending_review (contributor) / published (admin, …) |
Contoh URL asset setelah upload:
https://cdn.jsdelivr.net/gh/sambasku/audios@main/assets/audio/umum/makatn/<ulid>.m4a
(jsDelivr CDN di depan repo GitHub publik; path file immutable / ULID.)
Baca di word detail: audios[] (lemma) dan
meanings[].examples[].audios[] (contoh).
| Method | Path | Keterangan |
|---|---|---|
POST |
/api/v1/words/:wordId/pronunciations/audio |
Upload + buat record |
DELETE |
/api/v1/words/:wordId/pronunciations/audio/:audioId |
Soft-delete DB + hapus file (best-effort) |
File gambar kata dan avatar tidak disimpan di Turso. Backend menulis file ke repo publik sambasku/images, lalu menyimpan URL + path di database.
README asset (asal gambar, struktur path, URL CDN):
images/README.md ·
raw di GitHub.
Pola yang sama dengan audio pelafalan di atas. Bedanya: gambar kata tidak punya slug lemma (ULID dibuat sebelum kata tersimpan), dan tampilan client membungkus URL jsDelivr dengan wsrv.nl.
PUBLIC_IMAGE_PROVIDER=github
PUBLIC_IMAGE_GITHUB_URL=https://github.com/sambasku/images
PUBLIC_IMAGE_GITHUB_TOKEN= # PAT Contents RW, secret, jangan commitLokal: isi ketiga variabel di .env (lihat .env.example).
Di Workers, provider + URL jadi vars; token lewat
npx wrangler secret put PUBLIC_IMAGE_GITHUB_TOKEN.
Tanpa token/URL → upload balas 503 PUBLIC_IMAGE_UPLOAD_UNAVAILABLE.
Bukti verifikator dan lampiran laporan bug tetap ImageKit
(IMAGE_PROVIDER / IMAGEKIT_*). Jangan campur ke repo ini.
Client (admin / mobile)
→ POST /api/v1/images?purpose=word (gambar kata)
atau POST /api/v1/users/me/avatar (avatar)
→ API validasi MIME/ukuran (jpeg, png, webp, maks 5 MB)
→ GitHub Contents API PUT
assets/words/<ulid>.<ext>
assets/avatars/<userId>/<ulid>.<ext>
→ Client mengirim url + provider_file_id (+ sha) saat menyimpan kata
atau API langsung menulis users.avatar_url
| Field | Arti |
|---|---|
word_images.url / users.avatar_url |
Link aset publik (jsDelivr) |
provider |
github |
provider_file_id |
Path relatif di repo images |
sha |
Blob SHA, dipakai saat hapus |
Contoh URL aset setelah upload:
https://cdn.jsdelivr.net/gh/sambasku/images@main/assets/words/<ulid>.webp
| Method | Path | Keterangan |
|---|---|---|
POST |
/api/v1/images?purpose=word |
Upload gambar kata ke GitHub |
POST |
/api/v1/words/:wordId/images |
Simpan referensi ke word_images |
POST |
/api/v1/users/me/avatar |
Upload avatar + update user |
DELETE |
/api/v1/users/me/avatar |
Hapus avatar (file best-effort) |
Lokal
# CLI sqlite (kalau terpasang)
sqlite3 local.db
# Atau Drizzle Studio
DATABASE_URL=file:./local.db pnpm drizzle-kit studioTurso Cloud
turso db shell sambasku-staging
# atau dashboard: https://turso.techsrc/
├── modules/auth/ # fitur auth (domain → application → infrastructure → presentation/v1)
├── shared/ # lintas modul: db (libSQL), middlewares, errors, config, logging
├── scripts/seed.ts # orkestrasi lokal (accounts + reference)
├── scripts/seed-accounts.ts # akun default (upsert; insert-only via env)
├── scripts/seed-reference.ts # referensi + user sistem (insert-if-missing)
├── app.ts # composition root (dibagi 2 runtime)
└── main.ts / worker.ts # entry Node (@hono/node-server) / Cloudflare Workers
Migrasi Postgres lama (arsip, jangan di-apply):
src/shared/database/drizzle/migrations-pg-archive/.
Lineage aktif: migrations/0000_init-turso.sql.
