Skip to main content

Infrastructure Document

:::info Status Locked v1.0 — stack, face runtime, FCM, R2, ERD, timezone, dan kontrak OpenAPI terkunci untuk MVP. :::

FieldValue
ProductNexus Ops — Aplikasi Absensi Divisi Operation
Version1.0.0
TimKelompok B — Capstone Project 50 Team B 2026
Last updated2026-09-24

1. Tujuan dokumen​

Dokumen ini mendeskripsikan arsitektur infrastruktur Nexus Ops: komponen sistem, lingkungan, penyimpanan data, keamanan, observabilitas, dan deployment. Semua keputusan ADR di §10 terkunci untuk semester ini.

Dokumen terkait: PRD · Dev Setup · Git Workflow · SDLC


2. Ringkasan arsitektur​

LapisanKomponenTeknologi
MobileAplikasi absensi karyawan (Android)React Native + Expo (SDK 57) · Expo Router · TypeScript · Orval · vision-camera + fast-tflite (face)
WebDashboard admin / supervisor / HRDVue 3 + Vite + TypeScript · Vue Router · Orval · Vitest · Playwright
APIRESTful backendBun + TypeScript + Hono · Zod→OpenAPI · DDD modules
DataRelational DBNeon (PostgreSQL) · Drizzle ORM
AI/CVFace recognitionOn-device MobileFaceNet → embedding; Worker cosine similarity
LokasiGPS + geofencingDevice GPS + validasi server (Haversine / radius)
StorageEkspor laporan (Excel/PDF)Cloudflare R2 (signed URL) — bukan foto wajah
PushNotifikasiFirebase Cloud Messaging (FCM) HTTP v1
DesignUI/UXFigma (+ generator di docs/figma-plugin)
VCS / CIKolaborasi & otomatisasiGitHub · GitHub Actions

Diagram konteks​

Alur validasi absensi (server) — terkunci​


3. Komponen & tanggung jawab​

3.1 Mobile application (bootstrapped)​

AspekKeputusan
StackReact Native + Expo SDK 57 · TypeScript · Expo Router (app/)
Packagenpm (packageManager: npm@10.9.0)
API clientOrval → Axios dari openapi/openapi.json
Auth tokenAsyncStorage key nexus_ops_token (secure storage OS dapat ditambah nanti)
Facereact-native-vision-camera + vision-camera-resize-plugin + react-native-fast-tflite · model MobileFaceNet .tflite di assets/models/ (112×112 → embedding 192-d, L2-normalized)
Dev clientExpo Go tidak cukup untuk face/camera native — wajib dev client / debug APK (expo prebuild)
EnvEXPO_PUBLIC_APP_ENV, EXPO_PUBLIC_API_BASE_URL
QualityESLint · Prettier · Husky · Jest (src/lib coverage ≥ 95%) · E2E Maestro (manual gate vs APK; bukan CI)
ArtefakGitHub Actions APK: nightly → dev · tag v* → stg · manual dispatch (android/ tidak di-commit)
Target OSAndroid (MVP)

3.2 Web dashboard (bootstrapped)​

AspekKeputusan
StackVue 3 + Vite + TypeScript · Vue Router
Packagenpm
API clientOrval → Axios dari openapi/openapi.json
EnvVITE_APP_ENV, VITE_API_BASE_URL, VITE_BASE_PATH
QualityESLint · Prettier · Husky · Vitest (src/lib coverage ≥ 95%) · E2E Playwright (job CI dengan API mock via page.route)
DeployCloudflare Pages — trunk-based (main → dev-nexus-ops-web, tag v* → nexus-ops-web)

3.3 Backend API (implemented skeleton)​

AspekKeputusan
Runtime lokalBun (bun run dev)
Runtime produksiCloudflare Workers (src/worker.ts + Wrangler)
Framework HTTPHono + @hono/zod-openapi
OpenAPIAuto dari Zod schemas → /openapi.json + UI /docs
AuthJWT (HS256, jose) · RBAC: employee / supervisor / hrd · sesi 12 jam
Passwordbcryptjs
ORMDrizzle
DB driver Worker@neondatabase/serverless (HTTP)
Atomicitydb.batch() + invariant DB (unique / guarded UPDATE … RETURNING) — bukan interactive multi-statement TX
ArsitekturPure DDD — src/modules/<bounded_context>/<aggregate>/

