Skip to content
sambaskuPublic

Repository files navigation

SambasKu

SambasKu-API

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.

Dikerjakan dengan AI

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.

Database (tanpa Docker)

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.

Setup Pertama Kali

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)

Database test (integration & e2e)

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 Sehari-hari

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

Cloudflare Workers (deploy)

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.

Secret Worker (staging) - sekali

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 staging

Staging (NODE_ENV=staging): email tidak dikirim via Resend (hemat free tier). Semua OTP (verifikasi, reset password, hapus akun) tetap 111-111.

Deploy otomatis dari GitHub (CI/CD)

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=true

Production: 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.

Audio pelafalan (GitHub asset repo)

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.

Env (lokal / Workers)

PRONUNCIACION_PROVIDER=github
PRONUNCIACION_GITHUB_URL=https://github.com/sambasku/audios
PRONUNCIACION_GITHUB_TOKEN=          # PAT Contents RW - secret, jangan commit

Staging: 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.

Alur record (DB + asset)

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).

Endpoint ringkas

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)

Gambar publik (GitHub asset repo)

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.

Env (lokal / Workers)

PUBLIC_IMAGE_PROVIDER=github
PUBLIC_IMAGE_GITHUB_URL=https://github.com/sambasku/images
PUBLIC_IMAGE_GITHUB_TOKEN=          # PAT Contents RW, secret, jangan commit

Lokal: 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.

Alur record (DB + asset)

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

Endpoint ringkas

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)

Akses Database

Lokal

# CLI sqlite (kalau terpasang)
sqlite3 local.db

# Atau Drizzle Studio
DATABASE_URL=file:./local.db pnpm drizzle-kit studio

Turso Cloud

turso db shell sambasku-staging
# atau dashboard: https://turso.tech

Struktur Singkat

src/
├── 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.

Releases

Packages

Contributors

Languages