Kebanyakan aplikasi chat menyimpan semuanya selamanya. blackchat kebalikannya: tidak ada yang tersisa.
| 🔐 End-to-end encrypted | Kunci dibuat dan disimpan di browser. Server hanya pernah melihat ciphertext: teks, gambar, kontak, bahkan header percakapan. |
| 🔥 Melebur setelah dibaca | Timer 3/5/7/10 detik baru mulai saat pesan benar-benar terlihat di layar penerima, lalu pesan hancur jadi partikel. Pesan yang menumpuk melebur satu per satu. |
| ⏳ Akun hangus 72 jam | Daftar cukup username + password. Tanpa email, tanpa nomor HP, tanpa pemulihan. Setelah 3 hari akun dan semua datanya dihapus. |
| 🕶️ Server buta relasi | Server tidak menyimpan siapa bicara dengan siapa, tidak menyimpan IP, tidak tahu jenis pesan. Nama room diturunkan dari rahasia Diffie-Hellman. |
| 🖼️ Gambar tanpa jejak | EXIF & GPS dibuang, gambar digambar ulang di canvas, dipotong menjadi chunk berukuran identik sebelum dienkripsi. |
| ✅ Safety number + QR | Verifikasi kunci lawan bicara; peringatan otomatis jika username dipakai orang lain dengan kunci berbeda. |
| ↩️ Batalkan & sepakati timer | Tarik pesan dari kedua sisi dan server. Perubahan timer hanya berlaku jika kedua pihak setuju. |
| 🌐 Dua bahasa | Inggris (default) dan Indonesia, ganti langsung lewat toggle EN · ID. Layout dua panel di desktop, satu kolom di ponsel. |
| 💸 Gratis dioperasikan | Vercel (web statis) + Cloudflare Workers, Durable Objects, D1 (relay). |
![]() Layar awal |
![]() Percakapan & umur akun |
![]() Timer mulai saat terlihat |
![]() Pesan melebur |
![]() Gambar terenkripsi |
![]() Batalkan pesan |
![]() Timer disepakati berdua |
![]() Safety number + QR |
![]() Desktop: pratinjau lebur langsung di halaman depan |
|
![]() Chat desktop: kedua pesan menghitung mundur |
![]() Halaman masuk · tema terang otomatis |
Monokrom murni: grid titik ala arsip, crop marks, kaca buram, logo gelembung berpiksel yang ikut "melebur". Watermark samar di setiap pesan, layar dihitamkan saat jendela kehilangan fokus. Gambar dibuat ulang dengan pnpm readme:assets.
sequenceDiagram
autonumber
participant A as Browser A
participant R as Relay (Cloudflare)
participant B as Browser B
Note over A,B: Kunci identitas Ed25519 + X25519 hanya ada di browser
A->>A: room = H(DH(A, B)) · enkripsi XChaCha20-Poly1305 · padding
A->>R: ciphertext + bukti keanggotaan (MAC)
Note over R: RoomDO: ciphertext tanpa identitas<br/>InboxDO: header tersegel, tanpa waktu
R-->>B: notifikasi lewat inbox B (WebSocket hibernasi)
B->>R: ambil ciphertext
B->>B: dekripsi · tampilkan · tunggu sampai terlihat di layar
B->>R: opened (pesan terlihat → burn_at = now + ttl)
R-->>A: opened + remainingMs (bukan timestamp)
Note over A,B: timer habis → efek lebur di kedua sisi
R->>R: alarm burn_at → hapus pesan & chunk
flowchart LR
subgraph Browser["Browser · Svelte 5 SPA"]
UI[UI] --> C[packages/crypto<br/>libsodium]
end
Browser -- HTTPS --> V[Vercel<br/>file statis + CSP ketat]
Browser -- "WebSocket (1 per akun)" --> W[Worker relay]
W --> D1[(D1<br/>akun · kunci publik · vault terenkripsi)]
W --> I[InboxDO ×akun<br/>daftar room tersegel]
W --> Rm[RoomDO ×pasangan<br/>pesan & chunk terenkripsi]
W --> L[LimiterDO ×16<br/>rate limit]
W -. cron 15 menit .-> D1
| Lapisan | Pilihan |
|---|---|
| Kunci akun | Argon2id (64 MiB, t=3) dari password → membuka vault berisi kunci rahasia; server hanya menyimpan SHA-256 dari authKey |
| Pesan | XChaCha20-Poly1305, teks di-pad ke kelipatan 256 B, chunk gambar identik (256 KB) |
| Metadata | ID room berbeda per inbox, header percakapan selalu 560 B, expiresAt publik dibulatkan ke jam |
| Browser | CSP script-src 'self' tanpa inline, tanpa innerHTML/{@html}/eval (ditegakkan lint), kunci sesi non-extractable, kunci otomatis 10 menit |
Model ancaman lengkap (37 risiko, status & mitigasinya) dan batasan yang diterima ada di SECURITY.md.
| Jumlah | Di mana | |
|---|---|---|
| Unit & integrasi | 899 test | protocol (validator & encoding), crypto (vektor silang libsodium ↔ noble), relay di workerd sungguhan dengan D1 & Durable Objects lokal, web (termasuk Chromium sungguhan untuk canvas & EXIF) |
| End-to-end | 29 skenario × 2 viewport | Playwright terhadap relay lokal + build produksi dengan CSP produksi: 320×568 dan 1280×800 |
| Kriteria penerimaan | 32/32 | Setiap butir PRD §15.2 dipetakan ke test-nya di docs/acceptance.md |
| Uji mutasi | manual | Pemeriksaan keamanan utama dihapus satu per satu untuk membuktikan test-nya gagal |
Setiap E2E juga memeriksa: tidak ada request keluar origin, tidak ada pelanggaran CSP, tidak ada error konsol, tidak ada scroll horizontal.
Prasyarat: Node 22 (.nvmrc) dan pnpm 9 lewat corepack.
corepack enable
pnpm install
cp apps/relay/.dev.vars.example apps/relay/.dev.vars # isi SALT_SECRET: openssl rand -hex 32
pnpm dev # relay :8787 (D1 & DO lokal) + web :5173Semua perintah
| Perintah | Isi |
|---|---|
pnpm lint |
ESLint + bukti bahwa aturan PRD §12 aktif |
pnpm format:check |
Prettier |
pnpm typecheck |
TypeScript strict seluruh paket + e2e |
pnpm test |
Unit protocol/crypto/web, test relay di workerd, test browser (Chromium), test tooling |
pnpm build |
Build web + bundle relay produksi |
pnpm check:inline |
HTML build tanpa script/style inline (CSP) |
pnpm --filter @blackchat/relay check:bundle |
Bundle relay produksi bebas kode test |
pnpm test:e2e |
Playwright: relay lokal tersendiri + build produksi dengan CSP produksi |
pnpm readme:assets |
Buat ulang banner, social preview, dan screenshot README |
pnpm configure-domains --web … --relay … |
Isi domain produksi ke CSP & ALLOWED_ORIGIN |
Pertama kali: pnpm exec playwright install chromium. WebKit opsional: E2E_WEBKIT=1 pnpm test:e2e.
Struktur repo
apps/web Vite + Svelte 5 SPA (UI, kriptografi client)
apps/relay Cloudflare Worker + Durable Objects + D1
packages/protocol Tipe, konstanta, validator, encoding (dipakai web & relay)
packages/crypto Semua operasi kriptografi client (libsodium)
e2e/ Test end-to-end Playwright (relay lokal sungguhan)
tooling/ Pemeriksa aturan lint, CSP, bundle relay; konfigurasi domain; gambar README
docs/ Rencana gelombang, keputusan (D-001…), progres, kriteria penerimaan, deploy
Dokumen acuan: BLACKCHAT_PRD.md (spesifikasi) dan docs/DECISIONS.md (koreksi & keputusan; menang jika bertentangan dengan PRD). Proyek dibangun dalam 13 gelombang (W0–W12): docs/BLACKCHAT-WAVES.md, docs/PROGRESS.md.
Web di Vercel (blackchat-id.vercel.app), relay di Cloudflare Workers. Panduan langkah demi langkah: docs/DEPLOY.md.
Sengaja minimal. Semua versi dipin persis (.npmrc save-exact=true); dependensi baru wajib ditambahkan ke tabel ini beserta alasannya (PRD §12 aturan 14).
| Runtime | Dipakai di | Alasan |
|---|---|---|
libsodium-wrappers-sumo |
packages/crypto | Semua primitif kripto client. Varian sumo dibutuhkan untuk Argon2id (PRD §2.3). Membawa tipe TypeScript sendiri |
@noble/hashes |
apps/relay | BLAKE2b di relay (D-007): WebCrypto tidak punya BLAKE2b, libsodium tidak bisa dimuat di Workers. Diaudit, pure JS |
svelte |
apps/web | Framework UI, output statis tanpa inline script (CSP ketat) |
qrcode-generator |
apps/web | QR safety number (PRD §4.4). MIT, tanpa dependensi; modul QR digambar sendiri ke canvas (tanpa HTML/SVG string) |
Dependensi pengembangan
| Paket | Alasan |
|---|---|
typescript |
Typecheck strict seluruh monorepo |
vite, @sveltejs/vite-plugin-svelte, svelte-check |
Build dan typecheck SPA |
@noble/hashes |
Test packages/crypto: pemeriksaan silang Argon2id & BLAKE2b dengan implementasi independen |
@types/node |
Tipe Node khusus untuk file test (kode src tidak boleh memakai API Node) |
vitest |
Test unit semua paket. Dipin di 4.x karena @cloudflare/vitest-pool-workers membutuhkan vitest 4 |
wrangler, @cloudflare/workers-types |
Dev lokal, build, dan deploy relay |
@cloudflare/vitest-pool-workers |
Test relay di dalam runtime Workers (workerd) dengan D1 dan Durable Objects lokal |
eslint, @eslint/js, typescript-eslint, eslint-plugin-svelte, svelte-eslint-parser, globals |
Lint dengan aturan wajib PRD §12 |
prettier, prettier-plugin-svelte |
Format kode konsisten |
@playwright/test |
Smoke & E2E di browser sungguhan, dengan CSP produksi (e2e/); juga gambar README (tooling/readme-assets/) |
@vitest/browser-playwright, playwright |
Test unit yang butuh browser sungguhan (canvas, encoder gambar): *.browser.test.ts di apps/web |
- Temukan celah keamanan? Jangan buka issue publik. Laporkan lewat GitHub → Security → Report a vulnerability (SECURITY.md).
- Ingin berkontribusi? Baca CONTRIBUTING.md: satu perubahan = satu branch = satu PR, CI wajib hijau.
What is blackchat?
blackchat is an end-to-end encrypted, ephemeral one-to-one chat for the browser.
- Sign up with just a username and password. No email, no phone number, no recovery. Accounts self-destruct after 72 hours.
- English by default, with an Indonesian toggle. Two-pane layout on desktop, single column on mobile.
- Messages burn after reading. A 3/5/7/10-second timer starts only once the message is actually visible on the recipient's screen; then it dissolves into particles on both sides and is deleted from the server.
- The server is blind. It only ever stores ciphertext (libsodium: X25519, Ed25519, XChaCha20-Poly1305, Argon2id). It does not store who talks to whom, IP addresses, or message types. Room names are derived from a Diffie-Hellman secret, conversation headers are sealed and fixed-size, and image chunks are identical in size.
- Images are re-drawn through a canvas to strip EXIF/GPS before encryption.
- Safety numbers + QR for key verification, and a warning when a username is re-registered with a different key.
- Runs on free tiers: static SPA on Vercel, relay on Cloudflare Workers + Durable Objects + D1.
- Tested: 899 unit/integration tests (including the relay inside workerd), 29 Playwright end-to-end scenarios against the production build with the production CSP, and all 32 acceptance criteria mapped to tests.
The threat model is in SECURITY.md; deployment is in docs/DEPLOY.md.
MIT.
- Font Instrument Sans dan Martian Mono (SIL Open Font License 1.1), subset Latin dari Fontsource. Lisensi di
apps/web/public/fonts/. - Daftar password umum di
packages/crypto/src/common-passwords.tsditurunkan dari SecLists (MIT, © Daniel Miessler). Lihat D-010.
Tidak ada yang tersimpan. Tidak ada yang tersisa. 🖤