Modul:

ContextAggregateStatus
identityuserLogin + /v1/auth/me
opsdashboardContract skeleton (501 sampai impl)
attendancerecordPlaceholder → S3
leaverequestPlaceholder → S4
overtimerequestPlaceholder → S5
locationgeofencePlaceholder → S6

Endpoint hidup hari ini: GET /health, POST /v1/auth/login, GET /v1/auth/me, stubs dashboard.

3.4 Face recognition — terkunci (D-05)​

AspekKeputusan
RuntimeOn-device MobileFaceNet (TFLite); Worker hanya cosine similarity
EnrollmentPOST /v1/face/enrollments body { embeddings: number[][] } — 3 sampel → simpan sampel + mean
Clock-in/outPayload face: { embedding: number[], quality: number }
ThresholdCosine ≥ FACE_MATCH_THRESHOLD (default 0.65)
Escape hatchFACE_MODE=stub → terima embedding apa pun, catat face_result = skipped (untuk native build yang meleset)
FotoTidak disimpan (lihat D-09)
Limitasi MVPEmbedding dari klien dipercaya; mitigasi = JWT + rate limit + audit; anti-spoof out of scope

3.5 GPS geofencing​

  • Device mengirim lat, lng, accuracy_m
  • Server: titik dalam radius lokasi aktif org; default radius 100 m; reject jika accuracy_m > 50
  • Reason code: OUT_OF_GEOFENCE, LOW_GPS_ACCURACY (lihat PRD §10.6)

3.6 Object storage — Cloudflare R2 (D-06)​

  • Hanya artefak ekspor laporan (Excel/PDF) + signed URL unduh
  • Path per env: dev/reports/…, prod/reports/…
  • Binding Wrangler: [[r2_buckets]] per environment
  • Bukan untuk foto wajah / enrollment

3.7 Notifikasi — FCM HTTP v1 (D-12)​

  • Tanda tangan service-account JWT RS256 (jose + crypto.subtle) → OAuth2 access token → FCM HTTP v1
  • Cache access token per isolate Worker
  • Outbox: baris notifications di db.batch() yang sama dengan domain write; pengiriman via cron Worker terpisah
  • Tabel device_tokens untuk FCM registration token per user/device
  • Event P0: status approval; P1: pengingat absensi

4. Lingkungan (environments)​

EnvironmentTujuanRuntime
LocalPengembanganBun lokal + Neon (atau DB lokal opsional)
DevelopmentIntegrasi / preview APICloudflare Worker dev-nexus-ops-api · Neon dev
ProductionDemo / operasionalCloudflare Worker nexus-ops-api · Neon prod
TriggerCloudflare envWorker name
Push ke maindevelopmentdev-nexus-ops-api
Tag v*productionnexus-ops-api

Contoh URL:

  • Dev API: https://dev-nexus-ops-api.dev-akmal69.workers.dev
  • Prod API: https://nexus-ops-api.dev-akmal69.workers.dev
  • Dev web: https://dev-nexus-ops-web.pages.dev
  • Prod web: https://nexus-ops-web.pages.dev

Secret / env​

VariabelKeterangan
DATABASE_URLNeon connection string (per env)
JWT_SECRETShared secret HS256 (min 16 karakter)
JWT_EXPIRES_INDefault 12h
APP_URLBase URL publik Worker (per env)
APP_ENV / APP_NAMEVars Wrangler
CORS_ORIGINSOrigin web eksplisit (comma-separated); bukan * di prod
R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKETR2 report exports
FCM_PROJECT_ID / FCM_CLIENT_EMAIL / FCM_PRIVATE_KEYService account FCM
GEOFENCE_DEFAULT_RADIUSDefault 100 (meter)
FACE_MATCH_THRESHOLDDefault 0.65
FACE_MODElive (default) | stub

5. Data & penyimpanan​

5.1 Database — Neon PostgreSQL​

Provider: Neon. Schema dikelola Drizzle (drizzle/ migrations).

