ItsGoin v0.3.2 — Decentralized social media network

No central server, user-owned data, reverse-chronological feed.
Rust core + Tauri desktop + Android app + plain HTML/CSS/JS frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-15 20:22:08 -04:00
commit 800388cda4
146 changed files with 53227 additions and 0 deletions

1577
website/design.html Normal file

File diff suppressed because it is too large Load diff

224
website/download.html Normal file
View file

@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download — ItsGoin</title>
<meta name="description" content="Download the ItsGoin app for Android and Linux. Free, open source, Apache 2.0.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">ItsGoin</a>
<div class="links">
<a href="index.html">About</a>
<a href="tech.html">How It Works</a>
<a href="design.html">Design</a>
<a href="download.html" class="active">Download</a>
</div>
</nav>
<div class="container">
<section>
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.25rem;">Download ItsGoin</h1>
<p>Available for Android and Linux. Free and open source.</p>
<p style="color: var(--text-muted); font-size: 0.85rem;">Version 0.3.2 &mdash; March 15, 2026</p>
<div class="downloads">
<a href="itsgoin-0.3.2.apk" class="download-btn btn-android">
Android APK
<span class="sub">v0.3.2</span>
</a>
<a href="itsgoin_0.3.2_amd64.AppImage" class="download-btn btn-linux">
Linux AppImage
<span class="sub">v0.3.2</span>
</a>
</div>
</section>
<section>
<h2>Installation</h2>
<div class="card">
<h3 style="color: var(--accent);">Android</h3>
<ol class="steps">
<li><strong>Download the APK</strong> &mdash; Tap the button above. Your browser may warn that this type of file can be harmful &mdash; tap <strong>Download anyway</strong>.</li>
<li><strong>Open the file</strong> &mdash; When the download finishes, tap the notification or find <code>itsgoin-0.3.2.apk</code> in your Downloads folder and tap it.</li>
<li><strong>Allow installation</strong> &mdash; Android will ask you to allow installs from this source. Tap <strong>Settings</strong>, toggle <strong>"Allow from this source"</strong>, then go back and tap <strong>Install</strong>.</li>
<li><strong>Launch the app</strong> &mdash; Once installed, tap <strong>Open</strong> or find ItsGoin in your app drawer.</li>
</ol>
<div class="note">
<strong>Note:</strong> If you don't see the "Allow from this source" prompt, go to <strong>Settings &rarr; Apps &rarr; Special access &rarr; Install unknown apps</strong> and enable it for your browser.
</div>
</div>
<div class="card">
<h3 style="color: var(--green);">Linux (AppImage)</h3>
<ol class="steps">
<li><strong>Download the AppImage</strong> &mdash; Click the button above to download.</li>
<li><strong>Make it executable</strong> &mdash; Open a terminal and run:<br><code>chmod +x itsgoin_0.3.2_amd64.AppImage</code></li>
<li><strong>Run it</strong> &mdash; Double-click the file, or from the terminal:<br><code>./itsgoin_0.3.2_amd64.AppImage</code></li>
</ol>
<div class="note">
<strong>Note:</strong> If it doesn't launch, you may need to install FUSE:<br><code>sudo apt install libfuse2</code> (Debian/Ubuntu) or <code>sudo dnf install fuse</code> (Fedora).
</div>
</div>
</section>
<section>
<h2>Changelog</h2>
<div class="changelog">
<div class="changelog-date">v0.3.2 &mdash; March 15, 2026</div>
<ul>
<li><strong>Bidirectional engagement propagation</strong> &mdash; Reactions and comments now flow both upstream (toward author) and downstream through the CDN tree. Previously only downstream propagation existed, so the post author often never received reactions.</li>
<li><strong>Auto downstream registration</strong> &mdash; Nodes that receive a post via pull sync or push notification automatically register as downstream peers. This ensures engagement diffs reach all holders without manual registration.</li>
<li><strong>Upstream tracking</strong> &mdash; New <code>post_upstream</code> table records which peer each post was received from, enabling engagement to flow back toward the author hop-by-hop through the CDN tree.</li>
<li><strong>N2/N3 freshness</strong> &mdash; TTL reduced from 7 days to 5 hours. Full N1/N2 state re-broadcast every 4 hours catches missed diffs. Disconnect cleanup already removes departed peer's contributions immediately.</li>
<li><strong>Bootstrap isolation recovery</strong> &mdash; 24 hours after startup, nodes check if the bootstrap anchor is within their N1/N2/N3 reach. If absent, they reconnect and request referrals. The bootstrap is added to sticky N1 for 24 hours so mesh peers discover it via diffs, bridging isolated network segments back together.</li>
<li><strong>Following: Online/Offline</strong> &mdash; People tab splits followed peers into Online and Offline sections with &ldquo;Last online&rdquo; timestamps. DMs no longer appear in My Posts.</li>
<li><strong>Video playback</strong> &mdash; Videos now play correctly in-app on both desktop and Android.</li>
<li><strong>On-demand blob fetch</strong> &mdash; Media attachments that haven't synced locally are fetched from peers when you view them.</li>
<li><strong>Image loading fix</strong> &mdash; Asset protocol properly configured for streaming images from disk. Fallback to IPC if asset protocol fails.</li>
<li><strong>Image lightbox</strong> &mdash; Click any image to view full-size. Escape or click to close.</li>
<li><strong>File attachments</strong> &mdash; Attach any file type to posts. Non-media files show as download buttons with a trust warning prompt.</li>
<li><strong>Audio player</strong> &mdash; Audio attachments display with native playback controls.</li>
<li><strong>Video download</strong> &mdash; Download button under video player saves to Downloads folder.</li>
<li><strong>Share link fix</strong> &mdash; Fixed redirect to <code>0.0.0.0</code> &mdash; unroutable addresses are now skipped, falling through to QUIC proxy correctly.</li>
<li><strong>TCP hole punch protocol</strong> &mdash; New wire messages (TcpPunchRequest/Result) enable direct HTTP serving from nodes to browsers, reducing proxy load on itsgoin.net.</li>
<li><strong>Tiered web serving</strong> &mdash; Share links try direct redirect to HTTP-capable post holders before falling back to QUIC proxy.</li>
</ul>
<div class="changelog-date">v0.3.1 &mdash; March 13, 2026</div>
<ul>
<li><strong>Engagement sync</strong> &mdash; Pull sync now fetches reactions, comments, and policies from peers. Previously these only propagated via real-time push.</li>
<li><strong>Profile push fix</strong> &mdash; Name and bio changes now sent to all connected peers immediately, not just audience members.</li>
<li><strong>Auto-sync on follow</strong> &mdash; Following someone immediately pulls their posts into your feed.</li>
<li><strong>Popover UI</strong> &mdash; Notifications, diagnostics, and message threads open as overlay windows instead of inline sections.</li>
<li><strong>Notification settings</strong> &mdash; Configure message, post, and nearby-user notifications from Settings.</li>
<li><strong>Smart DM polling</strong> &mdash; Message refresh rate scales with conversation recency (5s&ndash;daily).</li>
<li><strong>Reaction display</strong> &mdash; Posts show top 5 emoji reactions + total response count.</li>
</ul>
<div class="changelog-date">v0.3.0 &mdash; March 12, 2026</div>
<ul>
<li><strong>Full rename distsoc &rarr; ItsGoin</strong> &mdash; ALPN, crypto contexts, data paths, Android package ID all changed. Clean break &mdash; incompatible with prior versions.</li>
</ul>
<div class="changelog-date">v0.2.11 &mdash; March 12, 2026</div>
<ul>
<li><strong>Engagement system</strong> &mdash; Reactions (public + private encrypted), inline comments with ed25519 signatures, and author-controlled comment/react policies. Full UI with emoji picker, reaction pills, and expandable comment threads.</li>
<li><strong>CDN tree for all posts</strong> &mdash; New <code>post_downstream</code> table gives every post (including text-only) a propagation tree. Engagement diffs flow through the file layer via BlobHeaderDiff (0xD0), never mesh.</li>
<li><strong>4 new wire messages</strong> &mdash; BlobHeaderDiff (0xD0), BlobHeaderRequest/Response (0xD1/0xD2), PostDownstreamRegister (0xD3). Policy enforcement in handlers (blocklist, audience-only, permission checks).</li>
<li><strong>Thread splitting</strong> &mdash; Headers exceeding 16KB automatically split oldest comments into linked thread posts, keeping engagement propagation lightweight.</li>
</ul>
<div class="changelog-date">v0.2.10 &mdash; March 12, 2026</div>
<ul>
<li><strong>Per-family NAT classification</strong> &mdash; IPv4 and IPv6 public reachability detected independently. Fixes false "public (v4+v6)" when only IPv6 is public, which was silently breaking IPv4 hole punches.</li>
<li><strong>STUN always runs</strong> &mdash; IPv6-only anchors now correctly probe their IPv4 NAT type instead of assuming "Public". Only <code>--bind</code> (explicit server) skips STUN.</li>
<li><strong>Anchor address fallback</strong> &mdash; Anchors without <code>--bind</code> or UPnP now advertise their public IPv6 address, so peers save them in known_anchors for preferential reconnection.</li>
<li><strong>Bootstrap deprioritization</strong> &mdash; Discovered anchors are tried before hardcoded bootstrap anchors at startup, reducing bootstrap server load as the network grows.</li>
</ul>
<div class="changelog-date">v0.2.9 &mdash; March 12, 2026</div>
<ul>
<li><strong>ConnectionManager actor redesign</strong> &mdash; Replaced global mutex with actor pattern (ConnHandle + ConnectionActor). Network operations no longer block each other &mdash; 14 I/O code paths that previously held the lock for up to 15 seconds now run concurrently.</li>
<li><strong>60+ call sites migrated</strong> &mdash; All network and node code uses the new lock-free ConnHandle API. Public mutex accessor removed.</li>
<li><strong>I/O extraction</strong> &mdash; Broadcast, push, pull, relay, anchor register, and address resolution all execute outside state locks.</li>
</ul>
<div class="changelog-date">v0.2.8 &mdash; March 11, 2026</div>
<ul>
<li><strong>NAT filter probe</strong> &mdash; Anchor probes your NAT filtering type (address-restricted vs port-restricted) by attempting to reach you from a different source port. Eliminates unnecessary port scanning for the majority of connections.</li>
<li><strong>Role-based NAT traversal</strong> &mdash; Each side now takes the correct role based on its NAT profile: EIM nodes punch every 2s (stable port), EDM nodes walk outward at 100/sec (opening firewall entries). Previous burst-scan design replaced with steady ~100 ports/sec outward walk.</li>
<li><strong>IPv4 vs IPv6 public</strong> &mdash; Public reachability now distinguished by protocol family (v4 only, v6 only, v4+v6). Prevents false assumptions when only one family is public.</li>
<li><strong>Scanning cleanup</strong> &mdash; Scan tasks properly cancelled via JoinSet between attempts. Previous design spawned 37K+ fire-and-forget tasks.</li>
</ul>
<div class="changelog-date">v0.2.7 &mdash; March 11, 2026</div>
<ul>
<li><strong>Focused port scanning</strong> &mdash; Scanning now targets only the anchor-observed IP address instead of all self-reported addresses, reducing wasted scan budget on unreachable VPN/cellular IPs.</li>
<li><strong>Scan on unknown NAT</strong> &mdash; Port scanning now triggers when peer NAT type is unknown (e.g. peer running older version), not just when explicitly EDM.</li>
</ul>
<div class="changelog-date">v0.2.6 &mdash; March 11, 2026</div>
<ul>
<li><strong>Anchor self-verification</strong> &mdash; Nodes with public addresses now verify their reachability via cold-connect probes from N2 strangers. Two consecutive failures revoke anchor status.</li>
<li><strong>Advanced NAT traversal</strong> &mdash; Hard+hard NAT pairs are no longer skipped. Port scanning (tiered: &plusmn;500, &plusmn;2000, full ephemeral range) attempts direct connection before falling back to relay.</li>
<li><strong>NAT profile sharing</strong> &mdash; Peers exchange mapping (EIM/EDM) and filtering (open/port-restricted) classification during connection, enabling smarter hole punch decisions.</li>
<li><strong>Behavioral NAT inference</strong> &mdash; Filtering type is refined from actual connection outcomes &mdash; no unreliable upfront probing needed.</li>
</ul>
<div class="changelog-date">v0.2.3 &mdash; March 11, 2026</div>
<ul>
<li><strong>NAT type detection</strong> &mdash; STUN probing on startup classifies your NAT as Public, Easy, or Hard. Shared with peers during connection.</li>
<li><strong>Skip futile hole punches</strong> &mdash; When both peers are behind hard/symmetric NATs, skip the 30-second hole punch attempt entirely.</li>
</ul>
<div class="changelog-date">March 10, 2026 (v2)</div>
<ul>
<li><strong>Anchor advertised address</strong> &mdash; Anchors advertise their stable bind address in initial exchange, so peers always store the correct reconnection address.</li>
<li><strong>Observed address notification</strong> &mdash; Peers tell each other "I see you at &lt;ip:port&gt;" during connection &mdash; lightweight STUN-like address feedback.</li>
</ul>
<div class="changelog-date">March 10, 2026</div>
<ul>
<li><strong>UPnP port mapping</strong> &mdash; Devices behind NAT routers automatically request a port forward via UPnP, becoming directly reachable without manual configuration.</li>
<li><strong>Auto-anchor promotion</strong> &mdash; Devices with a successful UPnP mapping self-declare as anchors, improving bootstrap diversity for all peers.</li>
<li><strong>Hole punch fix</strong> &mdash; Target-side hole punch connections were silently discarded; now properly registered as sessions so both sides of a NAT punch succeed.</li>
</ul>
<div class="changelog-date">February 24, 2026 (v2)</div>
<ul>
<li><strong>Request Referrals button</strong> &mdash; On-demand peer referral requesting in Network Diagnostics.</li>
<li><strong>Docker bridge IP filtering</strong> &mdash; Peer introductions no longer leak Docker bridge IPs.</li>
<li><strong>Keepalive fix</strong> &mdash; Sender-side last_activity update prevents false zombie detection.</li>
<li><strong>Message threads</strong> &mdash; DM conversations grouped by partner with expandable thread view.</li>
<li><strong>Mesh/reach diagnostics</strong> &mdash; Peer cards show reach level badges (Mesh/N1/N2/N3).</li>
</ul>
<div class="changelog-date">February 24, 2026</div>
<ul>
<li><strong>Audience request button</strong> &mdash; "Ask to join audience" on followed peers.</li>
<li><strong>DMs filtered from feed</strong> &mdash; Direct messages show only in Messages tab.</li>
<li><strong>Peer bios displayed</strong> &mdash; Bio text shown below peer names in People tab.</li>
<li><strong>People tab auto-refresh</strong> &mdash; Follows, peers, audience refresh every 10 seconds.</li>
<li><strong>Button feedback</strong> &mdash; Loading state and toast confirmation on diagnostics refresh.</li>
</ul>
<div class="changelog-date">February 23, 2026</div>
<ul>
<li><strong>Fix connection lock contention</strong> &mdash; QUIC connect no longer blocks all tasks.</li>
<li><strong>Image attachments display</strong> &mdash; Fixed CSP blocking blob: URLs.</li>
<li><strong>Non-blocking startup</strong> &mdash; Referral connections run in background.</li>
<li><strong>Connect timeout</strong> &mdash; 15-second cap on connection attempts.</li>
<li><strong>Android foreground service</strong> &mdash; Mesh stays alive in background.</li>
</ul>
<div class="changelog-date">February 20, 2026</div>
<ul>
<li><strong>IPv4-mapped address fix</strong> &mdash; Normalized dual-stack addresses for NAT traversal.</li>
<li><strong>Mesh keepalive</strong> &mdash; 30-second pings prevent zombie detection on idle connections.</li>
<li><strong>Audience management</strong> &mdash; Approve/Deny/Remove audience members.</li>
<li><strong>Network diagnostics overhaul</strong> &mdash; Summary grid, peer cards, connection breakdown.</li>
<li><strong>Manual rebalance</strong> &mdash; Trigger immediate mesh rebalancing from Settings.</li>
<li><strong>Reset all data</strong> &mdash; Clear local data while preserving identity key.</li>
</ul>
<div class="changelog-date">February 19, 2026</div>
<ul>
<li><strong>Reactive mesh growth loop</strong> &mdash; New peers connect within seconds.</li>
<li><strong>Anchor matchmaking only</strong> &mdash; Anchors introduce peers, never proxy bytes.</li>
<li><strong>Parallel hole punching</strong> &mdash; NAT traversal tries all addresses simultaneously.</li>
<li><strong>Relay-observed address injection</strong> &mdash; Fixes NAT-to-NAT hole punching.</li>
<li><strong>Growth loop introduction fallback</strong> &mdash; Bilateral hole punching when direct fails.</li>
</ul>
</div>
</section>
</div>
<footer>
<p>ItsGoin &mdash; Apache 2.0 License &mdash; <a href="https://itsgoin.com">itsgoin.com</a></p>
</footer>
</body>
</html>

