1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>sueta โ serverless p2p e2e chat stack</title>
<style>
:root {
--bg: #0d1117; --panel: #161b22; --panel-2: #1c2129; --border: #262c36;
--text: #e6edf3; --muted: #8b949e; --accent: #3fb68b; --accent-2: #2ea043;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--text);
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 40px 20px 80px;
}
main { max-width: 780px; margin: 0 auto; }
h1 { font-size: 34px; letter-spacing: 1px; }
h1 .lock { font-size: 24px; }
.tag { color: var(--muted); margin: 6px 0 30px; font-size: 17px; }
h2 { margin: 44px 0 12px; font-size: 22px; color: var(--accent); }
h3 { margin: 24px 0 8px; font-size: 17px; }
p, li { color: #c9d3dd; }
ul { padding-left: 22px; margin: 10px 0; }
li { margin: 5px 0; }
a { color: var(--accent); }
pre {
background: var(--panel); border: 1px solid var(--border); border-radius: 10px;
padding: 14px 16px; overflow-x: auto; margin: 12px 0;
font: 13.5px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
}
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 14px;
background: var(--panel-2); border-radius: 5px; padding: 1px 6px; }
pre code { background: none; padding: 0; }
.card {
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
padding: 18px 20px; margin: 14px 0;
}
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.muted { color: var(--muted); font-size: 14px; }
.btn {
display: inline-block; background: var(--accent-2); color: #fff; text-decoration: none;
font-weight: 600; border-radius: 10px; padding: 12px 22px; margin: 8px 12px 8px 0;
}
.btn.ghost { background: var(--panel-2); border: 1px solid var(--border); }
footer { margin-top: 60px; color: var(--muted); font-size: 13px; }
</style>
</head>
<body>
<main>
<h1><span class="lock">๐</span> sueta</h1>
<p class="tag">a complete, working stack for <strong>serverless p2p end-to-end-encrypted web apps</strong> โ this page is also the git repo.</p>
<a class="btn" href="https://chat.ardegazu.ro">try the live chat</a>
<a class="btn ghost" href="browse/index.html">browse the code</a>
<a class="btn ghost" href="#clone">clone the repo</a>
<h2 id="clone">Clone</h2>
<p>This site serves the repository over git's dumb-HTTP protocol as plain static files (it's hosted on IPFS โ there is no git server here, or any server at all):</p>
<pre><code>git clone https://git-chat.ardegazu.ro/sueta.git
cd sueta</code></pre>
<h2>What you get</h2>
<div class="grid">
<div class="card"><h3>client/src/lib/ โ the p2p core</h3>
<p class="muted">Reusable, app-agnostic TypeScript: WebRTC full-mesh with perfect negotiation, E2E crypto (HKDF + AES-GCM, structurally nonce-safe), blind signaling client, TURN credentials, persistent Ed25519 identities with emoji fingerprints.</p></div>
<div class="card"><h3>server/ โ one Go binary</h3>
<p class="muted">Blind WebSocket relay (routes only ciphertext), coturn REST credential minter, built-in Let's Encrypt. No database, no state, no logs of content.</p></div>
<div class="card"><h3>client/src/app/ โ the chat</h3>
<p class="muted">Installable PWA: rooms as capability links, threads, reactions, EXIF-stripped image sharing, p2p audio/video room calls (DTLS-SRTP), peer-to-peer history sync + on-device persistence, local notifications, numbered releases with an in-app update banner.</p></div>
<div class="card"><h3>docs/ โ the contracts</h3>
<p class="muted"><code>PROTOCOL.md</code> โ crypto & wire spec. <code>SIGNAL-SERVER.md</code> โ ops runbook and how to attach new apps/domains to one relay.</p></div>
</div>
<h2>How it works (the short version)</h2>
<ul>
<li><strong>The room secret never leaves the browser.</strong> It lives in the URL fragment (<code>#โฆ</code>). HKDF derives a one-way room id (rendezvous only) and per-sender AES-256-GCM keys.</li>
<li><strong>The relay is blind.</strong> Peers meet through a ~500-line WebSocket relay that forwards opaque encrypted blobs. It cannot read offers, names, or messages โ verified by tests that grep every frame for plaintext.</li>
<li><strong>Everything else is browser โ browser.</strong> WebRTC datachannels (DTLS + app-layer AES-GCM), full mesh up to ~12 peers, STUN/TURN for NAT traversal with ephemeral HMAC credentials.</li>
<li><strong>History lives in the members.</strong> Devices persist their rooms locally (IndexedDB) and replay history to newcomers over the encrypted channels. No server stores a byte.</li>
<li><strong>Identity without accounts.</strong> One Ed25519 seed per device (password-manager-sized), session binding signatures, emoji fingerprints, trust-on-first-use with key-change warnings.</li>
<li><strong>Hosting without an origin server.</strong> The app is a static build pinned to IPFS, served through an HTTPS tunnel that is deliberately never connected โ its "offline page" <em>is</em> the site. This page you're reading works the same way.</li>
</ul>
<h2>Run it yourself</h2>
<pre><code># dev: relay + app
cd server && go run . --dev # :8080
cd client && npm install && npm run dev # :5173, proxies /ws to :8080
# tests (real Chromium โ WebRTC needs real UDP)
cd server && go test ./...
cd client && node e2e/mesh.e2e.mjs</code></pre>
<p>To deploy your own server side, one command on any Ubuntu/Debian box: <code>sudo deploy/install-server.sh signal.yourdomain.com</code> โ installs coturn + the relay from source, secrets, systemd, firewall. Host the client build anywhere static โ IPFS, a CDN, a folder behind nginx. Full steps in <code>docs/SIGNAL-SERVER.md</code>.</p>
<h2>Build your own p2p app on this</h2>
<p>The chat is just one consumer of the core. A new app is ~40 lines of bootstrap:</p>
<ul>
<li>pick an <strong>app salt</strong> (e.g. <code>"myapp.example.com/v1"</code>) โ it namespaces your rooms so apps can share one relay without collisions,</li>
<li>copy <code>client/src/lib/</code> (zero app-specific code in it),</li>
<li>point it at your relay โ or reuse an existing one; the relay never learns what app it's carrying,</li>
<li>define your own payloads and broadcast them โ they're sealed per-sender automatically.</li>
</ul>
<p>The recipe with code is in <code>client/src/lib/README.md</code>. Shared whiteboards, turn-based games, collaborative editors, file drops โ anything that fits "small group, real-time, private" fits this stack.</p>
<h2>Guarantees & limits, honestly</h2>
<ul>
<li>The relay operator (or anyone on the wire) sees: connection metadata, opaque room ids, ciphertext sizes and timing. Not content, not names, not who-is-who inside a room.</li>
<li>Anyone with the room link is a full member โ capability model. Guard links like keys.</li>
<li>Identity verification is human: compare the four emoji out-of-band, like reading safety numbers aloud. No servers means no authority that can vouch for anyone.</li>
<li>Full mesh scales to ~a dozen peers; beyond that you'd want an SFU, which is no longer serverless.</li>
<li>History survives as long as any member's device kept it; a room everyone truly abandons is forgotten. That's the design, not a bug.</li>
</ul>
<footer>
MIT licensed. Built with WebCrypto, WebRTC, Go, coturn, and IPFS โ no other services involved.<br>
Live app: <a href="https://chat.ardegazu.ro">chat.ardegazu.ro</a> ยท this repo: <code>git clone https://git-chat.ardegazu.ro/sueta.git</code>
</footer>
</main>
</body>
</html>
|