Timezone (terkunci):

  • Semua timestamp disimpan timestamptz UTC
  • work_date date = (ts AT TIME ZONE 'Asia/Jakarta')::date
  • Default shift start 08:00 WIB; grace 15 menit → terlambat jika clock-in setelah 08:15 WIB
  • Org timezone: Asia/Jakarta (W-S01)

Sudah ada (migrasi 0000):

TabelCatatan
usersid UUID, email, password_hash, full_name, role (employee|supervisor|hrd), is_active, audit columns

ERD v1.1 (migrasi 0001 + seeds — skalabilitas: index, outbox claim, mean embedding, site/shift):

TabelKolom kunci
org_settingstimezone, default_shift_start, grace_minutes, geofence/GPS defaults (single-tenant row)
locationsid, name, lat, lng, radius_m default 100, is_active · partial index aktif
shiftsid, name, start_time/end_time, is_default — filter dashboard shiftId
employeesuser_id UK, nip UK, supervisor_id, default_location_id, shift_id, shift_start nullable
attendance_recordsUNIQUE (employee_id, work_date), lat/lng in/out, matched_location_id, face_result, gps_result, status · idx (work_date, status), (matched_location_id, work_date)
employee_face_templatesemployee_id PK, embeddings jsonb (sampel), mean_embedding real[] (hot-path cosine)
leave_requestsdates, reject_reason, reviewed_by/reviewed_at · CHECK end≥start · EXCLUDE gist no-overlap pending|approved · partial idx pending
overtime_requestswork_date, start_at/end_at, hours CHECK ≤ 4, status, reviewer · partial idx pending
device_tokensuser_id, UNIQUE (token), platform, last_seen_at, deactivated_at
notificationsoutbox + claimed_at / attempts / next_attempt_at · partial idx pending
audit_logsactor_id, action, entity, entity_id, payload · idx created_at, (entity, entity_id)
holidaysholiday_date UK, name — sumber status holiday

Login identifier: mengandung @ → cocokkan users.email; selain itu → cocokkan employees.nip.

5.2 Object storage (R2)​

  • Path: {env}/reports/{yyyy}/{mm}/{file}
  • Retensi artefak laporan: 90 hari lalu boleh dihapus (cron opsional)
  • Tidak ada retensi foto wajah — embedding only (D-09)

5.3 Backup & recovery​

ItemKeputusan
DB backupNeon automated PITR / branch sesuai plan
Retention DBMengikuti plan Neon; snapshot sebelum tag v1.0.0
Restore drillMinimal 1× sebelum demo akhir (S8)

6. Jaringan, keamanan & akses​

Prinsip​

  • HTTPS (Workers / Pages)
  • JWT Bearer · RBAC · secrets di Wrangler / GitHub Environments
  • Embedding wajah: private DB column; tidak di-log penuh
  • CORS: origin web dashboard eksplisit per env (CORS_ORIGINS)

Checklist keamanan​

  • JWT + hashed password (bcryptjs)
  • Role di claim / model user
  • Rate limiting login & absensi
  • Tidak upload gambar wajah ke server (embedding only)
  • CORS diperketat (hapus * di prod)
  • Logging tanpa embedding penuh / password
  • Least privilege service account FCM / R2

7. Deployment & CI/CD​

Repositories​

RepoLink
BackendCapstone-Project-Team-B-2026/backend
WebCapstone-Project-Team-B-2026/web
MobileCapstone-Project-Team-B-2026/mobile
DocsCapstone-Project-Team-B-2026/docs

Disiplin kontrak OpenAPI (terkunci)​

  1. Backend generate openapi.json dari Zod; info.version wajib naik setiap perubahan path/schema; file di-commit di repo backend.
  2. Publish mirror publik hanya dari Deploy backend setelah push ke main:
  3. Web/mobile: saat mulai kerja / setelah contract merge ke main → npm run api:sync (curl mirror publik, tanpa PAT) → Orval. Commit hasil generate di PR FE.
  4. Design tokens: docs/static/tokens.json → npm run tokens:sync di web / mobile.

Pipeline​

RepoCIDeploy / artefak
Backendformat · lint · tsc · unit coverage ≥95%main → Worker dev + publish OpenAPI → docs · v* → prod
Webformat · lint · tsc · unit ≥95% (src/lib) · E2E Playwright smokePages dev-nexus-ops-web / nexus-ops-web
Mobileformat · lint · tsc · unit ≥95% (src/lib)APK: nightly → nexus-ops-mobile-dev · tag v* → stg · Maestro manual
DocsDocusaurus build · project-sprint-backlog.ymlGitHub Pages · static/openapi.json diisi oleh backend deploy