135
website/index.html Normal file
View file

@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ItsGoin — Social Media That's Yours</title>
<meta name="description" content="Social media with no account, no ads, no algorithm. Your posts, your people, your device. Free forever, open source.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">ItsGoin</a>
<div class="links">
<a href="index.html" class="active">About</a>
<a href="tech.html">How It Works</a>
<a href="design.html">Design</a>
<a href="download.html">Download</a>
</div>
</nav>
<div class="hero">
<p class="tagline" style="margin-bottom: 0.25rem; font-size: 1.1rem; color: var(--text-muted);">"How's it going?"</p>
<h1>It's goin.</h1>
<p class="tagline">Social media that's actually yours. No algorithm deciding what you see. No company deciding what you can say. Just your people, in order, the way it should be.</p>
<div class="cta">
<a href="download.html" class="btn btn-primary">Download</a>
<a href="tech.html" class="btn btn-secondary">How It Works</a>
</div>
</div>
<div class="container">
<section>
<h2>Your feed, your way</h2>
<p>When you open ItsGoin, you see posts from your family and friends &mdash; <strong>newest first</strong>. That's it. No algorithm choosing what to show you, no promoted content, no engagement bait pushed to the top.</p>
<p>Want more? You can browse what your friends recommend, or see what's popular around you. But that's <strong>your choice</strong>, not a default you can't turn off.</p>
<p><strong>Nobody controls what you say. Nobody controls what you see.</strong></p>
</section>
<section>
<h2>No account. No ads. No catch.</h2>
<p>Open the app. Pick a name. You're in. No email address. No phone number. No verification. No sign-up form.</p>
<p>There are no ads &mdash; not because we chose not to run them, but because there's <strong>no company in the middle</strong> to sell them. There never will be.</p>
<p>ItsGoin is free forever. Not "free with limits" or "free for now." It's a gift, open source under the <strong>Apache 2.0 license</strong>. There's nothing to buy.</p>
</section>
<section>
<h2>Why this exists</h2>
<p>Every social platform starts the same way: connect people. Then come the ads, the algorithms, the data harvesting, the content moderation politics, the enshittification.</p>
<p>The root cause is always the same: one company owns the server, so they own you. ItsGoin removes the middleman entirely. <strong>Your data lives on your device.</strong> Your connections are private because of how it's built, not because of a promise someone made. No company can change the rules because there is no company in the middle.</p>
<p>We're not trying to replace every social network. We're building the one that <strong>can't betray you</strong>, even if it wanted to.</p>
</section>
<section>
<h2>What you get</h2>
<div class="features">
<div class="feature">
<div class="icon icon-blue">feed</div>
<h3>Your feed, not an algorithm's</h3>
<p>Family and friends, newest first. See what they recommend if you want. You choose &mdash; always.</p>
</div>
<div class="feature">
<div class="icon icon-green">E2E</div>
<h3>Private messages that are actually private</h3>
<p>Your messages are encrypted so only you and the recipient can read them. Not the network. Not us. Nobody.</p>
</div>
<div class="feature">
<div class="icon icon-amber">eye</div>
<h3>Who you follow is your business</h3>
<p>Your follow list is hidden from everyone. The network works without knowing your social connections. Even your contacts can't see your full list.</p>
</div>
<div class="feature">
<div class="icon icon-green">grp</div>
<h3>Private circles</h3>
<p>Create groups for family, close friends, your team. What's shared in a circle stays in the circle.</p>
</div>
<div class="feature">
<div class="icon icon-amber">file</div>
<h3>Share photos and files</h3>
<p>Attach images and files to posts. They're stored on your device and shared directly with your people.</p>
</div>
<div class="feature">
<div class="icon icon-blue">own</div>
<h3>You own everything</h3>
<p>Your posts, your connections, your identity &mdash; all on your device. No account to delete because there's no account to create.</p>
</div>
<div class="feature">
<div class="icon icon-green">net</div>
<h3>Can't be shut down</h3>
<p>There's no server to unplug. No company to go bankrupt. As long as people run the app, the network exists.</p>
</div>
<div class="feature">
<div class="icon icon-blue">app</div>
<h3>Works on Android and Linux</h3>
<p>Download the app and go. Mobile and desktop, with more platforms coming.</p>
</div>
</div>
</section>
<section>
<h2>Who is this for?</h2>
<p><strong>Families</strong> who want to share photos without a corporation watching.</p>
<p><strong>Friend groups</strong> tired of algorithms burying each other's posts.</p>
<p><strong>Communities</strong> that want private conversation without corporate terms of service.</p>
<p><strong>Anyone</strong> who thinks social media should work for people, not advertisers.</p>
</section>
<section>
<h2>Honest about what we are</h2>
<p>We believe in being upfront:</p>
<div class="card">
<h3>Early and growing</h3>
<p>ItsGoin works today for small groups of friends and communities. Larger audiences are on the roadmap but not there yet.</p>
</div>
<div class="card">
<h3>Your people need to be online</h3>
<p>Posts stay available as long as someone who has them is online. If the author and everyone who saw it go offline, the post goes with them. This is social sharing, not cloud storage.</p>
</div>
<div class="card">
<h3>No global search</h3>
<p>You find people through your connections, not a search engine. This is intentional &mdash; it's how real social networks grow.</p>
</div>
</section>
<section>
<h2>Get involved</h2>
<p>ItsGoin is open source and always will be.</p>
<p><a href="download.html">Download the app</a> &middot; <a href="tech.html">Read the technical overview</a> &middot; <a href="design.html">Explore the design document</a></p>
</section>
</div>
<footer>
<p>ItsGoin &mdash; Apache 2.0 License &mdash; <a href="https://itsgoin.com">itsgoin.com</a></p>
</footer>
</body>
</html>

