snapme
PublicARCHITECTURE.md
Versi 0.2, 18 Oktober 2026. Dibaca bersama PRD.md (apa yang dibangun) dan DESIGN-SYSTEM.md (tampilannya). Dokumen ini menjawab: dengan apa, disusun bagaimana, dan dikerjakan dengan alur apa.
Perubahan besar dari versi 0.1: backend dibongkar (18 Oktober 2026, keputusan pengguna; catatannya di docs/DECISIONS.md). snapme kini aplikasi klien murni: seluruh alur booth berjalan di browser tanpa server, seluruh rute /api adalah stub yang menjawab 503 "belum tersedia", dan halaman /s/[id] serta /r/[id] selalu menjawab 404.
1. Prinsip
- Pakai yang sudah jadi dulu. Kerangka, komponen, dan pustaka yang terawat dipasang lebih dulu, lalu disesuaikan dengan design system. Menulis sendiri adalah pilihan terakhir (§2).
- Semua di perangkat. Kamera, deteksi gesture, penyuntingan, perekaman Strip Hidup, dan simpanan sesi berjalan di browser. Tidak ada unggahan ke mana pun: tidak ada server yang menerimanya.
- Token saja. Warna, radius, bayangan, dan fon hanya didefinisikan di
globals.css. Komponen memakai nama token. - Modul berat dimuat belakangan. Welcome tidak memuat kode kamera, gesture, atau kanvas.
- Logika murni dipisah dari perangkat. Ambang gesture dan geometri strip adalah fungsi murni yang bisa diuji tanpa kamera.
2. Aturan "pakai yang sudah jadi dulu"
Aturan ini mengikat manusia maupun alat otomatis.
Tangga keputusan
Untuk setiap kebutuhan, turun satu anak tangga hanya bila anak tangga di atasnya tidak memenuhi:
- Fitur platform: API browser atau fitur Next.js (contoh: Web Share API,
MediaRecorder,next/font). - Komponen atau pustaka di tabel §3.
- Komponen dari registry shadcn lain atau pustaka npm terawat: cari dulu di registry shadcn dan npm, pakai yang terawat dan terus dirawat.
- Tulis sendiri, hanya untuk komponen khas produk di DESIGN-SYSTEM §9 atau bila tiga langkah di atas gagal. Catat alasannya di
docs/DECISIONS.md.
Cara menyesuaikan komponen jadi
- Pasang lewat CLI (
npx shadcn@latest add ...) sehingga kodenya masuk kesrc/components/ui. - Ubah tampilannya hanya lewat token (
globals.css) dan varian (class-variance-authority) agar cocok dengan DESIGN-SYSTEM §9. Perilaku dan aksesibilitas bawaan (Radix) tidak diubah. - Tampilkan hasilnya di halaman
/dev/uidan bandingkan berdampingan dengan berkas acuan didocs/ui-reference/(DESIGN-SYSTEM §10).
Yang tidak boleh ditulis sendiri
Dialog, lembar bawah, tab, sakelar, slider, tooltip, toast, carousel, pengelolaan fokus, pembuat QR, pustaka transformasi kanvas (geser, putar, ubah ukuran), model deteksi tangan, pembuat ID, validasi skema, ikon.
Syarat pustaka baru
Lisensi permisif (MIT, Apache-2.0, BSD), ada rilis atau commit dalam 12 bulan terakhir, mendukung versi React dan Tailwind yang terpasang, dan tidak mengirim data ke pihak ketiga. Periksa sebelum memasang, catat di docs/DECISIONS.md.
3. Stack
Versi tidak dipaku di dokumen ini. Pasang versi stabil terbaru, lalu biarkan lockfile yang mengunci.
| Lapisan | Pilihan | Dipakai untuk | Catatan |
|---|---|---|---|
| Kerangka | Next.js (App Router) + React + TypeScript | Halaman dan metadata | |
| Gaya | Tailwind CSS v4 | Token dan utilitas | Token di DESIGN-SYSTEM §13 |
| Komponen dasar | shadcn/ui (Radix) | Semua komponen umum di DESIGN-SYSTEM §9 | Kode disalin ke repo, jadi bebas disesuaikan |
| Titik awal gaya | Registry neobrutalism berbasis shadcn | Varian bergaris tebal dan berbayang keras | Lihat "Evaluasi registry" di bawah |
| Ikon | @phosphor-icons/react | Semua ikon | Mengganti lucide-react bawaan shadcn |
| Gerak | motion (motion/react) | Transisi di DESIGN-SYSTEM §7 | |
| State | zustand | State sesi booth | |
| Kamera | react-webcam | Pembungkus getUserMedia | |
| Gesture | @mediapipe/tasks-vision (Gesture Recognizer) | Gesture Snap | Kategori bawaannya memuat Victory, Open_Palm, Thumb_Up. Berkas wasm dan model di-host sendiri |
| Kanvas | konva + react-konva | Penyusunan strip, stiker, efek, ekspor PNG | Efek foto memakai filter bawaan Konva agar seragam antar browser |
| QR | qrcode | Kotak QR di kaki strip | Dimuat dengan import() dinamis; QR sungguhan hanya digambar bila link sudah ada, dan link itu kini tidak pernah jadi |
| Video | canvas.captureStream() + MediaRecorder | Unduhan Strip Hidup | API browser, tanpa pustaka |
| Validasi | zod | Batas jenis dan ukuran berkas yang divalidasi klien (lib/schemas.ts) | |
| ID | nanoid | ID sesi lokal (store booth dan IndexedDB) | |
| Analitik | Endpoint peristiwa first-party (POST /api/events, dikirim navigator.sendBeacon) | Ukuran PRD §4 | Tanpa cookie, tanpa pustaka pihak ketiga, daftar peristiwa tertutup di src/lib/analyticsEvents.ts. Selama rutenya 503, antrean membuang batchnya diam-diam (lib/analytics.ts) |
| Uji | Vitest, Playwright | Unit dan end-to-end | Playwright punya kamera palsu di Chromium; proyek firefox dan webkit memuat crossbrowser.spec.ts |
Tidak ada baris basis data, penyimpanan berkas, anggaran laju, hosting, dan CLI sumber daya: semuanya dibongkar bersama backendnya pada 18 Oktober 2026 (daftar yang dihapus dan alasannya di docs/DECISIONS.md). Tidak ada variabel lingkungan yang dibutuhkan build maupun runtime.
Evaluasi registry neobrutalism (dikerjakan di M0)
Yang sudah saya periksa pada 8 Oktober 2026:
- RetroUI (
retroui.dev, reponeobrutalism/neobrutalism): berbasis shadcn, lisensi MIT, komponen dipasang lewat registry. Tanggal commit terakhir belum saya lihat. - Neobrutalism components (
neobrutalism.dev, repoekmas/neobrutalism-components): README-nya menyatakan tidak lagi dirawat. Jangan dijadikan sumber utama. - BoldKit (
boldkit.dev): berbasis shadcn, sudut bawaannya nol, berlawanan dengan radius di design system.
Langkah M0: periksa tanggal commit terakhir RetroUI dan kecocokannya dengan versi React dan Tailwind yang terpasang. Bila lolos syarat §2, ambil varian button, card, dan badge dari sana sebagai titik awal. Bila tidak, pakai shadcn/ui polos: gaya snapme (garis 2 px, bayangan keras, pil) cukup diterapkan lewat token dan varian. Keputusan dicatat di docs/DECISIONS.md.
Perintah awal (panduan untuk M0, sudah dijalankan)
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
npx shadcn@latest init
npx shadcn@latest add button badge card dialog drawer tabs toggle-group switch slider sonner tooltip skeleton carousel
npm i zustand zod nanoid motion @phosphor-icons/react react-webcam konva react-konva @mediapipe/tasks-vision qrcode
npm i -D vitest @testing-library/react @playwright/test @axe-core/playwright @types/qrcode
Bila create-next-app menolak folder yang sudah berisi dokumen, buat di folder sementara lalu pindahkan isinya ke root.
4. Struktur folder
snapme/
├─ next.config.ts eslint.config.mjs tsconfig.json vitest.config.mts
├─ playwright.config.ts uji e2e terhadap dev server
├─ docs/
│ ├─ PRD.md DESIGN-SYSTEM.md ARCHITECTURE.md
│ ├─ MILESTONES.md rencana rilis: pekerjaan dan syarat selesai tiap tahap
│ ├─ DECISIONS.md catatan keputusan dan pustaka yang ditambah
│ └─ ui-reference/ acuan pasti UI: welcome.html, capture.html, customize.html
├─ public/
│ ├─ brand/snapme-mark.svg lambang merek (DESIGN-SYSTEM §8)
│ ├─ mediapipe/wasm/ disalin dari paket saat pemasangan, tidak di git
│ ├─ models/gesture_recognizer.task
│ ├─ stickers/
├─ src/
│ ├─ app/
│ │ ├─ page.tsx Welcome (komponen server, statis)
│ │ ├─ booth/layout.tsx Booth Shell + stepper
│ │ ├─ booth/{layout,capture,customize,result}/page.tsx
│ │ ├─ s/[id]/page.tsx selalu 404: tidak ada strip yang tersimpan (noindex)
│ │ ├─ r/[id]/page.tsx selalu 404: tidak ada estafet yang tersimpan (noindex)
│ │ ├─ privasi/page.tsx
│ │ ├─ dev/ui/page.tsx galeri komponen, hanya saat pengembangan
│ │ ├─ icon.svg favicon SVG, versi kecil lambang (DESIGN-SYSTEM §8)
│ │ ├─ apple-icon.png diturunkan `npm run brand:icons`
│ │ ├─ favicon.ico diturunkan `npm run brand:icons` (16, 32, 48 px)
│ │ └─ api/... stub kontrak lama: seluruhnya menjawab 503 "belum tersedia" (§7)
│ ├─ components/
│ │ ├─ ui/ komponen shadcn yang sudah disesuaikan
│ │ └─ site/ wordmark dan header, dipakai halaman situs dan kerangka booth
│ ├─ features/ tiap fitur memiliki logika dan komponennya sendiri
│ │ ├─ booth/ BoothPanel dan stepper: kerangka tiap langkah booth
│ │ ├─ camera/ CameraPicker, useCamera, izin, potong tengah
│ │ ├─ gesture/ GestureHUD, GestureToggle, gestureSource (MediaPipe), holdDetector (murni)
│ │ ├─ capture/ mesin keadaan, hitung mundur, CameraStage, ShotTray, perekam bingkai
│ │ ├─ editor/ CustomizeEditor, panel hias, StripCanvas, efek, stiker
│ │ ├─ result/ layar hasil: ResultView, ekspor strip, lembar cetak 4R
│ │ ├─ gallery/ pemilih foto galeri sebagai pengganti kamera (FR-13)
│ │ ├─ live/ perekam video Strip Hidup dan dukungan medianya
│ │ ├─ relay/ kartu undang estafet dan panggilan jaringannya (selalu 503)
│ │ ├─ session/ simpanan sesi IndexedDB
│ │ ├─ share/ kartu bagikan, dialog persetujuan, pembuat QR lokal
│ │ └─ metrics/ ping pembuka sesi
│ ├─ lib/
│ │ ├─ layouts.ts geometri ekspor (DESIGN-SYSTEM §5)
│ │ ├─ schemas.ts batas jenis dan ukuran berkas, dipakai klien
│ │ ├─ relaySlots.ts pembagian slot estafet (murni)
│ │ ├─ http.ts panggilan jaringan klien bersama (pesan galat, PUT ke URL bertanda tangan)
│ │ ├─ analytics.ts antrean beacon analitik
│ │ └─ imageElement.ts utils.ts analyticsEvents.ts
│ └─ stores/booth.ts state sesi (zustand)
└─ tests/ cermin `src/`, plus
├─ security/ pagar bundel klien
├─ e2e/ Playwright: alur kamera palsu dan pagar privasi
└─ components/ features/ lib/ stores/
Aturan penempatan, supaya tidak ada layar yang punya dua rumah: src/app/ hanya memuat rute
(page.tsx, layout.tsx, route.ts, globals.css); src/components/ui/ memuat primitif shadcn
apa adanya dari CLI, dan src/components/site/ hanya bagian kerangka yang dipakai bersama halaman
situs dan booth (wordmark dan header); komponen produk tinggal di dalam fiturnya masing-masing di
src/features/<area>/, bersama logikanya. Tidak ada modul khusus server di src/: seluruh src/
aman untuk bundel klien, dan tests/security menjaga pagar itu beserta rahasianya.
5. Rute dan keadaan sesi
| Rute | Jenis | Isi |
|---|---|---|
/ | Server, statis | Welcome |
/booth/layout | Klien | Pilih layout |
/booth/capture | Klien | Kamera, Gesture Snap, baki foto |
/booth/customize | Klien | Kanvas hias, Strip Hidup, QR |
/booth/result | Klien | Unduh, cetak, bagikan |
/s/[id] | Server | Selalu 404 (notFound()): tidak ada strip yang tersimpan. noindex dan no-referrer tetap dipasang next.config.ts |
/r/[id] | Server | Selalu 404, aturan yang sama dengan /s/[id] |
/privasi | Server, statis | Kebijakan privasi |
Masuk ke langkah booth tanpa prasyarat (misalnya /booth/customize tanpa foto) dialihkan ke langkah yang belum selesai.
Mesin keadaan pemotretan
idle ─► meminta-izin ─► siap ─► menahan ─► hitung-mundur ─► memotret ─► jeda ─► siap
│ ▲ │ (gesture lepas) │
▼ └────────┘ ▼
ditolak semua slot terisi ─► lengkap
siap ─► hitung-mundurjuga dipicu tombol kamera atau Spasi, tanpa lewatmenahan.- Selama
hitung-mundur, perekam bingkai mengumpulkan 10 bingkai pada 1,5 detik terakhir (Strip Hidup). - Mesin keadaan ini fungsi murni (
reduce(state, event)), diuji tanpa kamera.
State sesi (stores/booth.ts)
layoutId, shots[] (tiap entri: gambar diam + bingkai klip + pemilik slot), settings (gesture nyala, kamera terpilih), editor (frame, efek, stiker[], teks[]), live, share (id, token hapus, kedaluwarsa), relay (id, peran, slot terkunci). Foto disimpan di memori. Penyimpanan lintas muat ulang (FR-14) memakai IndexedDB. Bagian share dan relay tetap ada sebagai state UI: keduanya tidak pernah terisi karena pembuatan link selalu gagal 503.
6. Modul inti
Kamera. react-webcam dengan facingMode: "user", pratinjau dicerminkan lewat CSS, foto diambil dari bingkai video ke kanvas lalu dicerminkan agar sama dengan pratinjau. Resolusi diminta 1280 x 720 atau lebih, lalu di-crop ke rasio slot.
Gesture. Dua bagian terpisah:
gestureSource: memuatGestureRecognizerdalam mode video dari berkas dipublic/, membaca bingkai yang diperkecil dengan laju dibatasi (sekitar 10 sampai 15 kali per detik), dan memancarkan{kategori, skor, waktu}. Dimuat denganimport()dinamis setelah izin kamera diberikan.holdDetector: fungsi murni yang mengubah aliran itu menjadi peristiwatahan-mulai,tahan-progres,tahan-selesai,tahan-batalmemakai ambang dari PRD §7.2. Ambang ada di satu berkas konstanta.
Editor. react-konva menyusun strip dari geometri di lib/layouts.ts. Efek foto adalah preset filter Konva. Stiker memakai Transformer bawaan. Ekspor memanggil stage.toDataURL dengan pixelRatio yang menghasilkan ukuran di DESIGN-SYSTEM §5. Padanan daftar untuk keyboard dan pembaca layar membaca state yang sama.
Strip Hidup. Bingkai klip disimpan sebagai ImageBitmap. LiveStripPlayer menggambar ulang kanvas strip dengan bingkai berjalan. Unduhan merekam kanvas itu lewat captureStream dan MediaRecorder, dengan jenis berkas dipilih lewat MediaRecorder.isTypeSupported. Perekaman dan unduhannya murni lokal.
Bagikan dan QR. UI-nya utuh: dialog persetujuan menyebut apa yang akan diunggah, siapa yang bisa melihat, dan kapan terhapus (PRD SE-02, SH-04); kartu bagikan punya tombol buat link, alamat, dan salin. Alurnya: requestStrip() memanggil POST /api/strips, yang sekarang selalu menjawab 503, sehingga kartu selalu berakhir di pesan "belum tersedia" dan tidak ada permintaan yang membawa gambar. Sakelar QR menyalakan kotak penanda di kanvas; QR sungguhan (qrcode, dimuat dinamis) hanya digambar setelah link ada, dan karena link tidak pernah jadi, QR itu tidak pernah muncul.
Estafet. Kartu undangan dan dialog persetujuannya tetap ada di langkah Ambil Foto; POST /api/relays menjawab 503, jadi pembuatan link estafet selalu berakhir di pesan yang sama, dan halaman /r/[id] untuk teman selalu 404. Pembagian slot (lib/relaySlots.ts) dan token lokalnya tetap diuji sebagai fungsi murni.
7. Data dan API
Rute (stub kontrak lama)
Tidak ada basis data dan tidak ada penyimpanan berkas: semuanya dibongkar bersama backend pada 18 Oktober 2026 (docs/DECISIONS.md). Rute di bawah dipertahankan sebagai stub yang menjawab 503 dengan pesan tetap, supaya klien yang masih memanggilnya menampilkan "belum tersedia", bukan galat jaringan atau 404.
| Rute | Jawaban |
|---|---|
POST /api/strips | 503 "Pembuatan link belum tersedia." |
POST /api/strips/[id]/complete | 503, pesan yang sama |
DELETE /api/strips/[id] | 503 "Penghapusan link belum tersedia." |
POST /api/relays | 503 "Pembuatan link belum tersedia." |
POST /api/relays/[id]/claim | 503, pesan yang sama |
POST /api/relays/[id]/complete | 503, pesan yang sama |
POST /api/relays/[id]/strip | 503, pesan yang sama |
DELETE /api/relays/[id] | 503 "Penghapusan link belum tersedia." |
POST /api/events | 503 "Pencatatan peristiwa belum tersedia." |
Karena tidak ada yang tersimpan, tidak ada pula kedaluwarsa, token hapus di sisi server, kuota, atau pembersih terjadwal. Halaman /s/[id] dan /r/[id] menjawab 404 lewat notFound() untuk id apa pun, dan antrean analitik klien membuang batchnya diam-diam saat POST /api/events menjawab 503 (lib/analytics.ts, tanpa pengulangan).
8. Privasi dan keamanan
- Tidak ada unggahan. Tidak ada foto, klip, atau peristiwa yang meninggalkan perangkat: tidak ada layanan yang menerimanya. Uji Playwright menegaskan tidak ada permintaan yang membawa gambar selama
/booth/capture(PRD GS-01). - Persetujuan: dialog sebelum unggahan tetap dipertahankan di UI (PRD SE-02, SH-04), walaupun unggahannya tidak pernah terjadi; alurnya selalu berakhir di pesan "belum tersedia".
- Batas berkas: jenis dan ukuran (diam 2 MB, klip 5 MB, slot estafet 1 MB) divalidasi di klien lewat
lib/schemas.tssebelum klien menawarkan unggahan. - Link: halaman
/s/*dan/r/*diberinoindexdanReferrer-Policy: no-referrer, dan keduanya selalu 404. - Header: CSP penuh tanpa satu pun host luar (fon, wasm, dan model MediaPipe di-host sendiri;
wasm-unsafe-evaluntuk MediaPipe;blob:dandata:untuk kanvas, perekam, dan konversi Blob),Permissions-Policy: camera=(self),X-Content-Type-Options: nosniff, danframe-ancestors 'none'.script-srcmemuat'unsafe-inline'(skrip bootstrap App Router tanpa nonce; nonce memaksa render dinamis) dan'unsafe-eval'hanya di pengembangan. Rincian tiap direktif didocs/DECISIONS.mdM9 (sejarah). - Tanpa pihak ketiga: fon di-host sendiri, wasm dan model MediaPipe di-host sendiri, tanpa pelacak. Hitungan peristiwa PRD §4 lewat rute sendiri (
POST /api/events), tanpa cookie dan tanpa kolom identitas; selama rutenya 503, antrean klien membuang batchnya tanpa mengulang. - Metadata: ekspor lewat kanvas tidak membawa EXIF.
- Rahasia: tidak ada. Tidak ada variabel lingkungan yang dibutuhkan build maupun runtime, dan tidak ada berkas
.env*atau.dev.varsdi repo.
9. Kinerja
/adalah komponen server statis. Kode booth dipisah per rute.- MediaPipe dan Konva dimuat dengan
import()dinamis di langkah yang memakainya. - Deteksi gesture memakai bingkai yang diperkecil dan laju terbatas. Bila pratinjau tersendat, turunkan laju dulu, baru pertimbangkan Web Worker.
- Bingkai klip 480 px. Dilepas (
close()) saat sesi diulang. - Stiker dan frame dalam ukuran yang sudah dioptimalkan. Fon lewat
next/font.
10. Pengujian
| Lapisan | Alat | Yang diuji |
|---|---|---|
| Unit | Vitest | holdDetector, mesin keadaan pemotretan, geometri layouts.ts (jumlah piksel pas 1800), antrean analitik, pembagian slot estafet, perekam bingkai |
| Komponen | Vitest + Testing Library | Keadaan tombol, stepper, baki foto, HUD gesture, daftar hiasan |
| Keamanan | Vitest (tests/security) | Tidak ada berkas klien yang mengimpor modul khusus server, dan tidak ada rahasia yang muncul di bundel statis hasil build |
| End-to-end | Playwright (Chromium dengan --use-fake-ui-for-media-stream --use-fake-device-for-media-stream) | Alur dasar sampai unduh, izin ditolak, jalur "belum tersedia" (kartu bagikan, QR, estafet), dan halaman /s serta /r yang selalu 404 dan tidak diindeks |
| Privasi | Playwright | Tidak ada permintaan jaringan yang membawa gambar selama /booth/capture (PRD GS-01) |
| Aksesibilitas | @axe-core/playwright + uji keyboard manual | Semua halaman |
| Lintas peramban | Proyek firefox dan webkit (hanya crossbrowser.spec.ts) | Perilaku inti di mesin Gecko dan WebKit |
| Manual | HP Android kelas menengah, iPhone, laptop | Gesture pada berbagai cahaya, cetak 4R; pindai QR menunggu link yang bisa dibuat lagi |
Perintah pemeriksaan sebelum sebuah perubahan dianggap selesai: npm run lint, npm run typecheck, npm test, npm run build, dan npx playwright test untuk alur yang disentuh.
Yang sudah selesai dan tidak perlu diperiksa lagi: perintah di §3 sudah dijalankan sejak M0, pemilihan registry neobrutalism diputuskan di M0 (docs/DECISIONS.md), dan dukungan MediaRecorder antar-browser sudah dipetakan di M7 (pickLiveKind) dengan proyek WebKit dan Firefox ikut hijau di M9. M10 memindahkan backend ke Cloudflare, dan pembongkaran baliknya pada 18 Oktober 2026 dicatat di docs/DECISIONS.md.
11. Milestone
Urutan kerja, pekerjaan, dan syarat selesai tiap milestone ada di MILESTONES.md (M0 Fondasi sampai M10 Migrasi Cloudflare, lalu catatan pembongkaran backend 18 Oktober 2026). Berkas itu satu-satunya sumber untuk milestone dan memuat pula syarat selesai yang berlaku umum tiap tahap.
12. Yang belum diverifikasi
- Kinerja Gesture Recognizer di HP kelas menengah: ambang dan laju di PRD §7.2 masih nilai awal dan butuh uji pakai di perangkat sungguhan. Langkahnya ada di tabel manual pengguna
docs/DECISIONS.md(M9). - Keterbacaan QR 168 px dari cetakan asli: butuh cetak 4R dan kamera HP, dan QR-nya sendiri butuh link yang bisa dibuat, jadi butir ini tertunda sampai fitur bagikan hidup lagi. Langkah dan tabelnya di
docs/DECISIONS.md(M9). - Sentuhan, kamera sungguhan, dan unduhan di Chrome Android, Safari iOS, Edge, serta Firefox sungguhan. Mesin WebKit dan Gecko sudah tercakup otomatis (
npx playwright test --project=firefox --project=webkit, hijau di M9); sisanya tabel manual pengguna. - Pembaca layar (NVDA, VoiceOver, TalkBack): audit axe dan uji keyboard otomatis sudah hijau, verifikasi dengan pembaca layar butuh pengguna.
- LCP Welcome: pengukuran produksi terakhir (median 2,416 detik, 16 Oktober 2026) diambil terhadap deployment yang sudah dibongkar, jadi angka itu historis. Ukur ulang terhadap
next startlokal bila angka yang berlaku dibutuhkan; angka lokal lama (3,17 detik di M9) melebih-lebihkan selisihnya karena bukan CDN. - Belum ada deployment publik: snapme dijalankan lewat
npm run devataunext startmilik sendiri, tanpa layanan eksternal.