8. Observabilitas​

AspekApproach
LoggingStructured (request id, role, endpoint, latency, reason_code) — tanpa embedding penuh
MetricsError rate, latency p95 absensi, FCM failure rate
AlertingGitHub Issues auto dari failed deploy workflow + channel Discord/Slack tim (webhook) bila Worker down
Auditaudit_logs + hasil face/GPS di attendance_records

9. Kapasitas & asumsi skala​

  • Pengguna: satu divisi Operation (puluhan–ratusan)
  • Spike clock-in di awal shift
  • Laporan: generate on-demand → R2 signed URL

Neon HTTP: tidak ada interactive multi-statement TX — gunakan db.batch() + unique/guarded updates + outbox (lihat §3.3).


10. Keputusan (ADR)​

IDKeputusanStatusCatatan
D-01Stack mobileDipilihRN + Expo 57 · Orval · APK nightly/tag · dev client untuk face
D-02Stack webDipilihVue 3 + Vite · Orval · Cloudflare Pages · Playwright
D-03Stack APIDipilihBun + Hono + Zod OpenAPI + DDD
D-04DatabaseDipilihNeon PostgreSQL + Drizzle
D-05Face recognition runtimeDipilihOn-device MobileFaceNet → Worker cosine (≥0.65); FACE_MODE=stub escape
D-06Object storageDipilihCloudflare R2 untuk ekspor laporan saja
D-07Hosting APIDipilihCloudflare Workers
D-08Auth tokenDipilihJWT Bearer · expiry 12h
D-09Retensi foto wajahDipilihTidak ada foto — embedding only di DB
D-10Package managersDipilihBun (backend, docs) · npm (web, mobile)
D-11OpenAPI → klienDipilihCommit di backend · publish ke docs/static dari Deploy main · FE api:sync manual
D-12Push notificationDipilihFCM HTTP v1 + in-app feed + outbox
D-13TimezoneDipilihAsia/Jakarta · work_date lokal · timestamptz UTC
D-14Atomic writesDipilihdb.batch() + DB invariants + notification outbox

11. Rencana implementasi infrastruktur​

Selaras SDLC §4.

SprintAktivitas
S1Inventaris NFR; env & secret policy
S2Stack API, Neon, Workers, OpenAPI, CI; bootstrap web & mobile + Orval
S3–S5Attendance + face + geofence API paralel UI; wiring FCM; R2 untuk reports
S5–S6Harden CI APK / Pages; face enroll; geofence master
S7Integrasi & mulai UAT
S8Production tag, restore drill, evaluasi, dokumentasi final

12. Risiko infrastruktur​

RisikoMitigasi
Neon HTTP tanpa interactive TXdb.batch() + unique (employee_id, work_date) + guarded updates
CORS *Kunci CORS_ORIGINS ke Pages URL sebelum prod formal
Native face build gagal di S3FACE_MODE=stub tanpa ubah kontrak
Embedding dipercaya dari klienJWT + rate limit + audit; anti-spoof out of scope
Secret bocorEnv example saja; secrets di GitHub / Wrangler
Downtime demoSmoke H-1 pada Worker + Pages

13. Riwayat revisi​

VersiTanggalPerubahan
0.1.02026-09-22Draft awal dari proposal capstone Kelompok B
0.2.02026-09-23Selaraskan dengan backend aktual
0.2.12026-09-24Rencana infra per sprint 1 minggu
0.3.02026-09-24Klien terkunci: Vue Pages + RN Expo
0.3.12026-09-24ADR D-06 R2 + D-12 FCM
1.0.02026-09-24Locked: D-05 face on-device, D-09 embedding-only, ERD v1, timezone, batch/outbox, CORS, OpenAPI publish dari Deploy, R2=reports
1.0.12026-09-25ERD v1.1: org_settings, shifts, holidays; attendance matched_location_id; face mean_embedding; outbox claim fields; indexes + leave EXCLUDE overlap; OT CHECK ≤ 4

Referensi​