297
website/style.css Normal file
View file

@ -0,0 +1,297 @@
/* itsgoin.com — distsoc website */
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0c0f14;
--surface: #161b24;
--border: #1e2530;
--text: #e2e8f0;
--text-muted: #8892a4;
--accent: #3b82f6;
--accent-dim: #1e3a5f;
--green: #22c55e;
--green-dim: #14532d;
--amber: #f59e0b;
--amber-dim: #78350f;
}
body {
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* --- Nav --- */
nav {
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
align-items: center;
gap: 2rem;
max-width: 1100px;
margin: 0 auto;
}
nav .logo {
font-size: 1.25rem;
font-weight: 700;
color: var(--text);
letter-spacing: -0.02em;
}
nav .logo span { color: var(--text-muted); font-weight: 400; }
nav .links { display: flex; gap: 1.5rem; }
nav .links a {
color: var(--text-muted);
font-size: 0.9rem;
font-weight: 500;
transition: color 0.15s;
}
nav .links a:hover, nav .links a.active { color: var(--text); text-decoration: none; }
/* --- Layout --- */
.container { max-width: 800px; margin: 0 auto; padding: 3rem 2rem; }
.wide { max-width: 1000px; }
/* --- Hero --- */
.hero {
text-align: center;
padding: 5rem 2rem 3rem;
max-width: 700px;
margin: 0 auto;
}
.hero h1 {
font-size: 3rem;
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--text) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero .tagline {
font-size: 1.25rem;
color: var(--text-muted);
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.hero .cta {
display: inline-flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.btn {
display: inline-block;
padding: 0.75rem 1.75rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
transition: filter 0.15s, transform 0.1s;
border: none;
cursor: pointer;
}
.btn:hover { filter: brightness(1.1); transform: translateY(-1px); text-decoration: none; }
.btn-primary { background: var(--accent); color: white; }
.btn-secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
/* --- Section --- */
section { padding: 3rem 0; }
section + section { border-top: 1px solid var(--border); }
h2 {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
h3 {
font-size: 1.15rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text);
}
p { margin-bottom: 1rem; color: var(--text-muted); }
p strong { color: var(--text); }
/* --- Feature grid --- */
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.feature {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
}
.feature .icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
margin-bottom: 0.75rem;
}
.feature .icon-blue { background: var(--accent-dim); }
.feature .icon-green { background: var(--green-dim); }
.feature .icon-amber { background: var(--amber-dim); }
.feature h3 { margin-bottom: 0.4rem; }
.feature p { font-size: 0.9rem; margin-bottom: 0; }
/* --- Cards --- */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.75rem;
margin-bottom: 1.5rem;
}
.card h3 { margin-bottom: 0.5rem; }
.card p:last-child { margin-bottom: 0; }
/* --- Download buttons --- */
.downloads {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin: 1.5rem 0;
}
.download-btn {
display: inline-block;
color: white;
padding: 0.85rem 2rem;
border-radius: 10px;
font-size: 1.1rem;
font-weight: 600;
transition: filter 0.2s, transform 0.1s;
}
.download-btn:hover { filter: brightness(0.9); transform: translateY(-1px); text-decoration: none; }
.download-btn .sub { font-size: 0.82rem; font-weight: 400; opacity: 0.75; display: block; margin-top: 0.15rem; }
.btn-android { background: var(--accent); }
.btn-linux { background: var(--green); }
/* --- Steps --- */
.steps { list-style: none; padding: 0; counter-reset: step; }
.steps li {
counter-increment: step;
padding: 0.65rem 0 0.65rem 2.5rem;
position: relative;
border-bottom: 1px solid var(--border);
font-size: 0.95rem;
color: var(--text-muted);
}
.steps li:last-child { border-bottom: none; }
.steps li::before {
content: counter(step);
position: absolute; left: 0; top: 0.65rem;
width: 26px; height: 26px; border-radius: 50%;
color: white; display: flex; align-items: center;
justify-content: center; font-size: 0.8rem; font-weight: 700;
background: var(--accent);
}
.steps li strong { color: var(--text); }
/* --- Note box --- */
.note {
background: var(--amber-dim);
border: 1px solid #92400e;
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: #fbbf24;
margin-top: 0.75rem;
}
.note strong { color: #fde68a; }
/* --- Changelog --- */
.changelog-date {
font-size: 0.82rem;
color: var(--text-muted);
margin: 1.5rem 0 0.5rem;
font-weight: 600;
}
.changelog-date:first-child { margin-top: 0; }
.changelog ul { padding-left: 1.25rem; margin-bottom: 0.5rem; }
.changelog li { margin-bottom: 0.4rem; font-size: 0.93rem; color: var(--text-muted); }
.changelog li strong { color: var(--text); }
/* --- Table --- */
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
font-size: 0.9rem;
}
th, td {
text-align: left;
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border);
}
th { color: var(--text); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
td { color: var(--text-muted); }
/* --- Code --- */
code {
background: var(--surface);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-size: 0.88rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
color: var(--green);
}
pre {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.25rem;
overflow-x: auto;
margin: 1rem 0;
font-size: 0.85rem;
line-height: 1.6;
}
pre code { background: none; padding: 0; }
/* --- Badge --- */
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge-complete { background: var(--green-dim); color: var(--green); }
.badge-partial { background: var(--amber-dim); color: var(--amber); }
.badge-planned { background: var(--accent-dim); color: var(--accent); }
/* --- Footer --- */
footer {
border-top: 1px solid var(--border);
padding: 2rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
footer a { color: var(--text-muted); }
footer a:hover { color: var(--text); }
/* --- Responsive --- */
@media (max-width: 600px) {
nav { padding: 0.75rem 1rem; gap: 1rem; flex-wrap: wrap; }
.hero { padding: 3rem 1.5rem 2rem; }
.hero h1 { font-size: 2rem; }
.container { padding: 2rem 1.25rem; }
.features { grid-template-columns: 1fr; }
}

202
website/tech.html Normal file
View file

@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How It Works — ItsGoin</title>
<meta name="description" content="Technical overview of ItsGoin: mesh networking, encryption, sync protocol, content addressing, and peer discovery.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
<a href="index.html" class="logo">ItsGoin</a>
<div class="links">
<a href="index.html">About</a>
<a href="tech.html" class="active">How It Works</a>
<a href="design.html">Design</a>
<a href="download.html">Download</a>
</div>
</nav>
<div class="container wide">
<section>
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.5rem;">How ItsGoin works</h1>
<p>A technical overview of the networking, encryption, and sync mechanisms. For the full design rationale, see the <a href="design.html">design document</a>.</p>
</section>
<!-- Networking -->
<section>
<h2>Networking: QUIC mesh over iroh</h2>
<p>ItsGoin uses <a href="https://iroh.computer">iroh</a> (v0.96) for peer-to-peer networking. iroh provides QUIC transport with built-in NAT traversal, hole punching, and mDNS LAN discovery. Every node has a persistent ed25519 identity key.</p>
<div class="card">
<h3>Connection types</h3>
<table>
<tr><th>Type</th><th>Slots</th><th>Lifetime</th><th>Purpose</th></tr>
<tr><td><strong>Mesh</strong></td><td>101 (desktop), 15 (mobile)</td><td>Long-lived</td><td>Structural backbone for routing and propagation</td></tr>
<tr><td><strong>Session</strong></td><td>20 (desktop), 5 (mobile)</td><td>Minutes</td><td>Active interactions: DMs, delivery, peer discovery</td></tr>
<tr><td><strong>Ephemeral</strong></td><td>Unlimited</td><td>Single request</td><td>One-off sync or blob fetch</td></tr>
</table>
</div>
<div class="card">
<h3>Mesh architecture (101 slots)</h3>
<p>Desktop nodes maintain 101 mesh connections divided into:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>10 Preferred</strong> &mdash; bilateral agreements with close peers, protected from eviction</li>
<li><strong>71 Local</strong> &mdash; discovered through N2 diversity scoring</li>
<li><strong>20 Wide</strong> &mdash; random connections for network-wide reach</li>
</ul>
<p>New mesh connections require bilateral agreement: the connector sends an <code>InitialExchange</code> message, and the acceptor either grants a slot or sends <code>RefuseRedirect</code> with an alternative peer suggestion.</p>
</div>
<div class="card">
<h3>Growth loop</h3>
<p>Nodes reactively grow their mesh using diversity scoring. When a slot opens, the node evaluates N2 candidates (peers reported by mesh connections) and selects the one that maximizes reach into new parts of the network. The scoring formula:</p>
<pre><code>score = 1/reporter_count + 0.3 if not_already_in_N3</code></pre>
<p>Low reporter count means fewer of your existing peers know this candidate &mdash; connecting to them brings the most new reach. The growth loop backs off after 3 consecutive failures to avoid wasting resources on unreachable candidates.</p>
</div>
</section>
<!-- Knowledge layers -->
<section>
<h2>Knowledge layers: N1 / N2 / N3</h2>
<p>Each node has three layers of network awareness, built passively from mesh peer exchanges:</p>
<div class="card">
<table>
<tr><th>Layer</th><th>Contains</th><th>Source</th><th>Shared?</th></tr>
<tr><td><strong>N1 (Mesh)</strong></td><td>Live connections + social contacts</td><td>Direct</td><td>Yes (merged, no addresses)</td></tr>
<tr><td><strong>N2 (Reach)</strong></td><td>Peers' N1 shares</td><td>1 hop</td><td>Yes (NodeIds only)</td></tr>
<tr><td><strong>N3 (Search)</strong></td><td>Peers' N2 shares</td><td>2 hops</td><td>Never</td></tr>
</table>
</div>
<p><strong>Privacy guarantee</strong>: N1 shares merge mesh peers with social contacts into one list, making it impossible for outsiders to distinguish infrastructure connections from social relationships. Addresses are never shared &mdash; they're resolved on-demand through the chain.</p>
<p><strong>Discovery cascade</strong> when connecting to a specific peer:</p>
<ol class="steps">
<li><strong>Social route cache</strong> &mdash; cached addresses for follows/audience</li>
<li><strong>Peers table</strong> &mdash; stored addresses from previous connections</li>
<li><strong>N2: ask reporter</strong> &mdash; ask the mesh peer who reported the target</li>
<li><strong>N3: chain resolve</strong> &mdash; 2-hop chain through reporters</li>
<li><strong>Worm search</strong> &mdash; fan-out to all mesh peers, bloom to wide referrals</li>
<li><strong>Relay introduction</strong> &mdash; hole punch via intermediary</li>
<li><strong>Own-device relay</strong> &mdash; route through your own devices (e.g. home computer to phone) when direct connection fails</li>
</ol>
</section>
<!-- Encryption -->
<section>
<h2>Encryption: envelope model</h2>
<p>ItsGoin uses <strong>1-layer envelope encryption</strong> for all private content. The design preserves content addressing (PostId = BLAKE3 of ciphertext) while allowing visibility updates without changing the PostId.</p>
<div class="card">
<h3>Per-post encryption</h3>
<p>Each private post gets a random Content Encryption Key (CEK). The post body is encrypted with <strong>ChaCha20-Poly1305</strong> using this CEK. The CEK is then wrapped separately for each intended recipient using <strong>X25519 Diffie-Hellman</strong> derived from ed25519 identity keys.</p>
<pre><code>PostId = BLAKE3(ciphertext)
CEK = random 256-bit key
ciphertext = ChaCha20-Poly1305(post_body, CEK)
wrapped_key[i] = X25519_DH(author_ed25519, recipient_ed25519[i]) XOR CEK</code></pre>
<p>Visibility is separate metadata &mdash; re-wrapping the CEK for new recipients doesn't change the PostId or require re-encrypting the content.</p>
</div>
<div class="card">
<h3>Group keys for circles</h3>
<p>Circles (private groups) use per-circle ed25519 keypairs instead of per-recipient wrapping. A single DH operation wraps the CEK for the entire group, keeping overhead constant regardless of group size (~100 bytes vs. ~500 bytes per recipient).</p>
<p><strong>Epoch rotation</strong>: when a member is removed, the circle generates a new keypair (new epoch). The new seed is distributed to remaining members. Old posts remain readable with old keys; new posts use the new epoch. Forward secrecy without re-encrypting history.</p>
</div>
<div class="card">
<h3>Visibility levels</h3>
<table>
<tr><th>Level</th><th>Encryption</th><th>Audience</th></tr>
<tr><td><strong>Public</strong></td><td>None</td><td>Anyone</td></tr>
<tr><td><strong>Friends</strong></td><td>Per-recipient CEK wrapping</td><td>Mutual follows</td></tr>
<tr><td><strong>Circle</strong></td><td>Group key CEK wrapping</td><td>Circle members</td></tr>
<tr><td><strong>Direct</strong></td><td>Per-recipient CEK wrapping</td><td>Specified recipients</td></tr>
</table>
</div>
</section>
<!-- Sync -->
<section>
<h2>Sync protocol (v3)</h2>
<p>The protocol uses a single ALPN (<code>itsgoin/3</code>) with 37+ message types multiplexed over QUIC bi-streams and uni-streams.</p>
<div class="card">
<h3>Pull-based sync</h3>
<p>Every 5 minutes, nodes pull posts from connected peers. The <strong>sender</strong> filters posts before sending &mdash; only posts the requester is authorized to see (based on follows, encryption recipients, and audience membership). This means the requester never learns about posts they can't decrypt.</p>
</div>
<div class="card">
<h3>Push for immediacy</h3>
<p>Real-time push via uni-streams for instant delivery:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>PostNotification</strong> (0x42) &mdash; lightweight "new post" alert to mesh peers</li>
<li><strong>PostPush</strong> (0x43) &mdash; direct encrypted delivery to recipients</li>
<li><strong>ProfileUpdate</strong> (0x50) &mdash; instant profile propagation</li>
<li><strong>DeleteRecord</strong> (0x51) &mdash; signed deletion propagation</li>
<li><strong>VisibilityUpdate</strong> (0x52) &mdash; re-wrap CEK without changing PostId</li>
</ul>
</div>
</section>
<!-- Files -->
<section>
<h2>Files &amp; content distribution</h2>
<div class="card">
<h3>Content-addressed blobs</h3>
<p>Files are stored as content-addressed blobs: <code>BlobId = BLAKE3(file_bytes)</code>. Stored on the filesystem in 256 shards (<code>blobs/{hex[0..2]}/{hex}</code>) with metadata in SQLite. Max 4 attachments per post, 10MB each.</p>
</div>
<div class="card">
<h3>CDN hosting tree</h3>
<p>Each blob has a hosting tree: 1 upstream source + up to 100 downstream nodes. <strong>AuthorManifest</strong> (ed25519-signed) carries the author's 10 most recent posts as a neighborhood, traveling with blob responses. <strong>ManifestPush</strong> propagates updates down the tree.</p>
<p>When a node evicts a blob, it sends <strong>BlobDeleteNotice</strong> so the tree can heal &mdash; downstream nodes find a new upstream.</p>
</div>
<div class="card">
<h3>Eviction priority</h3>
<p>When storage fills up, blobs are evicted using a social-aware scoring formula:</p>
<pre><code>priority = pin_boost + relationship * recency * freshness / (peer_copies + 1)</code></pre>
<p>Your own blobs are auto-pinned and never evicted. Blobs from people you follow score higher. Blobs with many copies elsewhere score lower (safe to drop).</p>
</div>
</section>
<!-- Anchors -->
<section>
<h2>Anchors: always-on peers</h2>
<p>Anchors are standard ItsGoin nodes running on stable servers. They're regular peers that happen to be always on, which makes them useful for two things:</p>
<ul style="padding-left: 1.25rem; margin: 0.5rem 0; color: var(--text-muted);">
<li><strong>Bootstrap</strong> &mdash; new nodes connect here to discover the network and get referrals to other peers</li>
<li><strong>Matchmaker</strong> &mdash; introduce peers to each other so they can establish direct connections</li>
</ul>
<p>Anchors maintain a referral list of recently-seen peers with tiered usage caps (3/2/1 uses depending on list size). When an anchor's mesh is full, it keeps session connections for matchmaking so it remains useful as a bootstrap point.</p>
<p>Anchors run the same code as every other node. No special protocol, no special trust, no special services. They're just peers that are always on.</p>
</section>
<!-- Stack -->
<section>
<h2>Technology stack</h2>
<div class="card">
<table>
<tr><th>Component</th><th>Technology</th></tr>
<tr><td>Core library</td><td>Rust</td></tr>
<tr><td>P2P networking</td><td>iroh 0.96 (QUIC + mDNS)</td></tr>
<tr><td>Local storage</td><td>SQLite (rusqlite 0.32)</td></tr>
<tr><td>Content addressing</td><td>BLAKE3</td></tr>
<tr><td>Encryption</td><td>ChaCha20-Poly1305 + X25519 (from ed25519)</td></tr>
<tr><td>Desktop/mobile shell</td><td>Tauri v2</td></tr>
<tr><td>Frontend</td><td>Plain HTML/CSS/JS (no build step)</td></tr>
<tr><td>Platforms</td><td>Android, Linux (AppImage)</td></tr>
</table>
</div>
</section>
</div>
<footer>
<p>ItsGoin &mdash; Apache 2.0 License &mdash; <a href="https://itsgoin.com">itsgoin.com</a></p>
</footer>
</body>
</html>