summaryrefslogtreecommitdiffstats
path: root/news
diff options
context:
space:
mode:
Diffstat (limited to 'news')
-rw-r--r--news/atom.xml1991
-rw-r--r--news/atom.xml.gzbin0 -> 39593 bytes
-rw-r--r--news/cli-daemon-rpc/index.html142
-rw-r--r--news/cli-daemon-rpc/index.html.gzbin0 -> 3219 bytes
-rw-r--r--news/hello-world/index.html80
-rw-r--r--news/hello-world/index.html.gzbin0 -> 1316 bytes
-rw-r--r--news/index.html198
-rw-r--r--news/index.html.gzbin0 -> 2530 bytes
-rw-r--r--news/packaging-archlinux/index.html123
-rw-r--r--news/packaging-archlinux/index.html.gzbin0 -> 2143 bytes
-rw-r--r--news/packaging-debian/index.html157
-rw-r--r--news/packaging-debian/index.html.gzbin0 -> 2548 bytes
-rw-r--r--news/phase0-foundation/index.html125
-rw-r--r--news/phase0-foundation/index.html.gzbin0 -> 2680 bytes
-rw-r--r--news/phase1-basic-network/index.html173
-rw-r--r--news/phase1-basic-network/index.html.gzbin0 -> 4056 bytes
-rw-r--r--news/phase2-replication/index.html201
-rw-r--r--news/phase2-replication/index.html.gzbin0 -> 4319 bytes
-rw-r--r--news/phase3-api-and-apps/index.html163
-rw-r--r--news/phase3-api-and-apps/index.html.gzbin0 -> 3930 bytes
-rw-r--r--news/phase4-encryption-sealed/index.html178
-rw-r--r--news/phase4-encryption-sealed/index.html.gzbin0 -> 4246 bytes
-rw-r--r--news/phase4-institutional-onboarding/index.html239
-rw-r--r--news/phase4-institutional-onboarding/index.html.gzbin0 -> 5539 bytes
-rw-r--r--news/phase4-nat-traversal/index.html228
-rw-r--r--news/phase4-nat-traversal/index.html.gzbin0 -> 5328 bytes
-rw-r--r--news/phase4-performance-tuning/index.html164
-rw-r--r--news/phase4-performance-tuning/index.html.gzbin0 -> 3854 bytes
-rw-r--r--news/phase4-shamir-heir-recovery/index.html199
-rw-r--r--news/phase4-shamir-heir-recovery/index.html.gzbin0 -> 4586 bytes
-rw-r--r--news/phase4-storage-deduplication/index.html217
-rw-r--r--news/phase4-storage-deduplication/index.html.gzbin0 -> 4807 bytes
-rw-r--r--news/phase4-wasm-browser-verification/index.html192
-rw-r--r--news/phase4-wasm-browser-verification/index.html.gzbin0 -> 4765 bytes
-rw-r--r--news/reed-solomon/index.html200
-rw-r--r--news/reed-solomon/index.html.gzbin0 -> 4736 bytes
36 files changed, 4970 insertions, 0 deletions
diff --git a/news/atom.xml b/news/atom.xml
new file mode 100644
index 0000000..660ecac
--- /dev/null
+++ b/news/atom.xml
@@ -0,0 +1,1991 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
+ <title>Tesseras - News</title>
+ <subtitle>P2P network for preserving human memories across millennia</subtitle>
+ <link rel="self" type="application/atom+xml" href="https://tesseras.net/news/atom.xml"/>
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/"/>
+ <generator uri="https://www.getzola.org/">Zola</generator>
+ <updated>2026-02-16T10:00:00+00:00</updated>
+ <id>https://tesseras.net/news/atom.xml</id>
+ <entry xml:lang="en">
+ <title>Packaging Tesseras for Debian</title>
+ <published>2026-02-16T10:00:00+00:00</published>
+ <updated>2026-02-16T10:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/packaging-debian/"/>
+ <id>https://tesseras.net/news/packaging-debian/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/packaging-debian/">&lt;p&gt;Tesseras now ships a &lt;code&gt;.deb&lt;&#x2F;code&gt; package for Debian and Ubuntu. This post walks
+through building and installing the package from source using &lt;code&gt;cargo-deb&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;&#x2F;h2&gt;
+&lt;p&gt;You need a working Rust toolchain and the required system libraries:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo apt install build-essential pkg-config libsqlite3-dev
+rustup toolchain install stable
+cargo install cargo-deb
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;building&quot;&gt;Building&lt;&#x2F;h2&gt;
+&lt;p&gt;Clone the repository and run the &lt;code&gt;just deb&lt;&#x2F;code&gt; recipe:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;git clone https:&#x2F;&#x2F;git.sr.ht&#x2F;~ijanc&#x2F;tesseras
+cd tesseras
+just deb
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;This recipe does three things:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;&lt;strong&gt;Compiles&lt;&#x2F;strong&gt; &lt;code&gt;tesd&lt;&#x2F;code&gt; (the daemon) and &lt;code&gt;tes&lt;&#x2F;code&gt; (the CLI) in release mode with
+&lt;code&gt;cargo build --release&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Generates shell completions&lt;&#x2F;strong&gt; for bash, zsh, and fish from the &lt;code&gt;tes&lt;&#x2F;code&gt; binary&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Packages&lt;&#x2F;strong&gt; everything into a &lt;code&gt;.deb&lt;&#x2F;code&gt; file with
+&lt;code&gt;cargo deb -p tesseras-daemon --no-build&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;The result is a &lt;code&gt;.deb&lt;&#x2F;code&gt; file in &lt;code&gt;target&#x2F;debian&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;installing&quot;&gt;Installing&lt;&#x2F;h2&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo dpkg -i target&#x2F;debian&#x2F;tesseras-daemon_*.deb
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;If there are missing dependencies, fix them with:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo apt install -f
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;post-install-setup&quot;&gt;Post-install setup&lt;&#x2F;h2&gt;
+&lt;p&gt;The &lt;code&gt;postinst&lt;&#x2F;code&gt; script automatically creates a &lt;code&gt;tesseras&lt;&#x2F;code&gt; system user and the
+data directory &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tesseras&lt;&#x2F;code&gt;. To use the CLI without sudo, add yourself to
+the group:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo usermod -aG tesseras $USER
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;Log out and back in, then start the daemon:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo systemctl enable --now tesd
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;what-the-package-includes&quot;&gt;What the package includes&lt;&#x2F;h2&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Path&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;tesd&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Full node daemon&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;tes&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;CLI client&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;etc&#x2F;tesseras&#x2F;config.toml&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Default configuration (marked as conffile)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;tesd.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Systemd unit with security hardening&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Shell completions&lt;&#x2F;td&gt;&lt;td&gt;bash, zsh, and fish&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;h2 id=&quot;how-cargo-deb-works&quot;&gt;How cargo-deb works&lt;&#x2F;h2&gt;
+&lt;p&gt;The packaging metadata lives in &lt;code&gt;crates&#x2F;tesseras-daemon&#x2F;Cargo.toml&lt;&#x2F;code&gt; under
+&lt;code&gt;[package.metadata.deb]&lt;&#x2F;code&gt;. This section defines:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;depends&lt;&#x2F;strong&gt; — runtime dependencies: &lt;code&gt;libc6&lt;&#x2F;code&gt; and &lt;code&gt;libsqlite3-0&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;assets&lt;&#x2F;strong&gt; — files to include in the package (binaries, config, systemd unit,
+shell completions)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;conf-files&lt;&#x2F;strong&gt; — files treated as configuration (preserved on upgrade)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;maintainer-scripts&lt;&#x2F;strong&gt; — &lt;code&gt;postinst&lt;&#x2F;code&gt; and &lt;code&gt;postrm&lt;&#x2F;code&gt; scripts in
+&lt;code&gt;packaging&#x2F;debian&#x2F;scripts&#x2F;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;systemd-units&lt;&#x2F;strong&gt; — automatic systemd integration&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;The &lt;code&gt;postinst&lt;&#x2F;code&gt; script creates the &lt;code&gt;tesseras&lt;&#x2F;code&gt; system user and data directory on
+install. The &lt;code&gt;postrm&lt;&#x2F;code&gt; script cleans up the user, group, and data directory only
+on &lt;code&gt;purge&lt;&#x2F;code&gt; (not on simple removal).&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;systemd-hardening&quot;&gt;Systemd hardening&lt;&#x2F;h2&gt;
+&lt;p&gt;The &lt;code&gt;tesd.service&lt;&#x2F;code&gt; unit includes security hardening directives:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;ini&quot;&gt;NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=&#x2F;var&#x2F;lib&#x2F;tesseras
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+ProtectControlGroups=true
+RestrictSUIDSGID=true
+MemoryDenyWriteExecute=true
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;The daemon runs as the unprivileged &lt;code&gt;tesseras&lt;&#x2F;code&gt; user and can only write to
+&lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tesseras&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;deploying-to-a-remote-server&quot;&gt;Deploying to a remote server&lt;&#x2F;h2&gt;
+&lt;p&gt;The justfile includes a &lt;code&gt;deploy&lt;&#x2F;code&gt; recipe for pushing the &lt;code&gt;.deb&lt;&#x2F;code&gt; to a remote host:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;just deploy bootstrap1.tesseras.net
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;This builds the &lt;code&gt;.deb&lt;&#x2F;code&gt;, copies it via &lt;code&gt;scp&lt;&#x2F;code&gt;, installs it with &lt;code&gt;dpkg -i&lt;&#x2F;code&gt;, and
+restarts the &lt;code&gt;tesd&lt;&#x2F;code&gt; service.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;updating&quot;&gt;Updating&lt;&#x2F;h2&gt;
+&lt;p&gt;After pulling new changes, simply run &lt;code&gt;just deb&lt;&#x2F;code&gt; again and reinstall:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;git pull
+just deb
+sudo dpkg -i target&#x2F;debian&#x2F;tesseras-daemon_*.deb
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Packaging Tesseras for Arch Linux</title>
+ <published>2026-02-16T09:00:00+00:00</published>
+ <updated>2026-02-16T09:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/packaging-archlinux/"/>
+ <id>https://tesseras.net/news/packaging-archlinux/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/packaging-archlinux/">&lt;p&gt;Tesseras now ships a PKGBUILD for Arch Linux. This post walks through building
+and installing the package from source.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;&#x2F;h2&gt;
+&lt;p&gt;You need a working Rust toolchain and the base-devel group:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo pacman -S --needed base-devel sqlite
+rustup toolchain install stable
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;building&quot;&gt;Building&lt;&#x2F;h2&gt;
+&lt;p&gt;Clone the repository and run the &lt;code&gt;just arch&lt;&#x2F;code&gt; recipe:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;git clone https:&#x2F;&#x2F;git.sr.ht&#x2F;~ijanc&#x2F;tesseras
+cd tesseras
+just arch
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;This runs &lt;code&gt;makepkg -sf&lt;&#x2F;code&gt; inside &lt;code&gt;packaging&#x2F;archlinux&#x2F;&lt;&#x2F;code&gt;, which:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;&lt;strong&gt;prepare&lt;&#x2F;strong&gt; — fetches Cargo dependencies with &lt;code&gt;cargo fetch --locked&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;build&lt;&#x2F;strong&gt; — compiles &lt;code&gt;tesd&lt;&#x2F;code&gt; and &lt;code&gt;tes&lt;&#x2F;code&gt; (the CLI) in release mode&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;package&lt;&#x2F;strong&gt; — installs binaries, systemd service, sysusers&#x2F;tmpfiles configs,
+shell completions (bash, zsh, fish), and a default config file&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;The result is a &lt;code&gt;.pkg.tar.zst&lt;&#x2F;code&gt; file in &lt;code&gt;packaging&#x2F;archlinux&#x2F;&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;installing&quot;&gt;Installing&lt;&#x2F;h2&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo pacman -U packaging&#x2F;archlinux&#x2F;tesseras-*.pkg.tar.zst
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;post-install-setup&quot;&gt;Post-install setup&lt;&#x2F;h2&gt;
+&lt;p&gt;The package creates a &lt;code&gt;tesseras&lt;&#x2F;code&gt; system user and group automatically via
+systemd-sysusers. To use the CLI without sudo, add yourself to the group:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo usermod -aG tesseras $USER
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;Log out and back in, then start the daemon:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;sudo systemctl enable --now tesd
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;what-the-package-includes&quot;&gt;What the package includes&lt;&#x2F;h2&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Path&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;tesd&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Full node daemon&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;bin&#x2F;tes&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;CLI client&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;etc&#x2F;tesseras&#x2F;config.toml&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Default configuration (marked as backup)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;tesd.service&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Systemd unit with security hardening&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;sysusers.d&#x2F;tesseras.conf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;System user definition&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;&#x2F;usr&#x2F;lib&#x2F;tmpfiles.d&#x2F;tesseras.conf&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Data directory &lt;code&gt;&#x2F;var&#x2F;lib&#x2F;tesseras&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Shell completions&lt;&#x2F;td&gt;&lt;td&gt;bash, zsh, and fish&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;h2 id=&quot;pkgbuild-details&quot;&gt;PKGBUILD details&lt;&#x2F;h2&gt;
+&lt;p&gt;The PKGBUILD builds directly from the local git checkout rather than downloading
+a source tarball. The &lt;code&gt;TESSERAS_ROOT&lt;&#x2F;code&gt; environment variable points makepkg to the
+workspace root. Cargo&#x27;s target directory is set to &lt;code&gt;$srcdir&#x2F;target&lt;&#x2F;code&gt; to keep
+build artifacts inside the makepkg sandbox.&lt;&#x2F;p&gt;
+&lt;p&gt;The package depends only on &lt;code&gt;sqlite&lt;&#x2F;code&gt; at runtime and &lt;code&gt;cargo&lt;&#x2F;code&gt; at build time.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;updating&quot;&gt;Updating&lt;&#x2F;h2&gt;
+&lt;p&gt;After pulling new changes, simply run &lt;code&gt;just arch&lt;&#x2F;code&gt; again and reinstall:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;git pull
+just arch
+sudo pacman -U packaging&#x2F;archlinux&#x2F;tesseras-*.pkg.tar.zst
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Storage Deduplication</title>
+ <published>2026-02-15T23:00:00+00:00</published>
+ <updated>2026-02-15T23:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-storage-deduplication/"/>
+ <id>https://tesseras.net/news/phase4-storage-deduplication/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-storage-deduplication/">&lt;p&gt;When multiple tesseras share the same photo, the same audio clip, or the same
+fragment data, the old storage layer kept separate copies of each. On a node
+storing thousands of tesseras for the network, this duplication adds up fast.
+Phase 4 continues with storage deduplication: a content-addressable store (CAS)
+that ensures every unique piece of data is stored exactly once on disk,
+regardless of how many tesseras reference it.&lt;&#x2F;p&gt;
+&lt;p&gt;The design is simple and proven: hash the content with BLAKE3, use the hash as
+the filename, and maintain a reference count in SQLite. When two tesseras
+include the same 5 MB photo, one file exists on disk with a refcount of 2. When
+one tessera is deleted, the refcount drops to 1 and the file stays. When the
+last reference is released, a periodic sweep cleans up the orphan.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;CAS schema migration&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;migrations&#x2F;004_dedup.sql&lt;&#x2F;code&gt;) — Three
+new tables:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;cas_objects&lt;&#x2F;code&gt; — tracks every object in the store: BLAKE3 hash (primary key),
+byte size, reference count, and creation timestamp&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;blob_refs&lt;&#x2F;code&gt; — maps logical blob identifiers (tessera hash + memory hash +
+filename) to CAS hashes, replacing the old filesystem path convention&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;fragment_refs&lt;&#x2F;code&gt; — maps logical fragment identifiers (tessera hash + fragment
+index) to CAS hashes, replacing the old &lt;code&gt;fragments&#x2F;&lt;&#x2F;code&gt; directory layout&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Indexes on the hash columns ensure O(1) lookups during reads and reference
+counting.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CasStore&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;cas.rs&lt;&#x2F;code&gt;) — The core content-addressable
+storage engine. Files are stored under a two-level prefix directory:
+&lt;code&gt;&amp;lt;root&amp;gt;&#x2F;&amp;lt;2-char-hex-prefix&amp;gt;&#x2F;&amp;lt;full-hash&amp;gt;.blob&lt;&#x2F;code&gt;. The store provides five
+operations:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;put(hash, data)&lt;&#x2F;code&gt; — writes data to disk if not already present, increments
+refcount. Returns whether a dedup hit occurred.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;get(hash)&lt;&#x2F;code&gt; — reads data from disk by hash&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;release(hash)&lt;&#x2F;code&gt; — decrements refcount. If it reaches zero, the on-disk file is
+deleted immediately.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;contains(hash)&lt;&#x2F;code&gt; — checks existence without reading&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;ref_count(hash)&lt;&#x2F;code&gt; — returns the current reference count&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;All operations are atomic within a single SQLite transaction. The refcount is
+the source of truth — if the refcount says the object exists, the file must be
+on disk.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CAS-backed FsBlobStore&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;blob.rs&lt;&#x2F;code&gt;) — Rewritten to
+delegate all storage to the CAS. When a blob is written, its BLAKE3 hash is
+computed and passed to &lt;code&gt;cas.put()&lt;&#x2F;code&gt;. A row in &lt;code&gt;blob_refs&lt;&#x2F;code&gt; maps the logical path
+(tessera + memory + filename) to the CAS hash. Reads look up the CAS hash via
+&lt;code&gt;blob_refs&lt;&#x2F;code&gt; and fetch from &lt;code&gt;cas.get()&lt;&#x2F;code&gt;. Deleting a tessera releases all its blob
+references in a single transaction.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CAS-backed FsFragmentStore&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;fragment.rs&lt;&#x2F;code&gt;) — Same
+pattern for erasure-coded fragments. Each fragment&#x27;s BLAKE3 checksum is already
+computed during Reed-Solomon encoding, so it&#x27;s used directly as the CAS key.
+Fragment verification now checks the CAS hash instead of recomputing from
+scratch — if the CAS says the data is intact, it is.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Sweep garbage collector&lt;&#x2F;strong&gt; (&lt;code&gt;cas.rs:sweep()&lt;&#x2F;code&gt;) — A periodic GC pass that handles
+three edge cases the normal refcount path can&#x27;t:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;&lt;strong&gt;Orphan files&lt;&#x2F;strong&gt; — files on disk with no corresponding row in &lt;code&gt;cas_objects&lt;&#x2F;code&gt;.
+Can happen after a crash mid-write. Files younger than 1 hour are skipped
+(grace period for in-flight writes); older orphans are deleted.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Leaked refcounts&lt;&#x2F;strong&gt; — rows in &lt;code&gt;cas_objects&lt;&#x2F;code&gt; with refcount zero that weren&#x27;t
+cleaned up (e.g., if the process died between decrementing and deleting).
+These rows are removed.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Idempotent&lt;&#x2F;strong&gt; — running sweep twice produces the same result.&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;The sweep is wired into the existing repair loop in &lt;code&gt;tesseras-replication&lt;&#x2F;code&gt;, so
+it runs automatically every 24 hours alongside fragment health checks.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Migration from old layout&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;migration.rs&lt;&#x2F;code&gt;) — A
+copy-first migration strategy that moves data from the old directory-based
+layout (&lt;code&gt;blobs&#x2F;&amp;lt;tessera&amp;gt;&#x2F;&amp;lt;memory&amp;gt;&#x2F;&amp;lt;file&amp;gt;&lt;&#x2F;code&gt; and
+&lt;code&gt;fragments&#x2F;&amp;lt;tessera&amp;gt;&#x2F;&amp;lt;index&amp;gt;.shard&lt;&#x2F;code&gt;) into the CAS. The migration:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;Checks the storage version in &lt;code&gt;storage_meta&lt;&#x2F;code&gt; (version 1 = old layout, version
+2 = CAS)&lt;&#x2F;li&gt;
+&lt;li&gt;Walks the old &lt;code&gt;blobs&#x2F;&lt;&#x2F;code&gt; and &lt;code&gt;fragments&#x2F;&lt;&#x2F;code&gt; directories&lt;&#x2F;li&gt;
+&lt;li&gt;Computes BLAKE3 hashes and inserts into CAS via &lt;code&gt;put()&lt;&#x2F;code&gt; — duplicates are
+automatically deduplicated&lt;&#x2F;li&gt;
+&lt;li&gt;Creates corresponding &lt;code&gt;blob_refs&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;fragment_refs&lt;&#x2F;code&gt; entries&lt;&#x2F;li&gt;
+&lt;li&gt;Removes old directories only after all data is safely in CAS&lt;&#x2F;li&gt;
+&lt;li&gt;Updates the storage version to 2&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;The migration runs on daemon startup, is idempotent (safe to re-run), and
+reports statistics: files migrated, duplicates found, bytes saved.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Prometheus metrics&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;metrics.rs&lt;&#x2F;code&gt;) — Ten new metrics for
+observability:&lt;&#x2F;p&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Metric&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_objects_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Total unique objects in the CAS&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_bytes_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Total bytes stored&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_dedup_hits_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Number of writes that found an existing object&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_bytes_saved_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bytes saved by deduplication&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_gc_refcount_deletions_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Objects deleted when refcount reached zero&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_gc_sweep_orphans_cleaned_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Orphan files removed by sweep&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_gc_sweep_leaked_refs_cleaned_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Leaked refcount rows cleaned&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_gc_sweep_skipped_young_total&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Young orphans skipped (grace period)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;cas_gc_sweep_duration_seconds&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Time spent in sweep GC&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;p&gt;&lt;strong&gt;Property-based tests&lt;&#x2F;strong&gt; — Two proptest tests verify CAS invariants under random
+inputs:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;refcount_matches_actual_refs&lt;&#x2F;code&gt; — after N random put&#x2F;release operations, the
+refcount always matches the actual number of outstanding references&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;cas_path_is_deterministic&lt;&#x2F;code&gt; — the same hash always produces the same
+filesystem path&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;Integration test updates&lt;&#x2F;strong&gt; — All integration tests across &lt;code&gt;tesseras-core&lt;&#x2F;code&gt;,
+&lt;code&gt;tesseras-replication&lt;&#x2F;code&gt;, &lt;code&gt;tesseras-embedded&lt;&#x2F;code&gt;, and &lt;code&gt;tesseras-cli&lt;&#x2F;code&gt; updated for the
+new CAS-backed constructors. Tamper-detection tests updated to work with the CAS
+directory layout.&lt;&#x2F;p&gt;
+&lt;p&gt;347 tests pass across the workspace. Clippy clean with &lt;code&gt;-D warnings&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;BLAKE3 as CAS key&lt;&#x2F;strong&gt;: the content hash we already compute for integrity
+verification doubles as the deduplication key. No additional hashing step —
+the hash computed during &lt;code&gt;create&lt;&#x2F;code&gt; or &lt;code&gt;replicate&lt;&#x2F;code&gt; is reused as the CAS address.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;SQLite refcount over filesystem reflinks&lt;&#x2F;strong&gt;: we considered using
+filesystem-level copy-on-write (reflinks on btrfs&#x2F;XFS), but that would tie
+Tesseras to specific filesystems. SQLite refcounting works on any filesystem,
+including FAT32 on cheap USB drives and ext4 on Raspberry Pis.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Two-level hex prefix directories&lt;&#x2F;strong&gt;: storing all CAS objects in a flat
+directory would slow down filesystems with millions of entries. The
+&lt;code&gt;&amp;lt;2-char prefix&amp;gt;&#x2F;&lt;&#x2F;code&gt; split limits any single directory to ~65k entries before a
+second prefix level is needed. This matches the approach used by Git&#x27;s object
+store.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Grace period for orphan files&lt;&#x2F;strong&gt;: the sweep GC skips files younger than 1
+hour to avoid deleting objects that are being written by a concurrent
+operation. This is a pragmatic choice — it trades a small window of potential
+orphans for crash safety without requiring fsync or two-phase commit.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Copy-first migration&lt;&#x2F;strong&gt;: the migration copies data to CAS before removing old
+directories. If the process is interrupted, the old data is still intact and
+migration can be re-run. This is slower than moving files but guarantees no
+data loss.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Sweep in repair loop&lt;&#x2F;strong&gt;: rather than adding a separate GC timer, the CAS
+sweep piggybacks on the existing 24-hour repair loop. This keeps the daemon
+simple — one background maintenance cycle handles both fragment health and
+storage cleanup.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued&lt;&#x2F;strong&gt; — security audits, OS packaging (Alpine, Arch, Debian,
+OpenBSD, FreeBSD)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration
+(FamilySearch, Ancestry), physical media export (M-DISC, microfilm, acid-free
+paper with QR), AI-assisted context&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Storage deduplication completes the storage efficiency story for Tesseras. A
+node that stores fragments for thousands of users — common for institutional
+nodes and always-on full nodes — now pays the disk cost of unique data only.
+Combined with Reed-Solomon erasure coding (which already minimizes redundancy at
+the network level), the system achieves efficient storage at both the local and
+distributed layers.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Institutional Node Onboarding</title>
+ <published>2026-02-15T22:00:00+00:00</published>
+ <updated>2026-02-15T22:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-institutional-onboarding/"/>
+ <id>https://tesseras.net/news/phase4-institutional-onboarding/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-institutional-onboarding/">&lt;p&gt;A P2P network of individuals is fragile. Hard drives die, phones get lost,
+people lose interest. The long-term survival of humanity&#x27;s memories depends on
+institutions — libraries, archives, museums, universities — that measure their
+lifetimes in centuries. Phase 4 continues with institutional node onboarding:
+verified organizations can now pledge storage, run searchable indexes, and
+participate in the network with a distinct identity.&lt;&#x2F;p&gt;
+&lt;p&gt;The design follows a principle of trust but verify: institutions identify
+themselves via DNS TXT records (the same mechanism used by SPF, DKIM, and DMARC
+for email), pledge a storage budget, and receive reciprocity exemptions so they
+can store fragments for others without expecting anything in return. In
+exchange, the network treats their fragments as higher-quality replicas and
+limits over-reliance on any single institution through diversity constraints.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;Capability bits&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-core&#x2F;src&#x2F;network.rs&lt;&#x2F;code&gt;) — Two new flags added to
+the &lt;code&gt;Capabilities&lt;&#x2F;code&gt; bitfield: &lt;code&gt;INSTITUTIONAL&lt;&#x2F;code&gt; (bit 7) and &lt;code&gt;SEARCH_INDEX&lt;&#x2F;code&gt; (bit 8).
+A new &lt;code&gt;institutional_default()&lt;&#x2F;code&gt; constructor returns the full Phase 2 capability
+set plus these two bits and &lt;code&gt;RELAY&lt;&#x2F;code&gt;. Normal nodes advertise &lt;code&gt;phase2_default()&lt;&#x2F;code&gt;
+which lacks institutional flags. Serialization roundtrip tests verify the new
+bits survive MessagePack encoding.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Search types&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-core&#x2F;src&#x2F;search.rs&lt;&#x2F;code&gt;) — Three new domain types for
+the search subsystem:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;SearchFilters&lt;&#x2F;code&gt; — query parameters: &lt;code&gt;memory_type&lt;&#x2F;code&gt;, &lt;code&gt;visibility&lt;&#x2F;code&gt;, &lt;code&gt;language&lt;&#x2F;code&gt;,
+&lt;code&gt;date_range&lt;&#x2F;code&gt;, &lt;code&gt;geo&lt;&#x2F;code&gt; (bounding box), &lt;code&gt;page&lt;&#x2F;code&gt;, &lt;code&gt;page_size&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;SearchHit&lt;&#x2F;code&gt; — a single result: content hash plus a &lt;code&gt;MetadataExcerpt&lt;&#x2F;code&gt; (title,
+description, memory type, creation date, visibility, language, tags)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;GeoFilter&lt;&#x2F;code&gt; — bounding box with &lt;code&gt;min_lat&lt;&#x2F;code&gt;, &lt;code&gt;max_lat&lt;&#x2F;code&gt;, &lt;code&gt;min_lon&lt;&#x2F;code&gt;, &lt;code&gt;max_lon&lt;&#x2F;code&gt; for
+spatial queries&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;All types derive &lt;code&gt;Serialize&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Deserialize&lt;&#x2F;code&gt; for wire transport and
+&lt;code&gt;Clone&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Debug&lt;&#x2F;code&gt; for diagnostics.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Institutional daemon config&lt;&#x2F;strong&gt; (&lt;code&gt;tesd&#x2F;src&#x2F;config.rs&lt;&#x2F;code&gt;) — A new &lt;code&gt;[institutional]&lt;&#x2F;code&gt;
+TOML section with &lt;code&gt;domain&lt;&#x2F;code&gt; (the DNS domain to verify), &lt;code&gt;pledge_bytes&lt;&#x2F;code&gt; (storage
+commitment in bytes), and &lt;code&gt;search_enabled&lt;&#x2F;code&gt; (toggle for the FTS5 index). The
+&lt;code&gt;to_dht_config()&lt;&#x2F;code&gt; method now sets &lt;code&gt;Capabilities::institutional_default()&lt;&#x2F;code&gt; when
+institutional config is present, so institutional nodes advertise the right
+capability bits in Pong responses.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;DNS TXT verification&lt;&#x2F;strong&gt; (&lt;code&gt;tesd&#x2F;src&#x2F;institutional.rs&lt;&#x2F;code&gt;) — Async DNS resolution
+using &lt;code&gt;hickory-resolver&lt;&#x2F;code&gt; to verify institutional identity. The daemon looks up
+&lt;code&gt;_tesseras.&amp;lt;domain&amp;gt;&lt;&#x2F;code&gt; TXT records and parses key-value fields: &lt;code&gt;v&lt;&#x2F;code&gt; (version),
+&lt;code&gt;node&lt;&#x2F;code&gt; (hex-encoded node ID), and &lt;code&gt;pledge&lt;&#x2F;code&gt; (storage pledge in bytes).
+Verification checks:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;A TXT record exists at &lt;code&gt;_tesseras.&amp;lt;domain&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;The &lt;code&gt;node&lt;&#x2F;code&gt; field matches the daemon&#x27;s own node ID&lt;&#x2F;li&gt;
+&lt;li&gt;The &lt;code&gt;pledge&lt;&#x2F;code&gt; field is present and valid&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;On startup, the daemon attempts DNS verification. If it succeeds, the node runs
+with institutional capabilities. If it fails, the node logs a warning and
+downgrades to a normal full node — no crash, no manual intervention.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CLI setup command&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-cli&#x2F;src&#x2F;institutional.rs&lt;&#x2F;code&gt;) — A new
+&lt;code&gt;institutional setup&lt;&#x2F;code&gt; subcommand that guides operators through onboarding:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;Reads the node&#x27;s identity from the data directory&lt;&#x2F;li&gt;
+&lt;li&gt;Prompts for domain name and pledge size&lt;&#x2F;li&gt;
+&lt;li&gt;Generates the exact DNS TXT record to add:
+&lt;code&gt;v=tesseras1 node=&amp;lt;hex&amp;gt; pledge=&amp;lt;bytes&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
+&lt;li&gt;Writes the institutional section to the daemon&#x27;s config file&lt;&#x2F;li&gt;
+&lt;li&gt;Prints next steps: add the TXT record, restart the daemon&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;&lt;strong&gt;SQLite search index&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&lt;&#x2F;code&gt;) — A migration
+(&lt;code&gt;003_institutional.sql&lt;&#x2F;code&gt;) that creates three structures:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;search_content&lt;&#x2F;code&gt; — an FTS5 virtual table for full-text search over tessera
+metadata (title, description, creator, tags, language)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;geo_index&lt;&#x2F;code&gt; — an R-tree virtual table for spatial bounding-box queries over
+latitude&#x2F;longitude&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;geo_map&lt;&#x2F;code&gt; — a mapping table linking R-tree row IDs to content hashes&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;The &lt;code&gt;SqliteSearchIndex&lt;&#x2F;code&gt; adapter implements the &lt;code&gt;SearchIndex&lt;&#x2F;code&gt; port trait with
+&lt;code&gt;index_tessera()&lt;&#x2F;code&gt; (insert&#x2F;update) and &lt;code&gt;search()&lt;&#x2F;code&gt; (query with filters). FTS5
+queries support natural language search; geo queries use R-tree &lt;code&gt;INTERSECT&lt;&#x2F;code&gt; for
+bounding box lookups. Results are ranked by FTS5 relevance score.&lt;&#x2F;p&gt;
+&lt;p&gt;The migration also adds an &lt;code&gt;is_institutional&lt;&#x2F;code&gt; column to the &lt;code&gt;reciprocity&lt;&#x2F;code&gt; table,
+handled idempotently via &lt;code&gt;pragma_table_info&lt;&#x2F;code&gt; checks (SQLite&#x27;s
+&lt;code&gt;ALTER TABLE ADD COLUMN&lt;&#x2F;code&gt; lacks &lt;code&gt;IF NOT EXISTS&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Reciprocity bypass&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-replication&#x2F;src&#x2F;service.rs&lt;&#x2F;code&gt;) — Institutional
+nodes are exempt from reciprocity checks. When &lt;code&gt;receive_fragment()&lt;&#x2F;code&gt; is called,
+if the sender&#x27;s node ID is marked as institutional in the reciprocity ledger,
+the balance check is skipped entirely. This means institutions can store
+fragments for the entire network without needing to &quot;earn&quot; credits first — their
+DNS-verified identity and storage pledge serve as their credential.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Node-type diversity constraint&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-replication&#x2F;src&#x2F;distributor.rs&lt;&#x2F;code&gt;) —
+A new &lt;code&gt;apply_institutional_diversity()&lt;&#x2F;code&gt; function limits how many replicas of a
+single tessera can land on institutional nodes. The cap is
+&lt;code&gt;ceil(replication_factor &#x2F; 3.5)&lt;&#x2F;code&gt; — with the default &lt;code&gt;r=7&lt;&#x2F;code&gt;, at most 2 of 7
+replicas go to institutions. This prevents the network from becoming dependent
+on a small number of large institutions: if a university&#x27;s servers go down, at
+least 5 replicas remain on independent nodes.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;DHT message extensions&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-dht&#x2F;src&#x2F;message.rs&lt;&#x2F;code&gt;) — Two new message
+variants:&lt;&#x2F;p&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Message&lt;&#x2F;th&gt;&lt;th&gt;Purpose&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;Search&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Client sends query string, filters, and page number&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;SearchResult&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Institutional node responds with hits and total count&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;p&gt;The &lt;code&gt;encode()&lt;&#x2F;code&gt; function was switched from positional to named MessagePack
+serialization (&lt;code&gt;rmp_serde::to_vec_named&lt;&#x2F;code&gt;) to handle &lt;code&gt;SearchFilters&lt;&#x2F;code&gt;&#x27; optional
+fields correctly — positional encoding breaks when &lt;code&gt;skip_serializing_if&lt;&#x2F;code&gt; omits
+fields.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Prometheus metrics&lt;&#x2F;strong&gt; (&lt;code&gt;tesd&#x2F;src&#x2F;metrics.rs&lt;&#x2F;code&gt;) — Eight institutional-specific
+metrics:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_pledge_bytes&lt;&#x2F;code&gt; — configured storage pledge&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_stored_bytes&lt;&#x2F;code&gt; — actual bytes stored&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_pledge_utilization_ratio&lt;&#x2F;code&gt; — stored&#x2F;pledged ratio&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_peers_served&lt;&#x2F;code&gt; — unique peers served fragments&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_search_index_total&lt;&#x2F;code&gt; — tesseras in the search index&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_search_queries_total&lt;&#x2F;code&gt; — search queries received&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_dns_verification_status&lt;&#x2F;code&gt; — 1 if DNS verified, 0
+otherwise&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tesseras_institutional_dns_verification_last&lt;&#x2F;code&gt; — Unix timestamp of last
+verification&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;Integration tests&lt;&#x2F;strong&gt; — Two tests in
+&lt;code&gt;tesseras-replication&#x2F;tests&#x2F;integration.rs&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;institutional_peer_bypasses_reciprocity&lt;&#x2F;code&gt; — verifies that an institutional
+peer with a massive deficit (-999,999 balance) is still allowed to store
+fragments, while a non-institutional peer with the same deficit is rejected&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;institutional_node_accepts_fragment_despite_deficit&lt;&#x2F;code&gt; — full async test using
+&lt;code&gt;ReplicationService&lt;&#x2F;code&gt; with mocked DHT, fragment store, reciprocity ledger, and
+blob store: sends a fragment from an institutional sender and verifies it&#x27;s
+accepted&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;322 tests pass across the workspace. Clippy clean with &lt;code&gt;-D warnings&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;DNS TXT over PKI or blockchain&lt;&#x2F;strong&gt;: DNS is universally deployed, universally
+understood, and already used for domain verification (SPF, DKIM, Let&#x27;s
+Encrypt). Institutions already manage DNS. No certificate authority, no token,
+no on-chain transaction — just a TXT record. If an institution loses control
+of their domain, the verification naturally fails on the next check.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Graceful degradation on DNS failure&lt;&#x2F;strong&gt;: if DNS verification fails at startup,
+the daemon downgrades to a normal full node instead of refusing to start. This
+prevents operational incidents — a DNS misconfiguration shouldn&#x27;t take a node
+offline.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Diversity cap at &lt;code&gt;ceil(r &#x2F; 3.5)&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: with &lt;code&gt;r=7&lt;&#x2F;code&gt;, at most 2 replicas go to
+institutions. This is conservative — it ensures the network never depends on
+institutions for majority quorum, while still benefiting from their storage
+capacity and uptime.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Named MessagePack encoding&lt;&#x2F;strong&gt;: switching from positional to named encoding
+adds ~15% overhead per message but eliminates a class of serialization bugs
+when optional fields are present. The DHT is not bandwidth-constrained at the
+message level, so the tradeoff is worth it.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Reciprocity exemption over credit grants&lt;&#x2F;strong&gt;: rather than giving institutions
+a large initial credit balance (which is arbitrary and needs tuning), we
+exempt them entirely. Their DNS-verified identity and public storage pledge
+replace the bilateral reciprocity mechanism.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;FTS5 + R-tree in SQLite&lt;&#x2F;strong&gt;: full-text search and spatial indexing are built
+into SQLite as loadable extensions. No external search engine (Elasticsearch,
+Meilisearch) needed. This keeps the deployment a single binary with a single
+database file — critical for institutional operators who may not have a DevOps
+team.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued&lt;&#x2F;strong&gt; — storage deduplication (content-addressable store with
+BLAKE3 keying), security audits, OS packaging (Alpine, Arch, Debian, OpenBSD,
+FreeBSD)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration
+(FamilySearch, Ancestry), physical media export (M-DISC, microfilm, acid-free
+paper with QR), AI-assisted context&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Institutional onboarding closes a critical gap in Tesseras&#x27; preservation model.
+Individual nodes provide grassroots resilience — thousands of devices across the
+globe, each storing a few fragments. Institutional nodes provide anchoring —
+organizations with professional infrastructure, redundant storage, and
+multi-decade operational horizons. Together, they form a network where memories
+can outlast both individual devices and individual institutions.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Performance Tuning</title>
+ <published>2026-02-15T20:00:00+00:00</published>
+ <updated>2026-02-15T20:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-performance-tuning/"/>
+ <id>https://tesseras.net/news/phase4-performance-tuning/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-performance-tuning/">&lt;p&gt;A P2P network that can traverse NATs but chokes on its own I&#x2F;O is not much use.
+Phase 4 continues with performance tuning: centralizing database configuration,
+caching fragment blobs in memory, managing QUIC connection lifecycles, and
+eliminating unnecessary disk reads from the attestation hot path.&lt;&#x2F;p&gt;
+&lt;p&gt;The guiding principle was the same as the rest of Tesseras: do the simplest
+thing that actually works. No custom allocators, no lock-free data structures,
+no premature complexity. A centralized &lt;code&gt;StorageConfig&lt;&#x2F;code&gt;, an LRU cache, a
+connection reaper, and a targeted fix to avoid re-reading blobs that were
+already checksummed.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;Centralized SQLite configuration&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;database.rs&lt;&#x2F;code&gt;) — A
+new &lt;code&gt;StorageConfig&lt;&#x2F;code&gt; struct and &lt;code&gt;open_database()&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;open_in_memory()&lt;&#x2F;code&gt; functions
+that apply all SQLite pragmas in one place: WAL journal mode, foreign keys,
+synchronous mode (NORMAL by default, FULL for unstable hardware like RPi + SD
+card), busy timeout, page cache size, and WAL autocheckpoint interval.
+Previously, each call site opened a connection and applied pragmas ad hoc. Now
+the daemon, CLI, and tests all go through the same path. 7 tests covering
+foreign keys, busy timeout, journal mode, migrations, synchronous modes, and
+on-disk WAL file creation.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;LRU fragment cache&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;cache.rs&lt;&#x2F;code&gt;) — A
+&lt;code&gt;CachedFragmentStore&lt;&#x2F;code&gt; that wraps any &lt;code&gt;FragmentStore&lt;&#x2F;code&gt; with a byte-aware LRU
+cache. Fragment blobs are cached on read and invalidated on write or delete.
+When the cache exceeds its configured byte limit, the least recently used
+entries are evicted. The cache is transparent: it implements &lt;code&gt;FragmentStore&lt;&#x2F;code&gt;
+itself, so the rest of the stack doesn&#x27;t know it&#x27;s there. Optional Prometheus
+metrics track hits, misses, and current byte usage. 3 tests: cache hit avoids
+inner read, store invalidates cache, eviction when over max bytes.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Prometheus storage metrics&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-storage&#x2F;src&#x2F;metrics.rs&lt;&#x2F;code&gt;) — A
+&lt;code&gt;StorageMetrics&lt;&#x2F;code&gt; struct with three counters&#x2F;gauges: &lt;code&gt;fragment_cache_hits&lt;&#x2F;code&gt;,
+&lt;code&gt;fragment_cache_misses&lt;&#x2F;code&gt;, and &lt;code&gt;fragment_cache_bytes&lt;&#x2F;code&gt;. Registered with the
+Prometheus registry and wired into the fragment cache via &lt;code&gt;with_metrics()&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Attestation hot path fix&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-replication&#x2F;src&#x2F;service.rs&lt;&#x2F;code&gt;) — The
+attestation flow previously read every fragment blob from disk and recomputed
+its BLAKE3 checksum. Since &lt;code&gt;list_fragments()&lt;&#x2F;code&gt; already returns &lt;code&gt;FragmentId&lt;&#x2F;code&gt; with
+a stored checksum, the fix is trivial: use &lt;code&gt;frag.checksum&lt;&#x2F;code&gt; instead of
+&lt;code&gt;blake3::hash(&amp;amp;data)&lt;&#x2F;code&gt;. This eliminates one disk read per fragment during
+attestation — for a tessera with 100 fragments, that&#x27;s 100 fewer reads. A test
+with &lt;code&gt;expect_read_fragment().never()&lt;&#x2F;code&gt; verifies no blob reads happen during
+attestation.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;QUIC connection pool lifecycle&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-net&#x2F;src&#x2F;quinn_transport.rs&lt;&#x2F;code&gt;) — A
+&lt;code&gt;PoolConfig&lt;&#x2F;code&gt; struct controlling max connections, idle timeout, and reaper
+interval. &lt;code&gt;PooledConnection&lt;&#x2F;code&gt; wraps each &lt;code&gt;quinn::Connection&lt;&#x2F;code&gt; with a &lt;code&gt;last_used&lt;&#x2F;code&gt;
+timestamp. When the pool reaches capacity, the oldest idle connection is evicted
+before opening a new one. A background reaper task (Tokio spawn) periodically
+closes connections that have been idle beyond the timeout. 4 new pool metrics:
+&lt;code&gt;tesseras_conn_pool_size&lt;&#x2F;code&gt;, &lt;code&gt;pool_hits_total&lt;&#x2F;code&gt;, &lt;code&gt;pool_misses_total&lt;&#x2F;code&gt;,
+&lt;code&gt;pool_evictions_total&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Daemon integration&lt;&#x2F;strong&gt; (&lt;code&gt;tesd&#x2F;src&#x2F;config.rs&lt;&#x2F;code&gt;, &lt;code&gt;main.rs&lt;&#x2F;code&gt;) — A new &lt;code&gt;[performance]&lt;&#x2F;code&gt;
+section in the TOML config with fields for SQLite cache size, synchronous mode,
+busy timeout, fragment cache size, max connections, idle timeout, and reaper
+interval. The daemon&#x27;s &lt;code&gt;main()&lt;&#x2F;code&gt; now calls &lt;code&gt;open_database()&lt;&#x2F;code&gt; with the configured
+&lt;code&gt;StorageConfig&lt;&#x2F;code&gt;, wraps &lt;code&gt;FsFragmentStore&lt;&#x2F;code&gt; with &lt;code&gt;CachedFragmentStore&lt;&#x2F;code&gt;, and binds
+QUIC with the configured &lt;code&gt;PoolConfig&lt;&#x2F;code&gt;. The direct &lt;code&gt;rusqlite&lt;&#x2F;code&gt; dependency was
+removed from the daemon crate.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CLI migration&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-cli&#x2F;src&#x2F;commands&#x2F;init.rs&lt;&#x2F;code&gt;, &lt;code&gt;create.rs&lt;&#x2F;code&gt;) — Both
+&lt;code&gt;init&lt;&#x2F;code&gt; and &lt;code&gt;create&lt;&#x2F;code&gt; commands now use &lt;code&gt;tesseras_storage::open_database()&lt;&#x2F;code&gt; with
+the default &lt;code&gt;StorageConfig&lt;&#x2F;code&gt; instead of opening raw &lt;code&gt;rusqlite&lt;&#x2F;code&gt; connections. The
+&lt;code&gt;rusqlite&lt;&#x2F;code&gt; dependency was removed from the CLI crate.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Decorator pattern for caching&lt;&#x2F;strong&gt;: &lt;code&gt;CachedFragmentStore&lt;&#x2F;code&gt; wraps
+&lt;code&gt;Box&amp;lt;dyn FragmentStore&amp;gt;&lt;&#x2F;code&gt; and implements &lt;code&gt;FragmentStore&lt;&#x2F;code&gt; itself. This means
+caching is opt-in, composable, and invisible to consumers. The daemon enables
+it; tests can skip it.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Byte-aware eviction&lt;&#x2F;strong&gt;: the LRU cache tracks total bytes, not entry count.
+Fragment blobs vary wildly in size (a 4KB text fragment vs a 2MB photo shard),
+so counting entries would give a misleading picture of memory usage.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;No connection pool crate&lt;&#x2F;strong&gt;: instead of pulling in a generic pool library,
+the connection pool is a thin wrapper around
+&lt;code&gt;DashMap&amp;lt;SocketAddr, PooledConnection&amp;gt;&lt;&#x2F;code&gt; with a Tokio reaper. QUIC connections
+are multiplexed, so the &quot;pool&quot; is really about lifecycle management (idle
+cleanup, max connections) rather than borrowing&#x2F;returning.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Stored checksums over re-reads&lt;&#x2F;strong&gt;: the attestation fix is intentionally
+minimal — one line changed, one disk read removed per fragment. The checksums
+were already stored in SQLite by &lt;code&gt;store_fragment()&lt;&#x2F;code&gt;, they just weren&#x27;t being
+used.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Centralized pragma configuration&lt;&#x2F;strong&gt;: a single &lt;code&gt;StorageConfig&lt;&#x2F;code&gt; struct replaces
+scattered &lt;code&gt;PRAGMA&lt;&#x2F;code&gt; calls. The &lt;code&gt;sqlite_synchronous_full&lt;&#x2F;code&gt; flag exists
+specifically for Raspberry Pi deployments where the kernel can crash and lose
+un-checkpointed WAL transactions.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued&lt;&#x2F;strong&gt; — Shamir&#x27;s Secret Sharing for heirs, sealed tesseras
+(time-lock encryption), security audits, institutional node onboarding,
+storage deduplication, OS packaging&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;With performance tuning in place, Tesseras handles the common case efficiently:
+fragment reads hit the LRU cache, attestation skips disk I&#x2F;O, idle QUIC
+connections are reaped automatically, and SQLite is configured consistently
+across the entire stack. The next steps focus on cryptographic features (Shamir,
+time-lock) and hardening for production deployment.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Verify Without Installing Anything</title>
+ <published>2026-02-15T20:00:00+00:00</published>
+ <updated>2026-02-15T20:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-wasm-browser-verification/"/>
+ <id>https://tesseras.net/news/phase4-wasm-browser-verification/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-wasm-browser-verification/">&lt;p&gt;Trust shouldn&#x27;t require installing software. If someone sends you a tessera — a
+bundle of preserved memories — you should be able to verify it&#x27;s genuine and
+unmodified without downloading an app, creating an account, or trusting a
+server. That&#x27;s what &lt;code&gt;tesseras-wasm&lt;&#x2F;code&gt; delivers: drag a tessera archive into a web
+page, and cryptographic verification happens entirely in your browser.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-wasm&lt;&#x2F;strong&gt; — A Rust crate that compiles to WebAssembly via wasm-pack,
+exposing four stateless functions to JavaScript. The crate depends on
+&lt;code&gt;tesseras-core&lt;&#x2F;code&gt; for manifest parsing and calls cryptographic primitives directly
+(blake3, ed25519-dalek) rather than depending on &lt;code&gt;tesseras-crypto&lt;&#x2F;code&gt;, which pulls
+in C-based post-quantum libraries that don&#x27;t compile to
+&lt;code&gt;wasm32-unknown-unknown&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;code&gt;parse_manifest&lt;&#x2F;code&gt; takes raw MANIFEST bytes (UTF-8 plain text, not MessagePack),
+delegates to &lt;code&gt;tesseras_core::manifest::Manifest::parse()&lt;&#x2F;code&gt;, and returns a JSON
+string with the creator&#x27;s Ed25519 public key, signature file paths, and a list
+of files with their expected BLAKE3 hashes, sizes, and MIME types. Internal
+structs (&lt;code&gt;ManifestJson&lt;&#x2F;code&gt;, &lt;code&gt;CreatorPubkey&lt;&#x2F;code&gt;, &lt;code&gt;SignatureFiles&lt;&#x2F;code&gt;, &lt;code&gt;FileEntry&lt;&#x2F;code&gt;) are
+serialized with serde_json. The ML-DSA public key and signature file fields are
+present in the JSON contract but set to &lt;code&gt;null&lt;&#x2F;code&gt; — ready for when post-quantum
+signing is implemented on the native side.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;code&gt;hash_blake3&lt;&#x2F;code&gt; computes a BLAKE3 hash of arbitrary bytes and returns a
+64-character hex string. It&#x27;s called once per file in the tessera to verify
+integrity against the MANIFEST.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;code&gt;verify_ed25519&lt;&#x2F;code&gt; takes a message, a 64-byte signature, and a 32-byte public key,
+constructs an &lt;code&gt;ed25519_dalek::VerifyingKey&lt;&#x2F;code&gt;, and returns whether the signature
+is valid. Length validation returns descriptive errors (&quot;Ed25519 public key must
+be 32 bytes&quot;) rather than panicking.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;code&gt;verify_ml_dsa&lt;&#x2F;code&gt; is a stub that returns an error explaining ML-DSA verification
+is not yet available. This is deliberate: the &lt;code&gt;ml-dsa&lt;&#x2F;code&gt; crate on crates.io is
+v0.1.0-rc.7 (pre-release), and &lt;code&gt;tesseras-crypto&lt;&#x2F;code&gt; uses &lt;code&gt;pqcrypto-dilithium&lt;&#x2F;code&gt;
+(C-based CRYSTALS-Dilithium) which is byte-incompatible with FIPS 204 ML-DSA.
+Both sides need to use the same pure Rust implementation before
+cross-verification works. Ed25519 verification is sufficient — every tessera is
+Ed25519-signed.&lt;&#x2F;p&gt;
+&lt;p&gt;All four functions use a two-layer pattern for testability: inner functions
+return &lt;code&gt;Result&amp;lt;T, String&amp;gt;&lt;&#x2F;code&gt; and are tested natively, while thin &lt;code&gt;#[wasm_bindgen]&lt;&#x2F;code&gt;
+wrappers convert errors to &lt;code&gt;JsError&lt;&#x2F;code&gt;. This avoids &lt;code&gt;JsError::new()&lt;&#x2F;code&gt; panicking on
+non-WASM targets during testing.&lt;&#x2F;p&gt;
+&lt;p&gt;The compiled WASM binary is 109 KB raw and 44 KB gzipped — well under the 200 KB
+budget. wasm-opt applies &lt;code&gt;-Oz&lt;&#x2F;code&gt; optimization after wasm-pack builds with
+&lt;code&gt;opt-level = &quot;z&quot;&lt;&#x2F;code&gt;, LTO, and single codegen unit.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;@tesseras&#x2F;verify&lt;&#x2F;strong&gt; — A TypeScript npm package (&lt;code&gt;crates&#x2F;tesseras-wasm&#x2F;js&#x2F;&lt;&#x2F;code&gt;)
+that orchestrates browser-side verification. The public API is a single
+function:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code data-lang=&quot;typescript&quot;&gt;async function verifyTessera(
+ archive: Uint8Array,
+ onProgress?: (current: number, total: number, file: string) =&amp;gt; void
+): Promise&amp;lt;VerificationResult&amp;gt;
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;The &lt;code&gt;VerificationResult&lt;&#x2F;code&gt; type provides everything a UI needs: overall validity,
+tessera hash, creator public keys, signature status (valid&#x2F;invalid&#x2F;missing for
+both Ed25519 and ML-DSA), per-file integrity results with expected and actual
+hashes, a list of unexpected files not in the MANIFEST, and an errors array.&lt;&#x2F;p&gt;
+&lt;p&gt;Archive unpacking (&lt;code&gt;unpack.ts&lt;&#x2F;code&gt;) handles three formats: gzip-compressed tar
+(detected by &lt;code&gt;\x1f\x8b&lt;&#x2F;code&gt; magic bytes, decompressed with fflate then parsed as
+tar), ZIP (&lt;code&gt;PK\x03\x04&lt;&#x2F;code&gt; magic, unpacked with fflate&#x27;s &lt;code&gt;unzipSync&lt;&#x2F;code&gt;), and raw tar
+(&lt;code&gt;ustar&lt;&#x2F;code&gt; at offset 257). A &lt;code&gt;normalizePath&lt;&#x2F;code&gt; function strips the leading
+&lt;code&gt;tessera-&amp;lt;hash&amp;gt;&#x2F;&lt;&#x2F;code&gt; prefix so internal paths match MANIFEST entries.&lt;&#x2F;p&gt;
+&lt;p&gt;Verification runs in a Web Worker (&lt;code&gt;worker.ts&lt;&#x2F;code&gt;) to keep the UI thread
+responsive. The worker initializes the WASM module, unpacks the archive, parses
+the MANIFEST, verifies the Ed25519 signature against the creator&#x27;s public key,
+then hashes each file with BLAKE3 and compares against expected values. Progress
+messages stream back to the main thread after each file. If any signature is
+invalid, verification stops early without hashing files — failing fast on the
+most critical check.&lt;&#x2F;p&gt;
+&lt;p&gt;The archive is transferred to the worker with zero-copy
+(&lt;code&gt;worker.postMessage({ type: &quot;verify&quot;, archive }, [archive.buffer])&lt;&#x2F;code&gt;) to avoid
+duplicating potentially large tessera files in memory.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Build pipeline&lt;&#x2F;strong&gt; — Three new justfile targets: &lt;code&gt;wasm-build&lt;&#x2F;code&gt; runs wasm-pack
+with &lt;code&gt;--target web --release&lt;&#x2F;code&gt; and optimizes with wasm-opt; &lt;code&gt;wasm-size&lt;&#x2F;code&gt; reports
+raw and gzipped binary size; &lt;code&gt;test-wasm&lt;&#x2F;code&gt; runs the native test suite.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Tests&lt;&#x2F;strong&gt; — 9 native unit tests cover BLAKE3 hashing (empty input, known value),
+Ed25519 verification (valid signature, invalid signature, wrong key, bad key
+length), and MANIFEST parsing (valid manifest, invalid UTF-8, garbage input). 3
+WASM integration tests run in headless Chrome via
+&lt;code&gt;wasm-pack test --headless --chrome&lt;&#x2F;code&gt;, verifying that &lt;code&gt;hash_blake3&lt;&#x2F;code&gt;,
+&lt;code&gt;verify_ed25519&lt;&#x2F;code&gt;, and &lt;code&gt;parse_manifest&lt;&#x2F;code&gt; work correctly when compiled to
+&lt;code&gt;wasm32-unknown-unknown&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;No tesseras-crypto dependency&lt;&#x2F;strong&gt;: the WASM crate calls blake3 and
+ed25519-dalek directly. &lt;code&gt;tesseras-crypto&lt;&#x2F;code&gt; depends on &lt;code&gt;pqcrypto-kyber&lt;&#x2F;code&gt; (C-based
+ML-KEM via pqcrypto-traits) which requires a C compiler toolchain and doesn&#x27;t
+target wasm32. By depending only on pure Rust crates, the WASM build has zero
+C dependencies and compiles cleanly to WebAssembly.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;ML-DSA deferred, not faked&lt;&#x2F;strong&gt;: rather than silently skipping post-quantum
+verification, the stub returns an explicit error. This ensures that if a
+tessera contains an ML-DSA signature, the verification result will report
+&lt;code&gt;ml_dsa: &quot;missing&quot;&lt;&#x2F;code&gt; rather than pretending it was checked. The JS orchestrator
+handles this gracefully — a tessera is valid if Ed25519 passes and ML-DSA is
+missing (not yet implemented on either side).&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Inner function pattern&lt;&#x2F;strong&gt;: &lt;code&gt;JsError&lt;&#x2F;code&gt; cannot be constructed on non-WASM
+targets (it panics). Splitting each function into
+&lt;code&gt;foo_inner() -&amp;gt; Result&amp;lt;T, String&amp;gt;&lt;&#x2F;code&gt; and &lt;code&gt;foo() -&amp;gt; Result&amp;lt;T, JsError&amp;gt;&lt;&#x2F;code&gt; lets the
+native test suite exercise all logic without touching JavaScript types. The
+WASM integration tests in headless Chrome test the full &lt;code&gt;#[wasm_bindgen]&lt;&#x2F;code&gt;
+surface.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Web Worker isolation&lt;&#x2F;strong&gt;: cryptographic operations (especially BLAKE3 over
+large media files) can take hundreds of milliseconds. Running in a Worker
+prevents UI jank. The streaming progress protocol
+(&lt;code&gt;{ type: &quot;progress&quot;, current, total, file }&lt;&#x2F;code&gt;) lets the UI show a progress bar
+during verification of tesseras with many files.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Zero-copy transfer&lt;&#x2F;strong&gt;: &lt;code&gt;archive.buffer&lt;&#x2F;code&gt; is transferred to the Worker, not
+copied. For a 50 MB tessera archive, this avoids doubling memory usage during
+verification.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Plain text MANIFEST, not MessagePack&lt;&#x2F;strong&gt;: the WASM crate parses the same
+plain-text MANIFEST format as the CLI. This is by design — the MANIFEST is the
+tessera&#x27;s Rosetta Stone, readable by anyone with a text editor. The
+&lt;code&gt;rmp-serde&lt;&#x2F;code&gt; dependency in the Cargo.toml is not used and will be removed.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4: Resilience and Scale&lt;&#x2F;strong&gt; — OS packaging (Alpine, Arch, Debian,
+FreeBSD, OpenBSD), CI on SourceHut and GitHub Actions, security audits,
+browser-based tessera explorer at tesseras.net using @tesseras&#x2F;verify&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — Public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Verification no longer requires trust in software. A tessera archive dropped
+into a browser is verified with the same cryptographic rigor as the CLI — same
+BLAKE3 hashes, same Ed25519 signatures, same MANIFEST parser. The difference is
+that now anyone can do it.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Punching Through NATs</title>
+ <published>2026-02-15T18:00:00+00:00</published>
+ <updated>2026-02-15T18:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-nat-traversal/"/>
+ <id>https://tesseras.net/news/phase4-nat-traversal/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-nat-traversal/">&lt;p&gt;Most people&#x27;s devices sit behind a NAT — a network address translator that lets
+them reach the internet but prevents incoming connections. For a P2P network,
+this is an existential problem: if two nodes behind NATs can&#x27;t talk to each
+other, the network fragments. Phase 4 continues with a full NAT traversal stack:
+STUN-based discovery, coordinated hole punching, and relay fallback.&lt;&#x2F;p&gt;
+&lt;p&gt;The approach follows the same pattern as most battle-tested P2P systems (WebRTC,
+BitTorrent, IPFS): try the cheapest option first, escalate only when necessary.
+Direct connectivity costs nothing. Hole punching costs a few coordinated
+packets. Relaying costs sustained bandwidth from a third party. Tesseras tries
+them in that order.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;NatType classification&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-core&#x2F;src&#x2F;network.rs&lt;&#x2F;code&gt;) — A new &lt;code&gt;NatType&lt;&#x2F;code&gt;
+enum (Public, Cone, Symmetric, Unknown) added to the core domain layer. This
+type is shared across the entire stack: the STUN client writes it, the DHT
+advertises it in Pong messages, and the punch coordinator reads it to decide
+whether hole punching is even worth attempting (Cone-to-Cone works ~80% of the
+time; Symmetric-to-Symmetric almost never works).&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;STUN client&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-net&#x2F;src&#x2F;stun.rs&lt;&#x2F;code&gt;) — A minimal STUN implementation
+(RFC 5389 Binding Request&#x2F;Response) that discovers a node&#x27;s external address.
+The codec encodes 20-byte binding requests with a random transaction ID and
+decodes XOR-MAPPED-ADDRESS responses. The &lt;code&gt;discover_nat()&lt;&#x2F;code&gt; function queries
+multiple STUN servers in parallel (Google, Cloudflare by default), compares the
+mapped addresses, and classifies the NAT type:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;Same IP and port from all servers → &lt;strong&gt;Public&lt;&#x2F;strong&gt; (no NAT)&lt;&#x2F;li&gt;
+&lt;li&gt;Same mapped address from all servers → &lt;strong&gt;Cone&lt;&#x2F;strong&gt; (hole punching works)&lt;&#x2F;li&gt;
+&lt;li&gt;Different mapped addresses → &lt;strong&gt;Symmetric&lt;&#x2F;strong&gt; (hole punching unreliable)&lt;&#x2F;li&gt;
+&lt;li&gt;No responses → &lt;strong&gt;Unknown&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Retries with exponential backoff and configurable timeouts. 12 tests covering
+codec roundtrips, all classification paths, and async loopback queries.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Signed punch coordination&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-net&#x2F;src&#x2F;punch.rs&lt;&#x2F;code&gt;) — Ed25519 signing
+and verification for &lt;code&gt;PunchIntro&lt;&#x2F;code&gt;, &lt;code&gt;RelayRequest&lt;&#x2F;code&gt;, and &lt;code&gt;RelayMigrate&lt;&#x2F;code&gt; messages.
+Every introduction is signed by the initiator with a 30-second timestamp window,
+preventing reflection attacks (where an attacker replays an old introduction to
+redirect traffic). The payload format is &lt;code&gt;target || external_addr || timestamp&lt;&#x2F;code&gt;
+— changing any field invalidates the signature. 6 unit tests plus 3
+property-based tests with proptest (arbitrary node IDs, ports, and session
+tokens).&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Relay session manager&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-net&#x2F;src&#x2F;relay.rs&lt;&#x2F;code&gt;) — Manages transparent
+UDP relay sessions between NATed peers. Each session has a random 16-byte token;
+peers prefix their packets with the token, the relay strips it and forwards.
+Features:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;Bidirectional forwarding (A→R→B and B→R→A)&lt;&#x2F;li&gt;
+&lt;li&gt;Rate limiting: 256 KB&#x2F;s for reciprocal peers, 64 KB&#x2F;s for non-reciprocal&lt;&#x2F;li&gt;
+&lt;li&gt;10-minute maximum duration for bootstrap (non-reciprocal) sessions&lt;&#x2F;li&gt;
+&lt;li&gt;Address migration: when a peer&#x27;s IP changes (Wi-Fi to cellular), a signed
+&lt;code&gt;RelayMigrate&lt;&#x2F;code&gt; updates the session without tearing it down&lt;&#x2F;li&gt;
+&lt;li&gt;Idle cleanup with configurable timeout&lt;&#x2F;li&gt;
+&lt;li&gt;8 unit tests plus 2 property-based tests&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;DHT message extensions&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-dht&#x2F;src&#x2F;message.rs&lt;&#x2F;code&gt;) — Seven new message
+variants added to the DHT protocol:&lt;&#x2F;p&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Message&lt;&#x2F;th&gt;&lt;th&gt;Purpose&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;PunchIntro&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&quot;I want to connect to node X, here&#x27;s my signed external address&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;PunchRequest&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Introducer forwards the request to the target&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;PunchReady&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Target confirms readiness, sends its external address&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;RelayRequest&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;&quot;Create a relay session to node X&quot;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;RelayOffer&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Relay responds with its address and session token&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;RelayClose&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Tear down a relay session&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;&lt;code&gt;RelayMigrate&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Update session after network change&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;p&gt;The &lt;code&gt;Pong&lt;&#x2F;code&gt; message was extended with NAT metadata: &lt;code&gt;nat_type&lt;&#x2F;code&gt;,
+&lt;code&gt;relay_slots_available&lt;&#x2F;code&gt;, and &lt;code&gt;relay_bandwidth_used_kbps&lt;&#x2F;code&gt;. All new fields use
+&lt;code&gt;#[serde(default)]&lt;&#x2F;code&gt; for backward compatibility — old nodes ignore what they
+don&#x27;t recognize, new nodes fall back to defaults. 9 new serialization roundtrip
+tests.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;NatHandler trait and dispatch&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-dht&#x2F;src&#x2F;engine.rs&lt;&#x2F;code&gt;) — A new
+&lt;code&gt;NatHandler&lt;&#x2F;code&gt; async trait (5 methods) injected into the DHT engine, following the
+same dependency injection pattern as the existing &lt;code&gt;ReplicationHandler&lt;&#x2F;code&gt;. The
+engine&#x27;s message dispatch loop now routes all punch&#x2F;relay messages to the
+handler. This keeps the DHT engine protocol-agnostic while allowing the NAT
+traversal logic to live in &lt;code&gt;tesseras-net&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Mobile reconnection types&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-embedded&#x2F;src&#x2F;reconnect.rs&lt;&#x2F;code&gt;) — A
+three-phase reconnection state machine for mobile devices:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;&lt;strong&gt;QuicMigration&lt;&#x2F;strong&gt; (0-2s) — try QUIC connection migration for all active peers&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;ReStun&lt;&#x2F;strong&gt; (2-5s) — re-discover external address via STUN&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;ReEstablish&lt;&#x2F;strong&gt; (5-10s) — reconnect peers that migration couldn&#x27;t save&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;Peers are reconnected in priority order: bootstrap nodes first, then nodes
+holding our fragments, then nodes whose fragments we hold, then general DHT
+neighbors. A new &lt;code&gt;NetworkChanged&lt;&#x2F;code&gt; event variant was added to the FFI event
+stream so the Flutter app can show reconnection progress.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Daemon NAT configuration&lt;&#x2F;strong&gt; (&lt;code&gt;tesd&#x2F;src&#x2F;config.rs&lt;&#x2F;code&gt;) — A new &lt;code&gt;[nat]&lt;&#x2F;code&gt; section in
+the TOML config with STUN server list, relay toggle, max relay sessions,
+bandwidth limits (reciprocal vs bootstrap), and idle timeout. All fields have
+sensible defaults; relay is disabled by default.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Prometheus metrics&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-net&#x2F;src&#x2F;metrics.rs&lt;&#x2F;code&gt;) — 16 metrics across four
+subsystems:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;STUN&lt;&#x2F;strong&gt;: requests, failures, latency histogram&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Punch&lt;&#x2F;strong&gt;: attempts&#x2F;successes&#x2F;failures (by NAT type pair), latency histogram&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Relay&lt;&#x2F;strong&gt;: active sessions, total sessions, bytes forwarded, idle timeouts,
+rate limit hits&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Reconnect&lt;&#x2F;strong&gt;: network changes, attempts&#x2F;successes by phase, duration
+histogram&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;6 tests verifying registration, increment, label cardinality, and
+double-registration detection.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Integration tests&lt;&#x2F;strong&gt; — Two end-to-end tests using &lt;code&gt;MemTransport&lt;&#x2F;code&gt; (in-memory
+simulated network):&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;punch_integration.rs&lt;&#x2F;code&gt; — Full 3-node hole-punch flow: A sends signed
+&lt;code&gt;PunchIntro&lt;&#x2F;code&gt; to introducer I, I verifies and forwards &lt;code&gt;PunchRequest&lt;&#x2F;code&gt; to B, B
+verifies the original signature and sends &lt;code&gt;PunchReady&lt;&#x2F;code&gt; back, A and B exchange
+messages directly. Also tests that a bad signature is correctly rejected.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;relay_integration.rs&lt;&#x2F;code&gt; — Full 3-node relay flow: A requests relay from R, R
+creates session and sends &lt;code&gt;RelayOffer&lt;&#x2F;code&gt; to both peers, A and B exchange
+token-prefixed packets through R, A migrates to a new address mid-session, A
+closes the session, and the test verifies the session is torn down and further
+forwarding fails.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;Property tests&lt;&#x2F;strong&gt; — 7 proptest-based tests covering: signature round-trips for
+all three signed message types (arbitrary node IDs, ports, tokens), NAT
+classification determinism (same inputs always produce same output), STUN
+binding request validity, session token uniqueness, and relay rejection of
+too-short packets.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Justfile targets&lt;&#x2F;strong&gt; — &lt;code&gt;just test-nat&lt;&#x2F;code&gt; runs all NAT traversal tests across
+&lt;code&gt;tesseras-net&lt;&#x2F;code&gt; and &lt;code&gt;tesseras-dht&lt;&#x2F;code&gt;. &lt;code&gt;just test-chaos&lt;&#x2F;code&gt; is a placeholder for future
+Docker Compose chaos tests with &lt;code&gt;tc netem&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;STUN over TURN&lt;&#x2F;strong&gt;: we implement STUN (discovery) and custom relay rather than
+full TURN. TURN requires authenticated allocation and is designed for media
+relay; our relay is simpler — token-prefixed UDP forwarding with rate limits.
+This keeps the protocol minimal and avoids depending on external TURN servers.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Signatures on introductions&lt;&#x2F;strong&gt;: every &lt;code&gt;PunchIntro&lt;&#x2F;code&gt; is signed by the
+initiator. Without this, an attacker could send forged introductions to
+redirect a node&#x27;s hole-punch attempts to an attacker-controlled address (a
+reflection attack). The 30-second timestamp window limits replay.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Reciprocal bandwidth tiers&lt;&#x2F;strong&gt;: relay nodes give 4x more bandwidth (256 vs 64
+KB&#x2F;s) to peers with good reciprocity scores. This incentivizes nodes to store
+fragments for others — if you contribute, you get better relay service when
+you need it.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Backward-compatible Pong extension&lt;&#x2F;strong&gt;: new NAT fields in &lt;code&gt;Pong&lt;&#x2F;code&gt; use
+&lt;code&gt;#[serde(default)]&lt;&#x2F;code&gt; and &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;&#x2F;code&gt;. Old nodes that don&#x27;t understand these
+fields simply skip them during deserialization. No protocol version bump
+needed.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;NatHandler as async trait&lt;&#x2F;strong&gt;: the NAT traversal logic is injected into the
+DHT engine via a trait, just like &lt;code&gt;ReplicationHandler&lt;&#x2F;code&gt;. This keeps the DHT
+engine focused on routing and peer management, and allows the NAT
+implementation to be swapped or disabled without touching core DHT code.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued&lt;&#x2F;strong&gt; — performance tuning (connection pooling, fragment
+caching, SQLite WAL), security audits, institutional node onboarding, OS
+packaging&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;With NAT traversal, Tesseras can connect nodes regardless of their network
+topology. Public nodes talk directly. Cone-NATed nodes punch through with an
+introducer&#x27;s help. Symmetric-NATed or firewalled nodes relay through willing
+peers. The network adapts to the real world, where most devices are behind a NAT
+and network conditions change constantly.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>CLI Meets Network: Publish, Fetch, and Status Commands</title>
+ <published>2026-02-15T00:00:00+00:00</published>
+ <updated>2026-02-15T00:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/cli-daemon-rpc/"/>
+ <id>https://tesseras.net/news/cli-daemon-rpc/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/cli-daemon-rpc/">&lt;p&gt;Until now the CLI operated in isolation: create a tessera, verify it, export it,
+list what you have. Everything stayed on your machine. With this release, &lt;code&gt;tes&lt;&#x2F;code&gt;
+gains three commands that bridge the gap between local storage and the P2P
+network — &lt;code&gt;publish&lt;&#x2F;code&gt;, &lt;code&gt;fetch&lt;&#x2F;code&gt;, and &lt;code&gt;status&lt;&#x2F;code&gt; — by talking to a running &lt;code&gt;tesd&lt;&#x2F;code&gt; over
+a Unix socket.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;&lt;code&gt;tesseras-rpc&lt;&#x2F;code&gt; crate&lt;&#x2F;strong&gt; — A new shared crate that both the CLI and daemon
+depend on. It defines the RPC protocol using MessagePack serialization with
+length-prefixed framing (4-byte big-endian size header, 64 MiB max). Three
+request types (&lt;code&gt;Publish&lt;&#x2F;code&gt;, &lt;code&gt;Fetch&lt;&#x2F;code&gt;, &lt;code&gt;Status&lt;&#x2F;code&gt;) and their corresponding responses.
+A sync &lt;code&gt;DaemonClient&lt;&#x2F;code&gt; handles the Unix socket connection with configurable
+timeouts. The protocol is deliberately simple — one request, one response,
+connection closed — to keep the implementation auditable.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;&lt;code&gt;tes publish &amp;lt;hash&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — Publishes a tessera to the network. Accepts full
+hashes or short prefixes (e.g., &lt;code&gt;tes publish a1b2&lt;&#x2F;code&gt;), which are resolved against
+the local database. The daemon reads all tessera files from storage, packs them
+into a single MessagePack buffer, and hands them to the replication engine.
+Small tesseras (&amp;lt; 4 MB) are replicated as a single fragment; larger ones go
+through Reed-Solomon erasure coding. Output shows the short hash and fragment
+count:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code&gt;Published tessera 9f2c4a1b (24 fragments created)
+Distribution in progress — use `tes status 9f2c4a1b` to track.
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;&lt;strong&gt;&lt;code&gt;tes fetch &amp;lt;hash&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — Retrieves a tessera from the network using its full
+content hash. The daemon collects locally available fragments, reconstructs the
+original data via erasure decoding if needed, unpacks the files, and stores them
+in the content-addressable store. Returns the number of memories and total size
+fetched.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;&lt;code&gt;tes status &amp;lt;hash&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — Displays the replication health of a tessera. The
+output maps directly to the replication engine&#x27;s internal health model:&lt;&#x2F;p&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;State&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;Local&lt;&#x2F;td&gt;&lt;td&gt;Not yet published — exists only on your machine&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Publishing&lt;&#x2F;td&gt;&lt;td&gt;Fragments being distributed, critical redundancy&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Replicated&lt;&#x2F;td&gt;&lt;td&gt;Distributed but below target redundancy&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Healthy&lt;&#x2F;td&gt;&lt;td&gt;Full redundancy achieved&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;p&gt;&lt;strong&gt;Daemon RPC listener&lt;&#x2F;strong&gt; — The daemon now binds a Unix socket (default:
+&lt;code&gt;$XDG_RUNTIME_DIR&#x2F;tesseras&#x2F;daemon.sock&lt;&#x2F;code&gt;) with proper directory permissions
+(0700), stale socket cleanup, and graceful shutdown. Each connection is handled
+in a Tokio task — the listener converts the async stream to sync I&#x2F;O for the
+framing layer, dispatches to the RPC handler, and writes the response back.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Pack&#x2F;unpack in &lt;code&gt;tesseras-core&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; — A small module that serializes a list of
+file entries (path + data) into a single MessagePack buffer and back. This is
+the bridge between the tessera&#x27;s directory structure and the replication
+engine&#x27;s opaque byte blobs.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Unix socket over TCP&lt;&#x2F;strong&gt;: RPC between CLI and daemon happens on the same
+machine. Unix sockets are faster, don&#x27;t need port allocation, and filesystem
+permissions provide access control without TLS.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;MessagePack over JSON&lt;&#x2F;strong&gt;: the same wire format used everywhere else in
+Tesseras. Compact, schema-less, and already a workspace dependency. A typical
+publish request&#x2F;response round-trip is under 200 bytes.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Sync client, async daemon&lt;&#x2F;strong&gt;: the &lt;code&gt;DaemonClient&lt;&#x2F;code&gt; uses blocking I&#x2F;O because
+the CLI doesn&#x27;t need concurrency — it sends one request and waits. The daemon
+listener is async (Tokio) to handle multiple connections. The framing layer
+works with any &lt;code&gt;Read&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Write&lt;&#x2F;code&gt; impl, bridging both worlds.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Hash prefix resolution on the client side&lt;&#x2F;strong&gt;: &lt;code&gt;publish&lt;&#x2F;code&gt; and &lt;code&gt;status&lt;&#x2F;code&gt; resolve
+short prefixes locally before sending the full hash to the daemon. This keeps
+the daemon stateless — it doesn&#x27;t need access to the CLI&#x27;s database.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Default data directory alignment&lt;&#x2F;strong&gt;: the CLI default changed from
+&lt;code&gt;~&#x2F;.tesseras&lt;&#x2F;code&gt; to &lt;code&gt;~&#x2F;.local&#x2F;share&#x2F;tesseras&lt;&#x2F;code&gt; (via &lt;code&gt;dirs::data_dir()&lt;&#x2F;code&gt;) to match
+the daemon. A migration hint is printed when legacy data is detected.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;DHT peer count&lt;&#x2F;strong&gt;: the &lt;code&gt;status&lt;&#x2F;code&gt; command currently reports 0 peers — wiring
+the actual peer count from the DHT is the next step&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;&lt;code&gt;tes show&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;: display the contents of a tessera (memories, metadata) without
+exporting&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Streaming fetch&lt;&#x2F;strong&gt;: for large tesseras, stream fragments as they arrive
+rather than waiting for all of them&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing</title>
+ <published>2026-02-15T00:00:00+00:00</published>
+ <updated>2026-02-15T00:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-shamir-heir-recovery/"/>
+ <id>https://tesseras.net/news/phase4-shamir-heir-recovery/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-shamir-heir-recovery/">&lt;p&gt;What happens to your memories when you die? Until now, Tesseras could preserve
+content across millennia — but the private and sealed keys died with their
+owner. Phase 4 continues with a solution: Shamir&#x27;s Secret Sharing, a
+cryptographic scheme that lets you split your identity into shares and
+distribute them to the people you trust most.&lt;&#x2F;p&gt;
+&lt;p&gt;The math is elegant: you choose a threshold T and a total N. Any T shares
+reconstruct the full secret; T-1 shares reveal absolutely nothing. This is not
+&quot;almost nothing&quot; — it is information-theoretically secure. An attacker with one
+fewer share than the threshold has exactly zero bits of information about the
+secret, no matter how much computing power they have.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;GF(256) finite field arithmetic&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;shamir&#x2F;gf256.rs&lt;&#x2F;code&gt;) —
+Shamir&#x27;s Secret Sharing requires arithmetic in a finite field. We implement
+GF(256) using the same irreducible polynomial as AES (x^8 + x^4 + x^3 + x + 1),
+with compile-time lookup tables for logarithm and exponentiation. All operations
+are constant-time via table lookups — no branches on secret data. The module
+includes Horner&#x27;s method for polynomial evaluation and Lagrange interpolation at
+x=0 for secret recovery. 233 lines, exhaustively tested: all 256 elements for
+identity&#x2F;inverse properties, commutativity, and associativity.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;ShamirSplitter&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;shamir&#x2F;mod.rs&lt;&#x2F;code&gt;) — The core
+split&#x2F;reconstruct API. &lt;code&gt;split()&lt;&#x2F;code&gt; takes a secret byte slice, a configuration
+(threshold T, total N), and the owner&#x27;s Ed25519 public key. For each byte of the
+secret, it constructs a random polynomial of degree T-1 over GF(256) with the
+secret byte as the constant term, then evaluates it at N distinct points.
+&lt;code&gt;reconstruct()&lt;&#x2F;code&gt; takes T or more shares and recovers the secret via Lagrange
+interpolation. Both operations include extensive validation: threshold bounds,
+session consistency, owner fingerprint matching, and BLAKE3 checksum
+verification.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;HeirShare format&lt;&#x2F;strong&gt; — Each share is a self-contained, serializable artifact
+with:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;Format version (v1) for forward compatibility&lt;&#x2F;li&gt;
+&lt;li&gt;Share index (1..N) and threshold&#x2F;total metadata&lt;&#x2F;li&gt;
+&lt;li&gt;Session ID (random 8 bytes) — prevents mixing shares from different split
+sessions&lt;&#x2F;li&gt;
+&lt;li&gt;Owner fingerprint (first 8 bytes of BLAKE3 hash of the Ed25519 public key)&lt;&#x2F;li&gt;
+&lt;li&gt;Share data (the Shamir y-values, same length as the secret)&lt;&#x2F;li&gt;
+&lt;li&gt;BLAKE3 checksum over all preceding fields&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Shares are serialized in two formats: &lt;strong&gt;MessagePack&lt;&#x2F;strong&gt; (compact binary, for
+programmatic use) and &lt;strong&gt;base64 text&lt;&#x2F;strong&gt; (human-readable, for printing and physical
+storage). The text format includes a header with metadata and delimiters:&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code&gt;--- TESSERAS HEIR SHARE ---
+Format: v1
+Owner: a1b2c3d4e5f6a7b8 (fingerprint)
+Share: 1 of 3 (threshold: 2)
+Session: 9f8e7d6c5b4a3210
+Created: 2026-02-15
+
+&amp;lt;base64-encoded MessagePack data&amp;gt;
+--- END HEIR SHARE ---
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;p&gt;This format is designed to be printed on paper, stored in a safe deposit box, or
+engraved on metal. The header is informational — only the base64 payload is
+parsed during reconstruction.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;CLI integration&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-cli&#x2F;src&#x2F;commands&#x2F;heir.rs&lt;&#x2F;code&gt;) — Three new
+subcommands:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;tes heir create&lt;&#x2F;code&gt; — splits your Ed25519 identity into heir shares. Prompts for
+confirmation (your full identity is at stake), generates both &lt;code&gt;.bin&lt;&#x2F;code&gt; and
+&lt;code&gt;.txt&lt;&#x2F;code&gt; files for each share, and writes &lt;code&gt;heir_meta.json&lt;&#x2F;code&gt; to your identity
+directory.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tes heir reconstruct&lt;&#x2F;code&gt; — loads share files (auto-detects binary vs text
+format), validates consistency, reconstructs the secret, derives the Ed25519
+keypair, and optionally installs it to &lt;code&gt;~&#x2F;.tesseras&#x2F;identity&#x2F;&lt;&#x2F;code&gt; (with automatic
+backup of the existing identity).&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;tes heir info&lt;&#x2F;code&gt; — displays share metadata and verifies the checksum without
+exposing any secret material.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;Secret blob format&lt;&#x2F;strong&gt; — Identity keys are serialized into a versioned blob
+before splitting: a version byte (0x01), a flags byte (0x00 for Ed25519-only),
+followed by the 32-byte Ed25519 secret key. This leaves room for future
+expansion when X25519 and ML-KEM-768 private keys are integrated into the heir
+share system.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 20 unit tests for ShamirSplitter (roundtrip, all share
+combinations, insufficient shares, wrong owner, wrong session, threshold-1
+boundary, large secrets up to ML-KEM-768 key size). 7 unit tests for GF(256)
+arithmetic (exhaustive field properties). 3 property-based tests with proptest
+(arbitrary secrets up to 5000 bytes, arbitrary T-of-N configurations,
+information-theoretic security verification). Serialization roundtrip tests for
+both MessagePack and base64 text formats. 2 integration tests covering the
+complete heir lifecycle: generate identity, split into shares, serialize,
+deserialize, reconstruct, verify keypair, and sign&#x2F;verify with reconstructed
+keys.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;GF(256) over GF(prime)&lt;&#x2F;strong&gt;: we use GF(256) rather than a prime field because
+it maps naturally to bytes — each element is a single byte, each share is the
+same length as the secret. No big-integer arithmetic, no modular reduction, no
+padding. This is the same approach used by most real-world Shamir
+implementations including SSSS and Hashicorp Vault.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Compile-time lookup tables&lt;&#x2F;strong&gt;: the LOG and EXP tables for GF(256) are
+computed at compile time using &lt;code&gt;const fn&lt;&#x2F;code&gt;. This means zero runtime
+initialization cost and constant-time operations via table lookups rather than
+loops.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Session ID prevents cross-session mixing&lt;&#x2F;strong&gt;: each call to &lt;code&gt;split()&lt;&#x2F;code&gt; generates
+a fresh random session ID. If an heir accidentally uses shares from two
+different split sessions (e.g., before and after a key rotation),
+reconstruction fails cleanly with a validation error rather than producing
+garbage output.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;BLAKE3 checksums detect corruption&lt;&#x2F;strong&gt;: each share includes a BLAKE3 checksum
+over its contents. This catches bit rot, transmission errors, and accidental
+truncation before any reconstruction attempt. A share printed on paper and
+scanned back via OCR will fail the checksum if a single character is wrong.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Owner fingerprint for identification&lt;&#x2F;strong&gt;: shares include the first 8 bytes of
+BLAKE3(Ed25519 public key) as a fingerprint. This lets heirs verify which
+identity a share belongs to without revealing the full public key. During
+reconstruction, the fingerprint is cross-checked against the recovered key.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Dual format for resilience&lt;&#x2F;strong&gt;: both binary (MessagePack) and text (base64)
+formats are generated because physical media has different failure modes than
+digital storage. A USB drive might fail; paper survives. A QR code might be
+unreadable; base64 text can be manually typed.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Blob versioning&lt;&#x2F;strong&gt;: the secret is wrapped in a versioned blob (version +
+flags + key material) so future versions can include additional keys (X25519,
+ML-KEM-768) without breaking backward compatibility with existing shares.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued: Resilience and Scale&lt;&#x2F;strong&gt; — advanced NAT traversal
+(STUN&#x2F;TURN), performance tuning (connection pooling, fragment caching, SQLite
+WAL), security audits, institutional node onboarding, OS packaging&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;With Shamir&#x27;s Secret Sharing, Tesseras closes the last critical gap in long-term
+preservation. Your memories survive infrastructure failures through erasure
+coding. Your privacy survives quantum computers through hybrid encryption. And
+now, your identity survives you — passed on to the people you chose, requiring
+their cooperation to unlock what you left behind.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 4: Encryption and Sealed Tesseras</title>
+ <published>2026-02-14T16:00:00+00:00</published>
+ <updated>2026-02-14T16:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase4-encryption-sealed/"/>
+ <id>https://tesseras.net/news/phase4-encryption-sealed/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase4-encryption-sealed/">&lt;p&gt;Some memories are not meant for everyone. A private journal, a letter to be
+opened in 2050, a family secret sealed until the grandchildren are old enough.
+Until now, every tessera on the network was open. Phase 4 changes that: Tesseras
+now encrypts private and sealed content with a hybrid cryptographic scheme
+designed to resist both classical and quantum attacks.&lt;&#x2F;p&gt;
+&lt;p&gt;The principle remains the same — encrypt as little as possible. Public memories
+need availability, not secrecy. But when someone creates a private or sealed
+tessera, the content is now locked behind AES-256-GCM encryption with keys
+protected by a hybrid key encapsulation mechanism combining X25519 and
+ML-KEM-768. Both algorithms must be broken to access the content.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;AES-256-GCM encryptor&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;encryption.rs&lt;&#x2F;code&gt;) — Symmetric
+content encryption with random 12-byte nonces and authenticated associated data
+(AAD). The AAD binds ciphertext to its context: for private tesseras, the
+content hash is included; for sealed tesseras, both the content hash and the
+&lt;code&gt;open_after&lt;&#x2F;code&gt; timestamp are bound into the AAD. This means moving ciphertext
+between tesseras with different open dates causes decryption failure — you
+cannot trick the system into opening a sealed memory early by swapping its
+ciphertext into a tessera with an earlier seal date.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Hybrid Key Encapsulation Mechanism&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;kem.rs&lt;&#x2F;code&gt;) — Key
+exchange using X25519 (classical elliptic curve Diffie-Hellman) combined with
+ML-KEM-768 (the NIST-standardized post-quantum lattice-based KEM, formerly
+Kyber). Both shared secrets are combined via &lt;code&gt;blake3::derive_key&lt;&#x2F;code&gt; with a fixed
+context string (&quot;tesseras hybrid kem v1&quot;) to produce a single 256-bit content
+encryption key. This follows the same &quot;dual from day one&quot; philosophy as the
+project&#x27;s dual signing (Ed25519 + ML-DSA): if either algorithm is broken in the
+future, the other still protects the content.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Sealed Key Envelope&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;sealed.rs&lt;&#x2F;code&gt;) — Wraps a content
+encryption key using the hybrid KEM, so only the tessera owner can recover it.
+The KEM produces a transport key, which is XORed with the content key to produce
+a wrapped key stored alongside the KEM ciphertext. On unsealing, the owner
+decapsulates the KEM ciphertext to recover the transport key, then XORs again to
+recover the content key.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Key Publication&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-crypto&#x2F;src&#x2F;sealed.rs&lt;&#x2F;code&gt;) — A standalone signed
+artifact for publishing a sealed tessera&#x27;s content key after its &lt;code&gt;open_after&lt;&#x2F;code&gt;
+date has passed. The owner signs the content key, tessera hash, and publication
+timestamp with their dual keys (Ed25519, with ML-DSA placeholder). The manifest
+stays immutable — the key publication is a separate document. Other nodes verify
+the signature against the owner&#x27;s public key before using the published key to
+decrypt the content.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;EncryptionContext&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-core&#x2F;src&#x2F;enums.rs&lt;&#x2F;code&gt;) — A domain type that
+represents the AAD context for encryption. It lives in tesseras-core rather than
+tesseras-crypto because it&#x27;s a domain concept (not a crypto implementation
+detail). The &lt;code&gt;to_aad_bytes()&lt;&#x2F;code&gt; method produces deterministic serialization: a tag
+byte (0x00 for Private, 0x01 for Sealed), followed by the content hash, and for
+Sealed, the &lt;code&gt;open_after&lt;&#x2F;code&gt; timestamp as little-endian i64.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Domain validation&lt;&#x2F;strong&gt; (&lt;code&gt;tesseras-core&#x2F;src&#x2F;service.rs&lt;&#x2F;code&gt;) —
+&lt;code&gt;TesseraService::create()&lt;&#x2F;code&gt; now rejects Sealed and Private tesseras that don&#x27;t
+provide encryption keys. This is a domain-level validation: the service layer
+enforces that you cannot create a sealed memory without the cryptographic
+machinery to protect it. The error message is clear: &quot;missing encryption keys
+for visibility sealed until 2050-01-01.&quot;&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Core type updates&lt;&#x2F;strong&gt; — &lt;code&gt;TesseraIdentity&lt;&#x2F;code&gt; now includes an optional
+&lt;code&gt;encryption_public: Option&amp;lt;HybridEncryptionPublic&amp;gt;&lt;&#x2F;code&gt; field containing both the
+X25519 and ML-KEM-768 public keys. &lt;code&gt;KeyAlgorithm&lt;&#x2F;code&gt; gained &lt;code&gt;X25519&lt;&#x2F;code&gt; and &lt;code&gt;MlKem768&lt;&#x2F;code&gt;
+variants. The identity filesystem layout now supports &lt;code&gt;node.x25519.key&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.pub&lt;&#x2F;code&gt;
+and &lt;code&gt;node.mlkem768.key&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;.pub&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 8 unit tests for AES-256-GCM (roundtrip, wrong key, tampered
+ciphertext, wrong AAD, cross-context decryption failure, unique nonces, plus 2
+property-based tests for arbitrary payloads and nonce uniqueness). 5 unit tests
+for HybridKem (roundtrip, wrong keypair, tampered X25519, KDF determinism, plus
+1 property-based test). 4 unit tests for SealedKeyEnvelope and KeyPublication. 2
+integration tests covering the complete sealed and private tessera lifecycle:
+generate keys, create content key, encrypt, seal, unseal, decrypt, publish key,
+and verify — the full cycle.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Hybrid KEM from day one&lt;&#x2F;strong&gt;: X25519 + ML-KEM-768 follows the same philosophy
+as dual signing. We don&#x27;t know which cryptographic assumptions will hold over
+millennia, so we combine classical and post-quantum algorithms. The cost is
+~1.2 KB of additional key material per identity — trivial compared to the
+photos and videos in a tessera.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;BLAKE3 for KDF&lt;&#x2F;strong&gt;: rather than adding &lt;code&gt;hkdf&lt;&#x2F;code&gt; + &lt;code&gt;sha2&lt;&#x2F;code&gt; as new dependencies, we
+use &lt;code&gt;blake3::derive_key&lt;&#x2F;code&gt; with a fixed context string. BLAKE3&#x27;s key derivation
+mode is specifically designed for this use case, and the project already
+depends on BLAKE3 for content hashing.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Immutable manifests&lt;&#x2F;strong&gt;: when a sealed tessera&#x27;s &lt;code&gt;open_after&lt;&#x2F;code&gt; date passes, the
+content key is published as a separate signed artifact (&lt;code&gt;KeyPublication&lt;&#x2F;code&gt;), not
+by modifying the manifest. This preserves the append-only, content-addressed
+nature of tesseras. The manifest was signed at creation time and never
+changes.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;AAD binding prevents ciphertext swapping&lt;&#x2F;strong&gt;: the &lt;code&gt;EncryptionContext&lt;&#x2F;code&gt; binds
+both the content hash and (for sealed tesseras) the &lt;code&gt;open_after&lt;&#x2F;code&gt; timestamp
+into the AES-GCM authenticated data. An attacker who copies encrypted content
+from a &quot;sealed until 2050&quot; tessera into a &quot;sealed until 2025&quot; tessera will
+find that decryption fails — the AAD no longer matches.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;XOR key wrapping&lt;&#x2F;strong&gt;: the sealed key envelope uses a simple XOR of the content
+key with the KEM-derived transport key, rather than an additional layer of
+AES-GCM. Since the transport key is a fresh random value from the KEM and is
+used exactly once, XOR is information-theoretically secure for this specific
+use case and avoids unnecessary complexity.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Domain validation, not storage validation&lt;&#x2F;strong&gt;: the &quot;missing encryption keys&quot;
+check lives in &lt;code&gt;TesseraService::create()&lt;&#x2F;code&gt;, not in the storage layer. This
+follows the hexagonal architecture pattern: domain rules are enforced at the
+service boundary, not scattered across adapters.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4 continued: Resilience and Scale&lt;&#x2F;strong&gt; — Shamir&#x27;s Secret Sharing for heir
+key distribution, advanced NAT traversal (STUN&#x2F;TURN), performance tuning,
+security audits, OS packaging&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — Public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Sealed tesseras make Tesseras a true time capsule. A father can now record a
+message for his unborn grandchild, seal it until 2060, and know that the
+cryptographic envelope will hold — even if the quantum computers of the future
+try to break it open early.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 3: Memories in Your Hands</title>
+ <published>2026-02-14T14:00:00+00:00</published>
+ <updated>2026-02-14T14:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase3-api-and-apps/"/>
+ <id>https://tesseras.net/news/phase3-api-and-apps/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase3-api-and-apps/">&lt;p&gt;People can now hold their memories in their hands. Phase 3 delivers what the
+previous phases built toward: a mobile app where someone downloads Tesseras,
+creates an identity, takes a photo, and that memory enters the preservation
+network. No cloud accounts, no subscriptions, no company between you and your
+memories.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-embedded&lt;&#x2F;strong&gt; — A full P2P node that runs inside a mobile app. The
+&lt;code&gt;EmbeddedNode&lt;&#x2F;code&gt; struct owns a Tokio runtime, SQLite database, QUIC transport,
+Kademlia DHT engine, replication service, and tessera service — the same stack
+as the desktop daemon, compiled into a shared library. A global singleton
+pattern (&lt;code&gt;Mutex&amp;lt;Option&amp;lt;EmbeddedNode&amp;gt;&amp;gt;&lt;&#x2F;code&gt;) ensures one node per app lifecycle. On
+start, it opens the database, runs migrations, loads or generates an Ed25519
+identity with proof-of-work node ID, binds QUIC on an ephemeral port, wires up
+DHT and replication, and spawns the repair loop. On stop, it sends a shutdown
+signal and drains gracefully.&lt;&#x2F;p&gt;
+&lt;p&gt;Eleven FFI functions are exposed to Dart via flutter_rust_bridge: lifecycle
+(&lt;code&gt;node_start&lt;&#x2F;code&gt;, &lt;code&gt;node_stop&lt;&#x2F;code&gt;, &lt;code&gt;node_is_running&lt;&#x2F;code&gt;), identity (&lt;code&gt;create_identity&lt;&#x2F;code&gt;,
+&lt;code&gt;get_identity&lt;&#x2F;code&gt;), memories (&lt;code&gt;create_memory&lt;&#x2F;code&gt;, &lt;code&gt;get_timeline&lt;&#x2F;code&gt;, &lt;code&gt;get_memory&lt;&#x2F;code&gt;), and
+network status (&lt;code&gt;get_network_stats&lt;&#x2F;code&gt;, &lt;code&gt;get_replication_status&lt;&#x2F;code&gt;). All types
+crossing the FFI boundary are flat structs with only &lt;code&gt;String&lt;&#x2F;code&gt;, &lt;code&gt;Option&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;,
+&lt;code&gt;Vec&amp;lt;String&amp;gt;&lt;&#x2F;code&gt;, and primitives — no trait objects, no generics, no lifetimes.&lt;&#x2F;p&gt;
+&lt;p&gt;Four adapter modules bridge core ports to concrete implementations:
+&lt;code&gt;Blake3HasherAdapter&lt;&#x2F;code&gt;, &lt;code&gt;Ed25519SignerAdapter&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;Ed25519VerifierAdapter&lt;&#x2F;code&gt; for
+cryptography, &lt;code&gt;DhtPortAdapter&lt;&#x2F;code&gt; for DHT operations, and
+&lt;code&gt;ReplicationHandlerAdapter&lt;&#x2F;code&gt; for incoming fragment and attestation RPCs.&lt;&#x2F;p&gt;
+&lt;p&gt;The &lt;code&gt;bundled-sqlite&lt;&#x2F;code&gt; feature flag compiles SQLite from source, required for
+Android and iOS where the system library may not be available. Cargokit
+configuration passes this flag automatically in both debug and release builds.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Flutter app&lt;&#x2F;strong&gt; — A Material Design 3 application with Riverpod state
+management, targeting Android, iOS, Linux, macOS, and Windows from a single
+codebase.&lt;&#x2F;p&gt;
+&lt;p&gt;The &lt;em&gt;onboarding flow&lt;&#x2F;em&gt; is three screens: a welcome screen explaining the project
+in one sentence (&quot;Preserve your memories across millennia. No cloud. No
+company.&quot;), an identity creation screen that triggers Ed25519 keypair generation
+in Rust, and a confirmation screen showing the user&#x27;s name and cryptographic
+identity.&lt;&#x2F;p&gt;
+&lt;p&gt;The &lt;em&gt;timeline screen&lt;&#x2F;em&gt; displays memories in reverse chronological order with
+image previews, context text, and chips for memory type and visibility.
+Pull-to-refresh reloads from the Rust node. A floating action button opens the
+&lt;em&gt;memory creation screen&lt;&#x2F;em&gt;, which supports photo selection from gallery or camera
+via &lt;code&gt;image_picker&lt;&#x2F;code&gt;, optional context text, memory type and visibility dropdowns,
+and comma-separated tags. Creating a memory calls the Rust FFI synchronously,
+then returns to the timeline.&lt;&#x2F;p&gt;
+&lt;p&gt;The &lt;em&gt;network screen&lt;&#x2F;em&gt; shows two cards: node status (peer count, DHT size,
+bootstrap state, uptime) and replication health (total fragments, healthy
+fragments, repairing fragments, replication factor). The &lt;em&gt;settings screen&lt;&#x2F;em&gt;
+displays the user&#x27;s identity — name, truncated node ID, truncated public key,
+and creation date.&lt;&#x2F;p&gt;
+&lt;p&gt;Three Riverpod providers manage state: &lt;code&gt;nodeProvider&lt;&#x2F;code&gt; starts the embedded node
+on app launch using the app documents directory and stops it on dispose;
+&lt;code&gt;identityProvider&lt;&#x2F;code&gt; loads the existing profile or creates a new one;
+&lt;code&gt;timelineProvider&lt;&#x2F;code&gt; fetches the memory list with pagination.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 9 Rust unit tests in tesseras-embedded covering node lifecycle
+(start&#x2F;stop without panic), identity persistence across restarts, restart cycles
+without SQLite corruption, network event streaming, stats retrieval, memory
+creation and timeline retrieval, and single memory lookup by hash. 2 Flutter
+tests: an integration test verifying Rust initialization and app startup, and a
+widget smoke test.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Embedded node, not client-server&lt;&#x2F;strong&gt;: the phone runs the full P2P stack, not a
+thin client talking to a remote daemon. This means memories are preserved even
+without internet. Users with a Raspberry Pi or VPS can optionally connect the
+app to their daemon via GraphQL for higher availability, but it&#x27;s not
+required.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Synchronous FFI&lt;&#x2F;strong&gt;: all flutter_rust_bridge functions are marked
+&lt;code&gt;#[frb(sync)]&lt;&#x2F;code&gt; and block on the internal Tokio runtime. This simplifies the
+Dart side (no async bridge complexity) while the Rust side handles concurrency
+internally. Flutter&#x27;s UI thread stays responsive because Riverpod wraps calls
+in async providers.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Global singleton&lt;&#x2F;strong&gt;: a &lt;code&gt;Mutex&amp;lt;Option&amp;lt;EmbeddedNode&amp;gt;&amp;gt;&lt;&#x2F;code&gt; global ensures the node
+lifecycle is predictable — one start, one stop, no races. Mobile platforms
+kill processes aggressively, so simplicity in lifecycle management is a
+feature.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Flat FFI types&lt;&#x2F;strong&gt;: no Rust abstractions leak across the FFI boundary. Every
+type is a plain struct with strings and numbers. This makes the auto-generated
+Dart bindings reliable and easy to debug.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Three-screen onboarding&lt;&#x2F;strong&gt;: identity creation is the only required step. No
+email, no password, no server registration. The app generates a cryptographic
+identity locally and is ready to use.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4: Resilience and Scale&lt;&#x2F;strong&gt; — Advanced NAT traversal (STUN&#x2F;TURN),
+Shamir&#x27;s Secret Sharing for heirs, sealed tesseras with time-lock encryption,
+performance tuning, security audits, OS packaging for
+Alpine&#x2F;Arch&#x2F;Debian&#x2F;FreeBSD&#x2F;OpenBSD&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — Public tessera browser by
+era&#x2F;location&#x2F;theme&#x2F;language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;The infrastructure is complete. The network exists, replication works, and now
+anyone with a phone can participate. What remains is hardening what we have and
+opening it to the world.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Reed-Solomon: How Tesseras Survives Data Loss</title>
+ <published>2026-02-14T14:00:00+00:00</published>
+ <updated>2026-02-14T14:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/reed-solomon/"/>
+ <id>https://tesseras.net/news/reed-solomon/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/reed-solomon/">&lt;p&gt;Your hard drive will die. Your cloud provider will pivot. The RAID array in your
+closet will outlive its controller but not its owner. If a memory is stored in
+exactly one place, it has exactly one way to be lost forever.&lt;&#x2F;p&gt;
+&lt;p&gt;Tesseras is a network that keeps human memories alive through mutual aid. The
+core survival mechanism is &lt;strong&gt;Reed-Solomon erasure coding&lt;&#x2F;strong&gt; — a technique
+borrowed from deep-space communication that lets us reconstruct data even when
+pieces go missing.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-is-reed-solomon&quot;&gt;What is Reed-Solomon?&lt;&#x2F;h2&gt;
+&lt;p&gt;Reed-Solomon is a family of error-correcting codes invented by Irving Reed and
+Gustave Solomon in 1960. The original use case was correcting errors in data
+transmitted over noisy channels — think Voyager sending photos from Jupiter, or
+a CD playing despite scratches.&lt;&#x2F;p&gt;
+&lt;p&gt;The key insight: if you add carefully computed redundancy to your data &lt;em&gt;before&lt;&#x2F;em&gt;
+something goes wrong, you can recover the original even after losing some
+pieces.&lt;&#x2F;p&gt;
+&lt;p&gt;Here&#x27;s the intuition. Suppose you have a polynomial of degree 2 — a parabola.
+You need 3 points to define it uniquely. But if you evaluate it at 5 points, you
+can lose any 2 of those 5 and still reconstruct the polynomial from the
+remaining 3. Reed-Solomon generalizes this idea to work over finite fields
+(Galois fields), where the &quot;polynomial&quot; is your data and the &quot;evaluation points&quot;
+are your fragments.&lt;&#x2F;p&gt;
+&lt;p&gt;In concrete terms:&lt;&#x2F;p&gt;
+&lt;ol&gt;
+&lt;li&gt;&lt;strong&gt;Split&lt;&#x2F;strong&gt; your data into &lt;em&gt;k&lt;&#x2F;em&gt; data shards&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Compute&lt;&#x2F;strong&gt; &lt;em&gt;m&lt;&#x2F;em&gt; parity shards from the data shards&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Distribute&lt;&#x2F;strong&gt; all &lt;em&gt;k + m&lt;&#x2F;em&gt; shards across different locations&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Reconstruct&lt;&#x2F;strong&gt; the original data from any &lt;em&gt;k&lt;&#x2F;em&gt; of the &lt;em&gt;k + m&lt;&#x2F;em&gt; shards&lt;&#x2F;li&gt;
+&lt;&#x2F;ol&gt;
+&lt;p&gt;You can lose up to &lt;em&gt;m&lt;&#x2F;em&gt; shards — any &lt;em&gt;m&lt;&#x2F;em&gt;, data or parity, in any combination —
+and still recover everything.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;why-not-just-make-copies&quot;&gt;Why not just make copies?&lt;&#x2F;h2&gt;
+&lt;p&gt;The naive approach to redundancy is replication: make 3 copies, store them in 3
+places. This gives you tolerance for 2 failures at the cost of 3x your storage.&lt;&#x2F;p&gt;
+&lt;p&gt;Reed-Solomon is dramatically more efficient:&lt;&#x2F;p&gt;
+&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Strategy&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Storage overhead&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Failures tolerated&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
+&lt;tr&gt;&lt;td&gt;3x replication&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;200%&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;2 out of 3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Reed-Solomon (16,8)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;50%&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;8 out of 24&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;tr&gt;&lt;td&gt;Reed-Solomon (48,24)&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;50%&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;24 out of 72&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
+&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
+&lt;p&gt;With 16 data shards and 8 parity shards, you use 50% extra storage but can
+survive losing a third of all fragments. To achieve the same fault tolerance
+with replication alone, you&#x27;d need 3x the storage.&lt;&#x2F;p&gt;
+&lt;p&gt;For a network that aims to preserve memories across decades and centuries, this
+efficiency isn&#x27;t a nice-to-have — it&#x27;s the difference between a viable system
+and one that drowns in its own overhead.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;how-tesseras-uses-reed-solomon&quot;&gt;How Tesseras uses Reed-Solomon&lt;&#x2F;h2&gt;
+&lt;p&gt;Not all data deserves the same treatment. A 500-byte text memory and a 100 MB
+video have very different redundancy needs. Tesseras uses a three-tier
+fragmentation strategy:&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Small (&amp;lt; 4 MB)&lt;&#x2F;strong&gt; — Whole-file replication to 7 peers. For small tesseras, the
+overhead of erasure coding (encoding time, fragment management, reconstruction
+logic) outweighs its benefits. Simple copies are faster and simpler.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Medium (4–256 MB)&lt;&#x2F;strong&gt; — 16 data shards + 8 parity shards = 24 total fragments.
+Each fragment is roughly 1&#x2F;16th of the original size. Any 16 of the 24 fragments
+reconstruct the original. Distributed across 7 peers.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Large (≥ 256 MB)&lt;&#x2F;strong&gt; — 48 data shards + 24 parity shards = 72 total fragments.
+Higher shard count means smaller individual fragments (easier to transfer and
+store) and higher absolute fault tolerance. Also distributed across 7 peers.&lt;&#x2F;p&gt;
+&lt;p&gt;The implementation uses the &lt;code&gt;reed-solomon-erasure&lt;&#x2F;code&gt; crate operating over GF(2⁸) —
+the same Galois field used in QR codes and CDs. Each fragment carries a BLAKE3
+checksum so corruption is detected immediately, not silently propagated.&lt;&#x2F;p&gt;
+&lt;pre&gt;&lt;code&gt;Tessera (120 MB photo album)
+ ↓ encode
+16 data shards (7.5 MB each) + 8 parity shards (7.5 MB each)
+ ↓ distribute
+24 fragments across 7 peers (subnet-diverse)
+ ↓ any 16 fragments
+Original tessera recovered
+&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
+&lt;h2 id=&quot;the-challenges&quot;&gt;The challenges&lt;&#x2F;h2&gt;
+&lt;p&gt;Reed-Solomon solves the mathematical problem of redundancy. The engineering
+challenges are everything around it.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;fragment-tracking&quot;&gt;Fragment tracking&lt;&#x2F;h3&gt;
+&lt;p&gt;Every fragment needs to be findable. Tesseras uses a Kademlia DHT for peer
+discovery and fragment-to-peer mapping. When a node goes offline, its fragments
+need to be re-created and distributed to new peers. This means tracking which
+fragments exist, where they are, and whether they&#x27;re still intact — across a
+network with no central authority.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;silent-corruption&quot;&gt;Silent corruption&lt;&#x2F;h3&gt;
+&lt;p&gt;A fragment that returns wrong data is worse than one that&#x27;s missing — at least a
+missing fragment is honestly absent. Tesseras addresses this with
+attestation-based health checks: the repair loop periodically asks fragment
+holders to prove possession by returning BLAKE3 checksums. If a checksum doesn&#x27;t
+match, the fragment is treated as lost.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;correlated-failures&quot;&gt;Correlated failures&lt;&#x2F;h3&gt;
+&lt;p&gt;If all 24 fragments of a tessera land on machines in the same datacenter, a
+single power outage kills them all. Reed-Solomon&#x27;s math assumes independent
+failures. Tesseras enforces &lt;strong&gt;subnet diversity&lt;&#x2F;strong&gt; during distribution: no more
+than 2 fragments per &#x2F;24 IPv4 subnet (or &#x2F;48 IPv6 prefix). This spreads
+fragments across different physical infrastructure.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;repair-speed-vs-network-load&quot;&gt;Repair speed vs. network load&lt;&#x2F;h3&gt;
+&lt;p&gt;When a peer goes offline, the clock starts ticking. Lost fragments need to be
+re-created before more failures accumulate. But aggressive repair floods the
+network. Tesseras balances this with a configurable repair loop (default: every
+24 hours with 2-hour jitter) and concurrent transfer limits (default: 4
+simultaneous transfers). The jitter prevents repair storms where every node
+checks its fragments at the same moment.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;long-term-key-management&quot;&gt;Long-term key management&lt;&#x2F;h3&gt;
+&lt;p&gt;Reed-Solomon protects against data loss, not against losing access. If a tessera
+is encrypted (private or sealed visibility), you need the decryption key to make
+the recovered data useful. Tesseras separates these concerns: erasure coding
+handles availability, while Shamir&#x27;s Secret Sharing (a future phase) will handle
+key distribution among heirs. The project&#x27;s design philosophy — encrypt as
+little as possible — keeps the key management problem small.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;galois-field-limitations&quot;&gt;Galois field limitations&lt;&#x2F;h3&gt;
+&lt;p&gt;The GF(2⁸) field limits the total number of shards to 255 (data + parity
+combined). For Tesseras, this is not a practical constraint — even the Large
+tier uses only 72 shards. But it does mean that extremely large files with
+thousands of fragments would require either a different field or a layered
+encoding scheme.&lt;&#x2F;p&gt;
+&lt;h3 id=&quot;evolving-codec-compatibility&quot;&gt;Evolving codec compatibility&lt;&#x2F;h3&gt;
+&lt;p&gt;A tessera encoded today must be decodable in 50 years. Reed-Solomon over GF(2⁸)
+is one of the most widely implemented algorithms in computing — it&#x27;s in every CD
+player, every QR code scanner, every deep-space probe. This ubiquity is itself a
+survival strategy. The algorithm won&#x27;t be forgotten because half the world&#x27;s
+infrastructure depends on it.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;the-bigger-picture&quot;&gt;The bigger picture&lt;&#x2F;h2&gt;
+&lt;p&gt;Reed-Solomon is a piece of a larger puzzle. It works in concert with:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Kademlia DHT&lt;&#x2F;strong&gt; for finding peers and routing fragments&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;BLAKE3 checksums&lt;&#x2F;strong&gt; for integrity verification&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Bilateral reciprocity&lt;&#x2F;strong&gt; for fair storage exchange (no blockchain needed)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Subnet diversity&lt;&#x2F;strong&gt; for failure independence&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Automatic repair&lt;&#x2F;strong&gt; for maintaining redundancy over time&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;No single technique makes memories survive. Reed-Solomon ensures that data &lt;em&gt;can&lt;&#x2F;em&gt;
+be recovered. The DHT ensures fragments &lt;em&gt;can be found&lt;&#x2F;em&gt;. Reciprocity ensures
+peers &lt;em&gt;want to help&lt;&#x2F;em&gt;. Repair ensures none of this degrades over time.&lt;&#x2F;p&gt;
+&lt;p&gt;A tessera is a bet that the sum of these mechanisms, running across many
+independent machines operated by many independent people, is more durable than
+any single institution. Reed-Solomon is the mathematical foundation of that bet.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 2: Memories Survive</title>
+ <published>2026-02-14T12:00:00+00:00</published>
+ <updated>2026-02-14T12:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase2-replication/"/>
+ <id>https://tesseras.net/news/phase2-replication/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase2-replication/">&lt;p&gt;A tessera is no longer tied to a single machine. Phase 2 delivers the
+replication layer: data is split into erasure-coded fragments, distributed
+across multiple peers, and automatically repaired when nodes go offline. A
+bilateral reciprocity ledger ensures fair storage exchange — no blockchain, no
+tokens.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-core&lt;&#x2F;strong&gt; (updated) — New replication domain types: &lt;code&gt;FragmentPlan&lt;&#x2F;code&gt;
+(selects fragmentation tier based on tessera size), &lt;code&gt;FragmentId&lt;&#x2F;code&gt; (tessera hash +
+index + shard count + checksum), &lt;code&gt;FragmentEnvelope&lt;&#x2F;code&gt; (fragment with its metadata
+for wire transport), &lt;code&gt;FragmentationTier&lt;&#x2F;code&gt; (Small&#x2F;Medium&#x2F;Large), &lt;code&gt;Attestation&lt;&#x2F;code&gt;
+(proof that a node holds a fragment at a given time), and &lt;code&gt;ReplicateAck&lt;&#x2F;code&gt;
+(acknowledgement of fragment receipt). Three new port traits define the
+hexagonal boundaries: &lt;code&gt;DhtPort&lt;&#x2F;code&gt; (find peers, replicate fragments, request
+attestations, ping), &lt;code&gt;FragmentStore&lt;&#x2F;code&gt; (store&#x2F;read&#x2F;delete&#x2F;list&#x2F;verify fragments),
+and &lt;code&gt;ReciprocityLedger&lt;&#x2F;code&gt; (record storage exchanges, query balances, find best
+peers). Maximum tessera size is 1 GB.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-crypto&lt;&#x2F;strong&gt; (updated) — The existing &lt;code&gt;ReedSolomonCoder&lt;&#x2F;code&gt; now powers
+fragment encoding. Data is split into shards, parity shards are computed, and
+any combination of data shards can reconstruct the original — as long as the
+number of missing shards does not exceed the parity count.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-storage&lt;&#x2F;strong&gt; (updated) — Two new adapters:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;FsFragmentStore&lt;&#x2F;code&gt; — stores fragment data as files on disk
+(&lt;code&gt;{root}&#x2F;{tessera_hash}&#x2F;{index:03}.shard&lt;&#x2F;code&gt;) with a SQLite metadata index
+tracking tessera hash, shard index, shard count, checksum, and byte size.
+Verification recomputes the BLAKE3 hash and compares it to the stored
+checksum.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;SqliteReciprocityLedger&lt;&#x2F;code&gt; — bilateral storage accounting in SQLite. Each peer
+has a row tracking bytes stored for them and bytes they store for us. The
+&lt;code&gt;balance&lt;&#x2F;code&gt; column is a generated column
+(&lt;code&gt;bytes_they_store_for_us - bytes_stored_for_them&lt;&#x2F;code&gt;). UPSERT ensures atomic
+increment of counters.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;New migration (&lt;code&gt;002_replication.sql&lt;&#x2F;code&gt;) adds tables for fragments, fragment plans,
+holders, holder-fragment mappings, and reciprocity balances.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-dht&lt;&#x2F;strong&gt; (updated) — Four new message variants: &lt;code&gt;Replicate&lt;&#x2F;code&gt; (send a
+fragment envelope), &lt;code&gt;ReplicateAck&lt;&#x2F;code&gt; (confirm receipt), &lt;code&gt;AttestRequest&lt;&#x2F;code&gt; (ask a
+node to prove it holds a tessera&#x27;s fragments), and &lt;code&gt;AttestResponse&lt;&#x2F;code&gt; (return
+attestation with checksums and timestamp). The engine handles these in its
+message dispatch loop.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-replication&lt;&#x2F;strong&gt; — The new crate, with five modules:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;
+&lt;p&gt;&lt;em&gt;Fragment encoding&lt;&#x2F;em&gt; (&lt;code&gt;fragment.rs&lt;&#x2F;code&gt;): &lt;code&gt;encode_tessera()&lt;&#x2F;code&gt; selects the
+fragmentation tier based on size, then calls Reed-Solomon encoding for Medium
+and Large tiers. Three tiers:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Small&lt;&#x2F;strong&gt; (&amp;lt; 4 MB): whole-file replication to r=7 peers, no erasure coding&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Medium&lt;&#x2F;strong&gt; (4–256 MB): 16 data + 8 parity shards, distributed across r=7
+peers&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Large&lt;&#x2F;strong&gt; (≥ 256 MB): 48 data + 24 parity shards, distributed across r=7
+peers&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;&#x2F;li&gt;
+&lt;li&gt;
+&lt;p&gt;&lt;em&gt;Distribution&lt;&#x2F;em&gt; (&lt;code&gt;distributor.rs&lt;&#x2F;code&gt;): subnet diversity filtering limits peers per
+&#x2F;24 IPv4 subnet (or &#x2F;48 IPv6 prefix) to avoid correlated failures. If all your
+fragments land on the same rack, a single power outage kills them all.&lt;&#x2F;p&gt;
+&lt;&#x2F;li&gt;
+&lt;li&gt;
+&lt;p&gt;&lt;em&gt;Service&lt;&#x2F;em&gt; (&lt;code&gt;service.rs&lt;&#x2F;code&gt;): &lt;code&gt;ReplicationService&lt;&#x2F;code&gt; is the orchestrator.
+&lt;code&gt;replicate_tessera()&lt;&#x2F;code&gt; encodes the data, finds the closest peers via DHT,
+applies subnet diversity, and distributes fragments round-robin.
+&lt;code&gt;receive_fragment()&lt;&#x2F;code&gt; validates the BLAKE3 checksum, checks reciprocity balance
+(rejects if the sender&#x27;s deficit exceeds the configured threshold), stores the
+fragment, and updates the ledger. &lt;code&gt;handle_attestation_request()&lt;&#x2F;code&gt; lists local
+fragments and computes their checksums as proof of possession.&lt;&#x2F;p&gt;
+&lt;&#x2F;li&gt;
+&lt;li&gt;
+&lt;p&gt;&lt;em&gt;Repair&lt;&#x2F;em&gt; (&lt;code&gt;repair.rs&lt;&#x2F;code&gt;): &lt;code&gt;check_tessera_health()&lt;&#x2F;code&gt; requests attestations from
+known holders, falls back to ping for unresponsive nodes, verifies local
+fragment integrity, and returns one of three actions: &lt;code&gt;Healthy&lt;&#x2F;code&gt;,
+&lt;code&gt;NeedsReplication { deficit }&lt;&#x2F;code&gt;, or &lt;code&gt;CorruptLocal { fragment_index }&lt;&#x2F;code&gt;. The
+repair loop runs every 24 hours (with 2-hour jitter) via &lt;code&gt;tokio::select!&lt;&#x2F;code&gt; with
+shutdown integration.&lt;&#x2F;p&gt;
+&lt;&#x2F;li&gt;
+&lt;li&gt;
+&lt;p&gt;&lt;em&gt;Configuration&lt;&#x2F;em&gt; (&lt;code&gt;config.rs&lt;&#x2F;code&gt;): &lt;code&gt;ReplicationConfig&lt;&#x2F;code&gt; with defaults for repair
+interval (24h), jitter (2h), concurrent transfers (4), minimum free space (1
+GB), deficit allowance (256 MB), and per-peer storage limit (1 GB).&lt;&#x2F;p&gt;
+&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;tesd&lt;&#x2F;strong&gt; (updated) — The daemon now opens a SQLite database (&lt;code&gt;db&#x2F;tesseras.db&lt;&#x2F;code&gt;),
+runs migrations, creates &lt;code&gt;FsFragmentStore&lt;&#x2F;code&gt;, &lt;code&gt;SqliteReciprocityLedger&lt;&#x2F;code&gt;, and
+&lt;code&gt;FsBlobStore&lt;&#x2F;code&gt; instances, wraps the DHT engine in a &lt;code&gt;DhtPortAdapter&lt;&#x2F;code&gt;, builds a
+&lt;code&gt;ReplicationService&lt;&#x2F;code&gt;, and spawns the repair loop as a background task with
+graceful shutdown.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 193 tests across the workspace:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;15 unit tests in tesseras-replication (fragment encoding tiers, checksum
+validation, subnet diversity, repair health checks, service receive&#x2F;replicate
+flows)&lt;&#x2F;li&gt;
+&lt;li&gt;3 integration tests with real storage (full encode→distribute→receive cycle
+for medium tessera, small whole-file replication, tampered fragment rejection)&lt;&#x2F;li&gt;
+&lt;li&gt;Tests use in-memory SQLite + tempdir fragments with mockall mocks for DHT and
+BlobStore&lt;&#x2F;li&gt;
+&lt;li&gt;Zero clippy warnings, clean formatting&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Three-tier fragmentation&lt;&#x2F;strong&gt;: small files don&#x27;t need erasure coding — the
+overhead isn&#x27;t worth it. Medium and large files get progressively more parity
+shards. This avoids wasting storage on small tesseras while providing strong
+redundancy for large ones.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Owner-push distribution&lt;&#x2F;strong&gt;: the tessera owner encodes fragments and pushes
+them to peers, rather than peers pulling. This simplifies the protocol (no
+negotiation phase) and ensures fragments are distributed immediately.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Bilateral reciprocity without consensus&lt;&#x2F;strong&gt;: each node tracks its own balance
+with each peer locally. No global ledger, no token, no blockchain. If peer A
+stores 500 MB for peer B, peer B should store roughly 500 MB for peer A. Free
+riders lose redundancy gradually — their fragments are deprioritized for
+repair, but never deleted.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Subnet diversity&lt;&#x2F;strong&gt;: fragments are spread across different network subnets to
+survive correlated failures. A datacenter outage shouldn&#x27;t take out all copies
+of a tessera.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Attestation-first health checks&lt;&#x2F;strong&gt;: the repair loop asks holders to prove
+possession (attestation with checksums) before declaring a tessera degraded.
+Only when attestation fails does it fall back to a simple ping. This catches
+silent data corruption, not just node departure.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 3: API and Apps&lt;&#x2F;strong&gt; — Flutter mobile&#x2F;desktop app via
+flutter_rust_bridge, GraphQL API (async-graphql), WASM browser node&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4: Resilience and Scale&lt;&#x2F;strong&gt; — ML-DSA post-quantum signatures, advanced
+NAT traversal, Shamir&#x27;s Secret Sharing for heirs, packaging for
+Alpine&#x2F;Arch&#x2F;Debian&#x2F;FreeBSD&#x2F;OpenBSD, CI on SourceHut&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser, institutional
+curation, genealogy integration, physical media export&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Nodes can find each other and keep each other&#x27;s memories alive. Next, we give
+people a way to hold their memories in their hands.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 1: Nodes Find Each Other</title>
+ <published>2026-02-14T11:00:00+00:00</published>
+ <updated>2026-02-14T11:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase1-basic-network/"/>
+ <id>https://tesseras.net/news/phase1-basic-network/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase1-basic-network/">&lt;p&gt;Tesseras is no longer a local-only tool. Phase 1 delivers the networking layer:
+nodes discover each other through a Kademlia DHT, communicate over QUIC, and
+publish tessera pointers that any peer on the network can find. A tessera
+created on node A is now findable from node C.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-core&lt;&#x2F;strong&gt; (updated) — New network domain types: &lt;code&gt;TesseraPointer&lt;&#x2F;code&gt;
+(lightweight reference to a tessera&#x27;s holders and fragment locations),
+&lt;code&gt;NodeIdentity&lt;&#x2F;code&gt; (node ID + public key + proof-of-work nonce), &lt;code&gt;NodeInfo&lt;&#x2F;code&gt;
+(identity + address + capabilities), and &lt;code&gt;Capabilities&lt;&#x2F;code&gt; (bitflags for what a
+node supports: DHT, storage, relay, replication).&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-net&lt;&#x2F;strong&gt; — The transport layer, built on QUIC via quinn. The &lt;code&gt;Transport&lt;&#x2F;code&gt;
+trait defines the port: &lt;code&gt;send&lt;&#x2F;code&gt;, &lt;code&gt;recv&lt;&#x2F;code&gt;, &lt;code&gt;disconnect&lt;&#x2F;code&gt;, &lt;code&gt;local_addr&lt;&#x2F;code&gt;. Two adapters
+implement it:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;QuinnTransport&lt;&#x2F;code&gt; — real QUIC with self-signed TLS, ALPN negotiation
+(&lt;code&gt;tesseras&#x2F;1&lt;&#x2F;code&gt;), connection pooling via DashMap, and a background accept loop
+that handles incoming streams.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;MemTransport&lt;&#x2F;code&gt; + &lt;code&gt;SimNetwork&lt;&#x2F;code&gt; — in-memory channels for deterministic testing
+without network I&#x2F;O. Every integration test in the DHT crate runs against
+this.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;The wire protocol uses length-prefixed MessagePack: a 4-byte big-endian length
+header followed by an rmp-serde payload. &lt;code&gt;WireMessage&lt;&#x2F;code&gt; carries a version byte,
+request ID, and a body that can be a request, response, or protocol-level error.
+Maximum message size is 64 KiB.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-dht&lt;&#x2F;strong&gt; — A complete Kademlia implementation:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;em&gt;Routing table&lt;&#x2F;em&gt;: 160 k-buckets with k=20. Least-recently-seen eviction,
+move-to-back on update, ping-check before replacing a full bucket&#x27;s oldest
+entry.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;em&gt;XOR distance&lt;&#x2F;em&gt;: 160-bit XOR metric with bucket indexing by highest differing
+bit.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;em&gt;Proof-of-work&lt;&#x2F;em&gt;: nodes grind a nonce until &lt;code&gt;BLAKE3(pubkey || nonce)[..20]&lt;&#x2F;code&gt; has
+8 leading zero bits (~256 hash attempts on average). Cheap enough for any
+device, expensive enough to make Sybil attacks impractical at scale.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;em&gt;Protocol messages&lt;&#x2F;em&gt;: Ping&#x2F;Pong, FindNode&#x2F;FindNodeResponse,
+FindValue&#x2F;FindValueResult, Store — all serialized with MessagePack via serde.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;em&gt;Pointer store&lt;&#x2F;em&gt;: bounded in-memory store with configurable TTL (24 hours
+default) and max entries (10,000 default). When full, evicts pointers furthest
+from the local node ID, following Kademlia&#x27;s distance-based responsibility
+model.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;em&gt;DhtEngine&lt;&#x2F;em&gt;: the main orchestrator. Handles incoming RPCs, runs iterative
+lookups (alpha=3 parallelism), bootstrap, publish, and find. The &lt;code&gt;run()&lt;&#x2F;code&gt;
+method drives a &lt;code&gt;tokio::select!&lt;&#x2F;code&gt; loop with maintenance timers: routing table
+refresh every 60 seconds, pointer expiry every 5 minutes.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;tesd&lt;&#x2F;strong&gt; — A full-node binary. Parses CLI args (bind address, bootstrap peers,
+data directory), generates a PoW-valid node identity, binds a QUIC endpoint,
+bootstraps into the network, and runs the DHT engine. Graceful shutdown on
+Ctrl+C via tokio signal handling.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Infrastructure&lt;&#x2F;strong&gt; — OpenTofu configuration for two Hetzner Cloud bootstrap
+nodes (cx22 instances in Falkenstein, Germany and Helsinki, Finland). Cloud-init
+provisioning script creates a dedicated &lt;code&gt;tesseras&lt;&#x2F;code&gt; user, writes a config file,
+and sets up a systemd service. Firewall rules open UDP 4433 (QUIC) and restrict
+metrics to internal access.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 139 tests across the workspace:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;47 unit tests in tesseras-dht (routing table, distance, PoW, pointer store,
+message serialization, engine RPCs)&lt;&#x2F;li&gt;
+&lt;li&gt;5 multi-node integration tests (3-node bootstrap, 10-node lookup convergence,
+publish-and-find, node departure detection, PoW rejection)&lt;&#x2F;li&gt;
+&lt;li&gt;14 tests in tesseras-net (codec roundtrips, transport send&#x2F;recv, backpressure,
+disconnect)&lt;&#x2F;li&gt;
+&lt;li&gt;Docker Compose smoke tests with 3 containerized nodes communicating over real
+QUIC&lt;&#x2F;li&gt;
+&lt;li&gt;Zero clippy warnings, clean formatting&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Transport as a port&lt;&#x2F;strong&gt;: the &lt;code&gt;Transport&lt;&#x2F;code&gt; trait is the only interface between
+the DHT engine and the network. Swapping QUIC for any other protocol means
+implementing four methods. All DHT tests use the in-memory adapter, making
+them fast and deterministic.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;One stream per RPC&lt;&#x2F;strong&gt;: each DHT request-response pair uses a fresh
+bidirectional QUIC stream. No multiplexing complexity, no head-of-line
+blocking between independent operations. QUIC handles the multiplexing at the
+connection level.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;MessagePack over Protobuf&lt;&#x2F;strong&gt;: compact binary encoding without code generation
+or schema files. Serde integration means adding a field to a message is a
+one-line change. Trade-off: no built-in schema evolution guarantees, but at
+this stage velocity matters more.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;PoW instead of stake or reputation&lt;&#x2F;strong&gt;: a node identity costs ~256 BLAKE3
+hashes. This runs in under a second on any hardware, including a Raspberry Pi,
+but generating thousands of identities for a Sybil attack becomes expensive.
+No tokens, no blockchain, no external dependencies.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Iterative lookup with routing table updates&lt;&#x2F;strong&gt;: discovered nodes are added to
+the routing table as they&#x27;re encountered during iterative lookups, following
+standard Kademlia behavior. This ensures the routing table improves
+organically as nodes interact.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 2: Replication&lt;&#x2F;strong&gt; — Reed-Solomon erasure coding over the network,
+fragment distribution, automatic repair loops, bilateral reciprocity ledger
+(no blockchain, no tokens)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 3: API and Apps&lt;&#x2F;strong&gt; — Flutter mobile&#x2F;desktop app via
+flutter_rust_bridge, GraphQL API (async-graphql), WASM browser node&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4: Resilience and Scale&lt;&#x2F;strong&gt; — ML-DSA post-quantum signatures, advanced
+NAT traversal, Shamir&#x27;s Secret Sharing for heirs, packaging for
+Alpine&#x2F;Arch&#x2F;Debian&#x2F;FreeBSD&#x2F;OpenBSD, CI on SourceHut&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 5: Exploration and Culture&lt;&#x2F;strong&gt; — public tessera browser, institutional
+curation, genealogy integration, physical media export&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;Nodes can find each other. Next, they learn to keep each other&#x27;s memories alive.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Phase 0: Foundation Laid</title>
+ <published>2026-02-14T10:00:00+00:00</published>
+ <updated>2026-02-14T10:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/phase0-foundation/"/>
+ <id>https://tesseras.net/news/phase0-foundation/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/phase0-foundation/">&lt;p&gt;The first milestone of the Tesseras project is complete. Phase 0 establishes the
+foundation that every future component will build on: domain types,
+cryptography, storage, and a usable command-line interface.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;what-was-built&quot;&gt;What was built&lt;&#x2F;h2&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-core&lt;&#x2F;strong&gt; — The domain layer defines the tessera format: &lt;code&gt;ContentHash&lt;&#x2F;code&gt;
+(BLAKE3, 32 bytes), &lt;code&gt;NodeId&lt;&#x2F;code&gt; (Kademlia, 20 bytes), memory types (Moment,
+Reflection, Daily, Relation, Object), visibility modes (Private, Circle, Public,
+PublicAfterDeath, Sealed), and a plain-text manifest format that can be parsed
+by any programming language for the next thousand years. The application service
+layer (&lt;code&gt;TesseraService&lt;&#x2F;code&gt;) handles create, verify, export, and list operations
+through port traits, following hexagonal architecture.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-crypto&lt;&#x2F;strong&gt; — Ed25519 key generation, signing, and verification. A
+dual-signature framework (Ed25519 + ML-DSA placeholder) ready for post-quantum
+migration. BLAKE3 content hashing. Reed-Solomon erasure coding behind a feature
+flag for future replication.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-storage&lt;&#x2F;strong&gt; — SQLite index via rusqlite with plain-SQL migrations.
+Filesystem blob store with content-addressable layout
+(&lt;code&gt;blobs&#x2F;&amp;lt;tessera_hash&amp;gt;&#x2F;&amp;lt;memory_hash&amp;gt;&#x2F;&amp;lt;filename&amp;gt;&lt;&#x2F;code&gt;). Identity key persistence on
+disk.&lt;&#x2F;p&gt;
+&lt;p&gt;&lt;strong&gt;tesseras-cli&lt;&#x2F;strong&gt; — A working &lt;code&gt;tesseras&lt;&#x2F;code&gt; binary with five commands:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;code&gt;init&lt;&#x2F;code&gt; — generates Ed25519 identity, creates SQLite database&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;create &amp;lt;dir&amp;gt;&lt;&#x2F;code&gt; — scans a directory for media files, creates a signed tessera&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;verify &amp;lt;hash&amp;gt;&lt;&#x2F;code&gt; — checks signature and file integrity&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;export &amp;lt;hash&amp;gt; &amp;lt;dest&amp;gt;&lt;&#x2F;code&gt; — writes a self-contained tessera directory&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;code&gt;list&lt;&#x2F;code&gt; — shows a table of stored tesseras&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;&lt;strong&gt;Testing&lt;&#x2F;strong&gt; — 67+ tests across the workspace: unit tests in every module,
+property-based tests (proptest) for hex roundtrips and manifest serialization,
+integration tests covering the full create-verify-export cycle including
+tampered file and invalid signature detection. Zero clippy warnings.&lt;&#x2F;p&gt;
+&lt;h2 id=&quot;architecture-decisions&quot;&gt;Architecture decisions&lt;&#x2F;h2&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Hexagonal architecture&lt;&#x2F;strong&gt;: crypto operations are injected via trait objects
+(&lt;code&gt;Box&amp;lt;dyn Hasher&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Box&amp;lt;dyn ManifestSigner&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;Box&amp;lt;dyn ManifestVerifier&amp;gt;&lt;&#x2F;code&gt;),
+keeping the core crate free of concrete crypto dependencies.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Feature flags&lt;&#x2F;strong&gt;: the &lt;code&gt;service&lt;&#x2F;code&gt; feature on tesseras-core gates the async
+application layer. The &lt;code&gt;classical&lt;&#x2F;code&gt; and &lt;code&gt;erasure&lt;&#x2F;code&gt; features on tesseras-crypto
+control which algorithms are compiled in.&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Plain-text manifest&lt;&#x2F;strong&gt;: parseable without any binary format library, with
+explicit &lt;code&gt;blake3:&lt;&#x2F;code&gt; hash prefixes and human-readable layout.&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;h2 id=&quot;what-comes-next&quot;&gt;What comes next&lt;&#x2F;h2&gt;
+&lt;p&gt;Phase 0 is the local-only foundation. The road ahead:&lt;&#x2F;p&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;strong&gt;Phase 1: Networking&lt;&#x2F;strong&gt; — QUIC transport (quinn), Kademlia DHT for peer
+discovery, NAT traversal&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 2: Replication&lt;&#x2F;strong&gt; — Reed-Solomon erasure coding over the network,
+repair loops, bilateral reciprocity (no blockchain, no tokens)&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 3: Clients&lt;&#x2F;strong&gt; — Flutter mobile&#x2F;desktop app via flutter_rust_bridge,
+GraphQL API, WASM browser node&lt;&#x2F;li&gt;
+&lt;li&gt;&lt;strong&gt;Phase 4: Hardening&lt;&#x2F;strong&gt; — ML-DSA post-quantum signatures, packaging for
+Alpine&#x2F;Arch&#x2F;Debian&#x2F;FreeBSD&#x2F;OpenBSD, CI on SourceHut&lt;&#x2F;li&gt;
+&lt;&#x2F;ul&gt;
+&lt;p&gt;The tessera format is stable. Everything built from here connects to and extends
+what exists today.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+ <entry xml:lang="en">
+ <title>Hello, World</title>
+ <published>2026-02-13T00:00:00+00:00</published>
+ <updated>2026-02-13T00:00:00+00:00</updated>
+
+ <author>
+ <name>
+
+ Unknown
+
+ </name>
+ </author>
+
+ <link rel="alternate" type="text/html" href="https://tesseras.net/news/hello-world/"/>
+ <id>https://tesseras.net/news/hello-world/</id>
+
+ <content type="html" xml:base="https://tesseras.net/news/hello-world/">&lt;p&gt;Today we&#x27;re announcing the Tesseras project: a peer-to-peer network for
+preserving human memories across millennia.&lt;&#x2F;p&gt;
+&lt;p&gt;Tesseras is built on a simple idea — your photos, recordings, and writings
+deserve to outlast any company, platform, or file format. Each person creates a
+tessera, a self-contained time capsule that the network keeps alive through
+mutual aid and redundancy.&lt;&#x2F;p&gt;
+&lt;p&gt;The project is in its earliest stage. We&#x27;re building the foundation: tools to
+create, verify, and export tesseras offline. The network layer, replication, and
+apps will follow.&lt;&#x2F;p&gt;
+&lt;p&gt;If this mission resonates with you, &lt;a href=&quot;&#x2F;subscriptions&#x2F;&quot;&gt;join the mailing list&lt;&#x2F;a&gt; or
+browse the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git.sr.ht&#x2F;~ijanc&#x2F;tesseras&quot;&gt;source code&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
+</content>
+
+ </entry>
+</feed>
diff --git a/news/atom.xml.gz b/news/atom.xml.gz
new file mode 100644
index 0000000..e04e2c9
--- /dev/null
+++ b/news/atom.xml.gz
Binary files differ
diff --git a/news/cli-daemon-rpc/index.html b/news/cli-daemon-rpc/index.html
new file mode 100644
index 0000000..a1ca0c1
--- /dev/null
+++ b/news/cli-daemon-rpc/index.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>CLI Meets Network: Publish, Fetch, and Status Commands — Tesseras</title>
+ <meta name="description" content="The tesseras CLI can now publish tesseras to the network, fetch them from peers, and monitor replication status — all through a new Unix socket RPC bridge to the daemon.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="CLI Meets Network: Publish, Fetch, and Status Commands">
+ <meta property="og:description" content="The tesseras CLI can now publish tesseras to the network, fetch them from peers, and monitor replication status — all through a new Unix socket RPC bridge to the daemon.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="CLI Meets Network: Publish, Fetch, and Status Commands">
+ <meta name="twitter:description" content="The tesseras CLI can now publish tesseras to the network, fetch them from peers, and monitor replication status — all through a new Unix socket RPC bridge to the daemon.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;cli-daemon-rpc&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>CLI Meets Network: Publish, Fetch, and Status Commands</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>Until now the CLI operated in isolation: create a tessera, verify it, export it,
+list what you have. Everything stayed on your machine. With this release, <code>tes</code>
+gains three commands that bridge the gap between local storage and the P2P
+network — <code>publish</code>, <code>fetch</code>, and <code>status</code> — by talking to a running <code>tesd</code> over
+a Unix socket.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong><code>tesseras-rpc</code> crate</strong> — A new shared crate that both the CLI and daemon
+depend on. It defines the RPC protocol using MessagePack serialization with
+length-prefixed framing (4-byte big-endian size header, 64 MiB max). Three
+request types (<code>Publish</code>, <code>Fetch</code>, <code>Status</code>) and their corresponding responses.
+A sync <code>DaemonClient</code> handles the Unix socket connection with configurable
+timeouts. The protocol is deliberately simple — one request, one response,
+connection closed — to keep the implementation auditable.</p>
+<p><strong><code>tes publish &lt;hash&gt;</code></strong> — Publishes a tessera to the network. Accepts full
+hashes or short prefixes (e.g., <code>tes publish a1b2</code>), which are resolved against
+the local database. The daemon reads all tessera files from storage, packs them
+into a single MessagePack buffer, and hands them to the replication engine.
+Small tesseras (&lt; 4 MB) are replicated as a single fragment; larger ones go
+through Reed-Solomon erasure coding. Output shows the short hash and fragment
+count:</p>
+<pre><code>Published tessera 9f2c4a1b (24 fragments created)
+Distribution in progress — use `tes status 9f2c4a1b` to track.
+</code></pre>
+<p><strong><code>tes fetch &lt;hash&gt;</code></strong> — Retrieves a tessera from the network using its full
+content hash. The daemon collects locally available fragments, reconstructs the
+original data via erasure decoding if needed, unpacks the files, and stores them
+in the content-addressable store. Returns the number of memories and total size
+fetched.</p>
+<p><strong><code>tes status &lt;hash&gt;</code></strong> — Displays the replication health of a tessera. The
+output maps directly to the replication engine's internal health model:</p>
+<table><thead><tr><th>State</th><th>Meaning</th></tr></thead><tbody>
+<tr><td>Local</td><td>Not yet published — exists only on your machine</td></tr>
+<tr><td>Publishing</td><td>Fragments being distributed, critical redundancy</td></tr>
+<tr><td>Replicated</td><td>Distributed but below target redundancy</td></tr>
+<tr><td>Healthy</td><td>Full redundancy achieved</td></tr>
+</tbody></table>
+<p><strong>Daemon RPC listener</strong> — The daemon now binds a Unix socket (default:
+<code>$XDG_RUNTIME_DIR/tesseras/daemon.sock</code>) with proper directory permissions
+(0700), stale socket cleanup, and graceful shutdown. Each connection is handled
+in a Tokio task — the listener converts the async stream to sync I/O for the
+framing layer, dispatches to the RPC handler, and writes the response back.</p>
+<p><strong>Pack/unpack in <code>tesseras-core</code></strong> — A small module that serializes a list of
+file entries (path + data) into a single MessagePack buffer and back. This is
+the bridge between the tessera's directory structure and the replication
+engine's opaque byte blobs.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Unix socket over TCP</strong>: RPC between CLI and daemon happens on the same
+machine. Unix sockets are faster, don't need port allocation, and filesystem
+permissions provide access control without TLS.</li>
+<li><strong>MessagePack over JSON</strong>: the same wire format used everywhere else in
+Tesseras. Compact, schema-less, and already a workspace dependency. A typical
+publish request/response round-trip is under 200 bytes.</li>
+<li><strong>Sync client, async daemon</strong>: the <code>DaemonClient</code> uses blocking I/O because
+the CLI doesn't need concurrency — it sends one request and waits. The daemon
+listener is async (Tokio) to handle multiple connections. The framing layer
+works with any <code>Read</code>/<code>Write</code> impl, bridging both worlds.</li>
+<li><strong>Hash prefix resolution on the client side</strong>: <code>publish</code> and <code>status</code> resolve
+short prefixes locally before sending the full hash to the daemon. This keeps
+the daemon stateless — it doesn't need access to the CLI's database.</li>
+<li><strong>Default data directory alignment</strong>: the CLI default changed from
+<code>~/.tesseras</code> to <code>~/.local/share/tesseras</code> (via <code>dirs::data_dir()</code>) to match
+the daemon. A migration hint is printed when legacy data is detected.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>DHT peer count</strong>: the <code>status</code> command currently reports 0 peers — wiring
+the actual peer count from the DHT is the next step</li>
+<li><strong><code>tes show</code></strong>: display the contents of a tessera (memories, metadata) without
+exporting</li>
+<li><strong>Streaming fetch</strong>: for large tesseras, stream fragments as they arrive
+rather than waiting for all of them</li>
+</ul>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/cli-daemon-rpc/index.html.gz b/news/cli-daemon-rpc/index.html.gz
new file mode 100644
index 0000000..72f916f
--- /dev/null
+++ b/news/cli-daemon-rpc/index.html.gz
Binary files differ
diff --git a/news/hello-world/index.html b/news/hello-world/index.html
new file mode 100644
index 0000000..5d3da15
--- /dev/null
+++ b/news/hello-world/index.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Hello, World — Tesseras</title>
+ <meta name="description" content="Introducing the Tesseras project — a P2P network for preserving human memories.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Hello, World">
+ <meta property="og:description" content="Introducing the Tesseras project — a P2P network for preserving human memories.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Hello, World">
+ <meta name="twitter:description" content="Introducing the Tesseras project — a P2P network for preserving human memories.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;hello-world&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Hello, World</h2>
+ <p class="news-date">2026-02-13</p>
+ <p>Today we're announcing the Tesseras project: a peer-to-peer network for
+preserving human memories across millennia.</p>
+<p>Tesseras is built on a simple idea — your photos, recordings, and writings
+deserve to outlast any company, platform, or file format. Each person creates a
+tessera, a self-contained time capsule that the network keeps alive through
+mutual aid and redundancy.</p>
+<p>The project is in its earliest stage. We're building the foundation: tools to
+create, verify, and export tesseras offline. The network layer, replication, and
+apps will follow.</p>
+<p>If this mission resonates with you, <a href="/subscriptions/">join the mailing list</a> or
+browse the <a rel="external" href="https://git.sr.ht/~ijanc/tesseras">source code</a>.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/hello-world/index.html.gz b/news/hello-world/index.html.gz
new file mode 100644
index 0000000..daf5840
--- /dev/null
+++ b/news/hello-world/index.html.gz
Binary files differ
diff --git a/news/index.html b/news/index.html
new file mode 100644
index 0000000..2417937
--- /dev/null
+++ b/news/index.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>News — Tesseras</title>
+ <meta name="description" content="Tesseras project news and announcements">
+ <!-- Open Graph -->
+ <meta property="og:type" content="website">
+ <meta property="og:title" content="Tesseras">
+ <meta property="og:description" content="P2P network for preserving human memories across millennia">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Tesseras">
+ <meta name="twitter:description" content="P2P network for preserving human memories across millennia">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<h2>News</h2>
+
+<ul class="news-list">
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;packaging-debian&#x2F;">Packaging Tesseras for Debian</a>
+ <span class="news-date">2026-02-16</span>
+
+ <p>How to build and install the Tesseras .deb package on Debian&#x2F;Ubuntu using cargo-deb.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;packaging-archlinux&#x2F;">Packaging Tesseras for Arch Linux</a>
+ <span class="news-date">2026-02-16</span>
+
+ <p>How to build and install the Tesseras package on Arch Linux from source using makepkg.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-storage-deduplication&#x2F;">Phase 4: Storage Deduplication</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>A new content-addressable storage layer eliminates duplicate data across tesseras, reducing disk usage and enabling automatic garbage collection.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-institutional-onboarding&#x2F;">Phase 4: Institutional Node Onboarding</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>Libraries, archives, and museums can now join the Tesseras network as verified institutional nodes with DNS-based identity, full-text search indexes, and configurable storage pledges.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-performance-tuning&#x2F;">Phase 4: Performance Tuning</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>SQLite WAL mode with centralized pragma configuration, LRU fragment caching, QUIC connection pool lifecycle management, and attestation hot path optimization.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-wasm-browser-verification&#x2F;">Phase 4: Verify Without Installing Anything</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>Tesseras now compiles to WebAssembly — anyone can verify a tessera&#x27;s integrity and authenticity directly in the browser, with no software to install.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-nat-traversal&#x2F;">Phase 4: Punching Through NATs</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>Tesseras nodes can now discover their NAT type via STUN, coordinate UDP hole punching through introducers, and fall back to transparent relay forwarding when direct connectivity fails.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;cli-daemon-rpc&#x2F;">CLI Meets Network: Publish, Fetch, and Status Commands</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>The tesseras CLI can now publish tesseras to the network, fetch them from peers, and monitor replication status — all through a new Unix socket RPC bridge to the daemon.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-shamir-heir-recovery&#x2F;">Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing</a>
+ <span class="news-date">2026-02-15</span>
+
+ <p>Tesseras now lets you split your cryptographic identity into shares distributed to trusted heirs — any threshold of them can reconstruct your keys, but fewer reveal nothing.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase4-encryption-sealed&#x2F;">Phase 4: Encryption and Sealed Tesseras</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>Tesseras now supports private and sealed memories with hybrid post-quantum encryption — AES-256-GCM, X25519 + ML-KEM-768, and time-lock key publication.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase3-api-and-apps&#x2F;">Phase 3: Memories in Your Hands</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>Tesseras now has a Flutter app and an embedded Rust node — anyone can create and preserve memories from their phone.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;reed-solomon&#x2F;">Reed-Solomon: How Tesseras Survives Data Loss</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>A deep dive into Reed-Solomon erasure coding — what it is, why Tesseras uses it, and the challenges of keeping memories alive across centuries.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase2-replication&#x2F;">Phase 2: Memories Survive</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>Tesseras now fragments, distributes, and automatically repairs data across the network using Reed-Solomon erasure coding and a bilateral reciprocity ledger.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase1-basic-network&#x2F;">Phase 1: Nodes Find Each Other</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>Tesseras nodes can now discover peers, form a Kademlia DHT over QUIC, and publish and find tessera pointers across the network.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;phase0-foundation&#x2F;">Phase 0: Foundation Laid</a>
+ <span class="news-date">2026-02-14</span>
+
+ <p>The foundation crates for Tesseras are now in place — core domain types, cryptographic primitives, SQLite storage, and a working CLI.</p>
+
+ </li>
+
+ <li>
+ <a href="https:&#x2F;&#x2F;tesseras.net&#x2F;news&#x2F;hello-world&#x2F;">Hello, World</a>
+ <span class="news-date">2026-02-13</span>
+
+ <p>Introducing the Tesseras project — a P2P network for preserving human memories.</p>
+
+ </li>
+
+</ul>
+
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/index.html.gz b/news/index.html.gz
new file mode 100644
index 0000000..d156023
--- /dev/null
+++ b/news/index.html.gz
Binary files differ
diff --git a/news/packaging-archlinux/index.html b/news/packaging-archlinux/index.html
new file mode 100644
index 0000000..043464f
--- /dev/null
+++ b/news/packaging-archlinux/index.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Packaging Tesseras for Arch Linux — Tesseras</title>
+ <meta name="description" content="How to build and install the Tesseras package on Arch Linux from source using makepkg.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Packaging Tesseras for Arch Linux">
+ <meta property="og:description" content="How to build and install the Tesseras package on Arch Linux from source using makepkg.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Packaging Tesseras for Arch Linux">
+ <meta name="twitter:description" content="How to build and install the Tesseras package on Arch Linux from source using makepkg.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;packaging-archlinux&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Packaging Tesseras for Arch Linux</h2>
+ <p class="news-date">2026-02-16</p>
+ <p>Tesseras now ships a PKGBUILD for Arch Linux. This post walks through building
+and installing the package from source.</p>
+<h2 id="prerequisites">Prerequisites</h2>
+<p>You need a working Rust toolchain and the base-devel group:</p>
+<pre><code data-lang="sh">sudo pacman -S --needed base-devel sqlite
+rustup toolchain install stable
+</code></pre>
+<h2 id="building">Building</h2>
+<p>Clone the repository and run the <code>just arch</code> recipe:</p>
+<pre><code data-lang="sh">git clone https://git.sr.ht/~ijanc/tesseras
+cd tesseras
+just arch
+</code></pre>
+<p>This runs <code>makepkg -sf</code> inside <code>packaging/archlinux/</code>, which:</p>
+<ol>
+<li><strong>prepare</strong> — fetches Cargo dependencies with <code>cargo fetch --locked</code></li>
+<li><strong>build</strong> — compiles <code>tesd</code> and <code>tes</code> (the CLI) in release mode</li>
+<li><strong>package</strong> — installs binaries, systemd service, sysusers/tmpfiles configs,
+shell completions (bash, zsh, fish), and a default config file</li>
+</ol>
+<p>The result is a <code>.pkg.tar.zst</code> file in <code>packaging/archlinux/</code>.</p>
+<h2 id="installing">Installing</h2>
+<pre><code data-lang="sh">sudo pacman -U packaging/archlinux/tesseras-*.pkg.tar.zst
+</code></pre>
+<h2 id="post-install-setup">Post-install setup</h2>
+<p>The package creates a <code>tesseras</code> system user and group automatically via
+systemd-sysusers. To use the CLI without sudo, add yourself to the group:</p>
+<pre><code data-lang="sh">sudo usermod -aG tesseras $USER
+</code></pre>
+<p>Log out and back in, then start the daemon:</p>
+<pre><code data-lang="sh">sudo systemctl enable --now tesd
+</code></pre>
+<h2 id="what-the-package-includes">What the package includes</h2>
+<table><thead><tr><th>Path</th><th>Description</th></tr></thead><tbody>
+<tr><td><code>/usr/bin/tesd</code></td><td>Full node daemon</td></tr>
+<tr><td><code>/usr/bin/tes</code></td><td>CLI client</td></tr>
+<tr><td><code>/etc/tesseras/config.toml</code></td><td>Default configuration (marked as backup)</td></tr>
+<tr><td><code>/usr/lib/systemd/system/tesd.service</code></td><td>Systemd unit with security hardening</td></tr>
+<tr><td><code>/usr/lib/sysusers.d/tesseras.conf</code></td><td>System user definition</td></tr>
+<tr><td><code>/usr/lib/tmpfiles.d/tesseras.conf</code></td><td>Data directory <code>/var/lib/tesseras</code></td></tr>
+<tr><td>Shell completions</td><td>bash, zsh, and fish</td></tr>
+</tbody></table>
+<h2 id="pkgbuild-details">PKGBUILD details</h2>
+<p>The PKGBUILD builds directly from the local git checkout rather than downloading
+a source tarball. The <code>TESSERAS_ROOT</code> environment variable points makepkg to the
+workspace root. Cargo's target directory is set to <code>$srcdir/target</code> to keep
+build artifacts inside the makepkg sandbox.</p>
+<p>The package depends only on <code>sqlite</code> at runtime and <code>cargo</code> at build time.</p>
+<h2 id="updating">Updating</h2>
+<p>After pulling new changes, simply run <code>just arch</code> again and reinstall:</p>
+<pre><code data-lang="sh">git pull
+just arch
+sudo pacman -U packaging/archlinux/tesseras-*.pkg.tar.zst
+</code></pre>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/packaging-archlinux/index.html.gz b/news/packaging-archlinux/index.html.gz
new file mode 100644
index 0000000..781dcda
--- /dev/null
+++ b/news/packaging-archlinux/index.html.gz
Binary files differ
diff --git a/news/packaging-debian/index.html b/news/packaging-debian/index.html
new file mode 100644
index 0000000..bfe7563
--- /dev/null
+++ b/news/packaging-debian/index.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Packaging Tesseras for Debian — Tesseras</title>
+ <meta name="description" content="How to build and install the Tesseras .deb package on Debian&#x2F;Ubuntu using cargo-deb.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Packaging Tesseras for Debian">
+ <meta property="og:description" content="How to build and install the Tesseras .deb package on Debian&#x2F;Ubuntu using cargo-deb.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Packaging Tesseras for Debian">
+ <meta name="twitter:description" content="How to build and install the Tesseras .deb package on Debian&#x2F;Ubuntu using cargo-deb.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;packaging-debian&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Packaging Tesseras for Debian</h2>
+ <p class="news-date">2026-02-16</p>
+ <p>Tesseras now ships a <code>.deb</code> package for Debian and Ubuntu. This post walks
+through building and installing the package from source using <code>cargo-deb</code>.</p>
+<h2 id="prerequisites">Prerequisites</h2>
+<p>You need a working Rust toolchain and the required system libraries:</p>
+<pre><code data-lang="sh">sudo apt install build-essential pkg-config libsqlite3-dev
+rustup toolchain install stable
+cargo install cargo-deb
+</code></pre>
+<h2 id="building">Building</h2>
+<p>Clone the repository and run the <code>just deb</code> recipe:</p>
+<pre><code data-lang="sh">git clone https://git.sr.ht/~ijanc/tesseras
+cd tesseras
+just deb
+</code></pre>
+<p>This recipe does three things:</p>
+<ol>
+<li><strong>Compiles</strong> <code>tesd</code> (the daemon) and <code>tes</code> (the CLI) in release mode with
+<code>cargo build --release</code></li>
+<li><strong>Generates shell completions</strong> for bash, zsh, and fish from the <code>tes</code> binary</li>
+<li><strong>Packages</strong> everything into a <code>.deb</code> file with
+<code>cargo deb -p tesseras-daemon --no-build</code></li>
+</ol>
+<p>The result is a <code>.deb</code> file in <code>target/debian/</code>.</p>
+<h2 id="installing">Installing</h2>
+<pre><code data-lang="sh">sudo dpkg -i target/debian/tesseras-daemon_*.deb
+</code></pre>
+<p>If there are missing dependencies, fix them with:</p>
+<pre><code data-lang="sh">sudo apt install -f
+</code></pre>
+<h2 id="post-install-setup">Post-install setup</h2>
+<p>The <code>postinst</code> script automatically creates a <code>tesseras</code> system user and the
+data directory <code>/var/lib/tesseras</code>. To use the CLI without sudo, add yourself to
+the group:</p>
+<pre><code data-lang="sh">sudo usermod -aG tesseras $USER
+</code></pre>
+<p>Log out and back in, then start the daemon:</p>
+<pre><code data-lang="sh">sudo systemctl enable --now tesd
+</code></pre>
+<h2 id="what-the-package-includes">What the package includes</h2>
+<table><thead><tr><th>Path</th><th>Description</th></tr></thead><tbody>
+<tr><td><code>/usr/bin/tesd</code></td><td>Full node daemon</td></tr>
+<tr><td><code>/usr/bin/tes</code></td><td>CLI client</td></tr>
+<tr><td><code>/etc/tesseras/config.toml</code></td><td>Default configuration (marked as conffile)</td></tr>
+<tr><td><code>/lib/systemd/system/tesd.service</code></td><td>Systemd unit with security hardening</td></tr>
+<tr><td>Shell completions</td><td>bash, zsh, and fish</td></tr>
+</tbody></table>
+<h2 id="how-cargo-deb-works">How cargo-deb works</h2>
+<p>The packaging metadata lives in <code>crates/tesseras-daemon/Cargo.toml</code> under
+<code>[package.metadata.deb]</code>. This section defines:</p>
+<ul>
+<li><strong>depends</strong> — runtime dependencies: <code>libc6</code> and <code>libsqlite3-0</code></li>
+<li><strong>assets</strong> — files to include in the package (binaries, config, systemd unit,
+shell completions)</li>
+<li><strong>conf-files</strong> — files treated as configuration (preserved on upgrade)</li>
+<li><strong>maintainer-scripts</strong> — <code>postinst</code> and <code>postrm</code> scripts in
+<code>packaging/debian/scripts/</code></li>
+<li><strong>systemd-units</strong> — automatic systemd integration</li>
+</ul>
+<p>The <code>postinst</code> script creates the <code>tesseras</code> system user and data directory on
+install. The <code>postrm</code> script cleans up the user, group, and data directory only
+on <code>purge</code> (not on simple removal).</p>
+<h2 id="systemd-hardening">Systemd hardening</h2>
+<p>The <code>tesd.service</code> unit includes security hardening directives:</p>
+<pre><code data-lang="ini">NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/var/lib/tesseras
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+ProtectControlGroups=true
+RestrictSUIDSGID=true
+MemoryDenyWriteExecute=true
+</code></pre>
+<p>The daemon runs as the unprivileged <code>tesseras</code> user and can only write to
+<code>/var/lib/tesseras</code>.</p>
+<h2 id="deploying-to-a-remote-server">Deploying to a remote server</h2>
+<p>The justfile includes a <code>deploy</code> recipe for pushing the <code>.deb</code> to a remote host:</p>
+<pre><code data-lang="sh">just deploy bootstrap1.tesseras.net
+</code></pre>
+<p>This builds the <code>.deb</code>, copies it via <code>scp</code>, installs it with <code>dpkg -i</code>, and
+restarts the <code>tesd</code> service.</p>
+<h2 id="updating">Updating</h2>
+<p>After pulling new changes, simply run <code>just deb</code> again and reinstall:</p>
+<pre><code data-lang="sh">git pull
+just deb
+sudo dpkg -i target/debian/tesseras-daemon_*.deb
+</code></pre>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/packaging-debian/index.html.gz b/news/packaging-debian/index.html.gz
new file mode 100644
index 0000000..69ee540
--- /dev/null
+++ b/news/packaging-debian/index.html.gz
Binary files differ
diff --git a/news/phase0-foundation/index.html b/news/phase0-foundation/index.html
new file mode 100644
index 0000000..783c9e6
--- /dev/null
+++ b/news/phase0-foundation/index.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 0: Foundation Laid — Tesseras</title>
+ <meta name="description" content="The foundation crates for Tesseras are now in place — core domain types, cryptographic primitives, SQLite storage, and a working CLI.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 0: Foundation Laid">
+ <meta property="og:description" content="The foundation crates for Tesseras are now in place — core domain types, cryptographic primitives, SQLite storage, and a working CLI.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 0: Foundation Laid">
+ <meta name="twitter:description" content="The foundation crates for Tesseras are now in place — core domain types, cryptographic primitives, SQLite storage, and a working CLI.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase0-foundation&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 0: Foundation Laid</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>The first milestone of the Tesseras project is complete. Phase 0 establishes the
+foundation that every future component will build on: domain types,
+cryptography, storage, and a usable command-line interface.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>tesseras-core</strong> — The domain layer defines the tessera format: <code>ContentHash</code>
+(BLAKE3, 32 bytes), <code>NodeId</code> (Kademlia, 20 bytes), memory types (Moment,
+Reflection, Daily, Relation, Object), visibility modes (Private, Circle, Public,
+PublicAfterDeath, Sealed), and a plain-text manifest format that can be parsed
+by any programming language for the next thousand years. The application service
+layer (<code>TesseraService</code>) handles create, verify, export, and list operations
+through port traits, following hexagonal architecture.</p>
+<p><strong>tesseras-crypto</strong> — Ed25519 key generation, signing, and verification. A
+dual-signature framework (Ed25519 + ML-DSA placeholder) ready for post-quantum
+migration. BLAKE3 content hashing. Reed-Solomon erasure coding behind a feature
+flag for future replication.</p>
+<p><strong>tesseras-storage</strong> — SQLite index via rusqlite with plain-SQL migrations.
+Filesystem blob store with content-addressable layout
+(<code>blobs/&lt;tessera_hash&gt;/&lt;memory_hash&gt;/&lt;filename&gt;</code>). Identity key persistence on
+disk.</p>
+<p><strong>tesseras-cli</strong> — A working <code>tesseras</code> binary with five commands:</p>
+<ul>
+<li><code>init</code> — generates Ed25519 identity, creates SQLite database</li>
+<li><code>create &lt;dir&gt;</code> — scans a directory for media files, creates a signed tessera</li>
+<li><code>verify &lt;hash&gt;</code> — checks signature and file integrity</li>
+<li><code>export &lt;hash&gt; &lt;dest&gt;</code> — writes a self-contained tessera directory</li>
+<li><code>list</code> — shows a table of stored tesseras</li>
+</ul>
+<p><strong>Testing</strong> — 67+ tests across the workspace: unit tests in every module,
+property-based tests (proptest) for hex roundtrips and manifest serialization,
+integration tests covering the full create-verify-export cycle including
+tampered file and invalid signature detection. Zero clippy warnings.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Hexagonal architecture</strong>: crypto operations are injected via trait objects
+(<code>Box&lt;dyn Hasher&gt;</code>, <code>Box&lt;dyn ManifestSigner&gt;</code>, <code>Box&lt;dyn ManifestVerifier&gt;</code>),
+keeping the core crate free of concrete crypto dependencies.</li>
+<li><strong>Feature flags</strong>: the <code>service</code> feature on tesseras-core gates the async
+application layer. The <code>classical</code> and <code>erasure</code> features on tesseras-crypto
+control which algorithms are compiled in.</li>
+<li><strong>Plain-text manifest</strong>: parseable without any binary format library, with
+explicit <code>blake3:</code> hash prefixes and human-readable layout.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<p>Phase 0 is the local-only foundation. The road ahead:</p>
+<ul>
+<li><strong>Phase 1: Networking</strong> — QUIC transport (quinn), Kademlia DHT for peer
+discovery, NAT traversal</li>
+<li><strong>Phase 2: Replication</strong> — Reed-Solomon erasure coding over the network,
+repair loops, bilateral reciprocity (no blockchain, no tokens)</li>
+<li><strong>Phase 3: Clients</strong> — Flutter mobile/desktop app via flutter_rust_bridge,
+GraphQL API, WASM browser node</li>
+<li><strong>Phase 4: Hardening</strong> — ML-DSA post-quantum signatures, packaging for
+Alpine/Arch/Debian/FreeBSD/OpenBSD, CI on SourceHut</li>
+</ul>
+<p>The tessera format is stable. Everything built from here connects to and extends
+what exists today.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase0-foundation/index.html.gz b/news/phase0-foundation/index.html.gz
new file mode 100644
index 0000000..31efa2d
--- /dev/null
+++ b/news/phase0-foundation/index.html.gz
Binary files differ
diff --git a/news/phase1-basic-network/index.html b/news/phase1-basic-network/index.html
new file mode 100644
index 0000000..4852505
--- /dev/null
+++ b/news/phase1-basic-network/index.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 1: Nodes Find Each Other — Tesseras</title>
+ <meta name="description" content="Tesseras nodes can now discover peers, form a Kademlia DHT over QUIC, and publish and find tessera pointers across the network.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 1: Nodes Find Each Other">
+ <meta property="og:description" content="Tesseras nodes can now discover peers, form a Kademlia DHT over QUIC, and publish and find tessera pointers across the network.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 1: Nodes Find Each Other">
+ <meta name="twitter:description" content="Tesseras nodes can now discover peers, form a Kademlia DHT over QUIC, and publish and find tessera pointers across the network.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase1-basic-network&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 1: Nodes Find Each Other</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>Tesseras is no longer a local-only tool. Phase 1 delivers the networking layer:
+nodes discover each other through a Kademlia DHT, communicate over QUIC, and
+publish tessera pointers that any peer on the network can find. A tessera
+created on node A is now findable from node C.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>tesseras-core</strong> (updated) — New network domain types: <code>TesseraPointer</code>
+(lightweight reference to a tessera's holders and fragment locations),
+<code>NodeIdentity</code> (node ID + public key + proof-of-work nonce), <code>NodeInfo</code>
+(identity + address + capabilities), and <code>Capabilities</code> (bitflags for what a
+node supports: DHT, storage, relay, replication).</p>
+<p><strong>tesseras-net</strong> — The transport layer, built on QUIC via quinn. The <code>Transport</code>
+trait defines the port: <code>send</code>, <code>recv</code>, <code>disconnect</code>, <code>local_addr</code>. Two adapters
+implement it:</p>
+<ul>
+<li><code>QuinnTransport</code> — real QUIC with self-signed TLS, ALPN negotiation
+(<code>tesseras/1</code>), connection pooling via DashMap, and a background accept loop
+that handles incoming streams.</li>
+<li><code>MemTransport</code> + <code>SimNetwork</code> — in-memory channels for deterministic testing
+without network I/O. Every integration test in the DHT crate runs against
+this.</li>
+</ul>
+<p>The wire protocol uses length-prefixed MessagePack: a 4-byte big-endian length
+header followed by an rmp-serde payload. <code>WireMessage</code> carries a version byte,
+request ID, and a body that can be a request, response, or protocol-level error.
+Maximum message size is 64 KiB.</p>
+<p><strong>tesseras-dht</strong> — A complete Kademlia implementation:</p>
+<ul>
+<li><em>Routing table</em>: 160 k-buckets with k=20. Least-recently-seen eviction,
+move-to-back on update, ping-check before replacing a full bucket's oldest
+entry.</li>
+<li><em>XOR distance</em>: 160-bit XOR metric with bucket indexing by highest differing
+bit.</li>
+<li><em>Proof-of-work</em>: nodes grind a nonce until <code>BLAKE3(pubkey || nonce)[..20]</code> has
+8 leading zero bits (~256 hash attempts on average). Cheap enough for any
+device, expensive enough to make Sybil attacks impractical at scale.</li>
+<li><em>Protocol messages</em>: Ping/Pong, FindNode/FindNodeResponse,
+FindValue/FindValueResult, Store — all serialized with MessagePack via serde.</li>
+<li><em>Pointer store</em>: bounded in-memory store with configurable TTL (24 hours
+default) and max entries (10,000 default). When full, evicts pointers furthest
+from the local node ID, following Kademlia's distance-based responsibility
+model.</li>
+<li><em>DhtEngine</em>: the main orchestrator. Handles incoming RPCs, runs iterative
+lookups (alpha=3 parallelism), bootstrap, publish, and find. The <code>run()</code>
+method drives a <code>tokio::select!</code> loop with maintenance timers: routing table
+refresh every 60 seconds, pointer expiry every 5 minutes.</li>
+</ul>
+<p><strong>tesd</strong> — A full-node binary. Parses CLI args (bind address, bootstrap peers,
+data directory), generates a PoW-valid node identity, binds a QUIC endpoint,
+bootstraps into the network, and runs the DHT engine. Graceful shutdown on
+Ctrl+C via tokio signal handling.</p>
+<p><strong>Infrastructure</strong> — OpenTofu configuration for two Hetzner Cloud bootstrap
+nodes (cx22 instances in Falkenstein, Germany and Helsinki, Finland). Cloud-init
+provisioning script creates a dedicated <code>tesseras</code> user, writes a config file,
+and sets up a systemd service. Firewall rules open UDP 4433 (QUIC) and restrict
+metrics to internal access.</p>
+<p><strong>Testing</strong> — 139 tests across the workspace:</p>
+<ul>
+<li>47 unit tests in tesseras-dht (routing table, distance, PoW, pointer store,
+message serialization, engine RPCs)</li>
+<li>5 multi-node integration tests (3-node bootstrap, 10-node lookup convergence,
+publish-and-find, node departure detection, PoW rejection)</li>
+<li>14 tests in tesseras-net (codec roundtrips, transport send/recv, backpressure,
+disconnect)</li>
+<li>Docker Compose smoke tests with 3 containerized nodes communicating over real
+QUIC</li>
+<li>Zero clippy warnings, clean formatting</li>
+</ul>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Transport as a port</strong>: the <code>Transport</code> trait is the only interface between
+the DHT engine and the network. Swapping QUIC for any other protocol means
+implementing four methods. All DHT tests use the in-memory adapter, making
+them fast and deterministic.</li>
+<li><strong>One stream per RPC</strong>: each DHT request-response pair uses a fresh
+bidirectional QUIC stream. No multiplexing complexity, no head-of-line
+blocking between independent operations. QUIC handles the multiplexing at the
+connection level.</li>
+<li><strong>MessagePack over Protobuf</strong>: compact binary encoding without code generation
+or schema files. Serde integration means adding a field to a message is a
+one-line change. Trade-off: no built-in schema evolution guarantees, but at
+this stage velocity matters more.</li>
+<li><strong>PoW instead of stake or reputation</strong>: a node identity costs ~256 BLAKE3
+hashes. This runs in under a second on any hardware, including a Raspberry Pi,
+but generating thousands of identities for a Sybil attack becomes expensive.
+No tokens, no blockchain, no external dependencies.</li>
+<li><strong>Iterative lookup with routing table updates</strong>: discovered nodes are added to
+the routing table as they're encountered during iterative lookups, following
+standard Kademlia behavior. This ensures the routing table improves
+organically as nodes interact.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 2: Replication</strong> — Reed-Solomon erasure coding over the network,
+fragment distribution, automatic repair loops, bilateral reciprocity ledger
+(no blockchain, no tokens)</li>
+<li><strong>Phase 3: API and Apps</strong> — Flutter mobile/desktop app via
+flutter_rust_bridge, GraphQL API (async-graphql), WASM browser node</li>
+<li><strong>Phase 4: Resilience and Scale</strong> — ML-DSA post-quantum signatures, advanced
+NAT traversal, Shamir's Secret Sharing for heirs, packaging for
+Alpine/Arch/Debian/FreeBSD/OpenBSD, CI on SourceHut</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser, institutional
+curation, genealogy integration, physical media export</li>
+</ul>
+<p>Nodes can find each other. Next, they learn to keep each other's memories alive.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase1-basic-network/index.html.gz b/news/phase1-basic-network/index.html.gz
new file mode 100644
index 0000000..fe517f3
--- /dev/null
+++ b/news/phase1-basic-network/index.html.gz
Binary files differ
diff --git a/news/phase2-replication/index.html b/news/phase2-replication/index.html
new file mode 100644
index 0000000..777bf42
--- /dev/null
+++ b/news/phase2-replication/index.html
@@ -0,0 +1,201 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 2: Memories Survive — Tesseras</title>
+ <meta name="description" content="Tesseras now fragments, distributes, and automatically repairs data across the network using Reed-Solomon erasure coding and a bilateral reciprocity ledger.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 2: Memories Survive">
+ <meta property="og:description" content="Tesseras now fragments, distributes, and automatically repairs data across the network using Reed-Solomon erasure coding and a bilateral reciprocity ledger.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 2: Memories Survive">
+ <meta name="twitter:description" content="Tesseras now fragments, distributes, and automatically repairs data across the network using Reed-Solomon erasure coding and a bilateral reciprocity ledger.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase2-replication&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 2: Memories Survive</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>A tessera is no longer tied to a single machine. Phase 2 delivers the
+replication layer: data is split into erasure-coded fragments, distributed
+across multiple peers, and automatically repaired when nodes go offline. A
+bilateral reciprocity ledger ensures fair storage exchange — no blockchain, no
+tokens.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>tesseras-core</strong> (updated) — New replication domain types: <code>FragmentPlan</code>
+(selects fragmentation tier based on tessera size), <code>FragmentId</code> (tessera hash +
+index + shard count + checksum), <code>FragmentEnvelope</code> (fragment with its metadata
+for wire transport), <code>FragmentationTier</code> (Small/Medium/Large), <code>Attestation</code>
+(proof that a node holds a fragment at a given time), and <code>ReplicateAck</code>
+(acknowledgement of fragment receipt). Three new port traits define the
+hexagonal boundaries: <code>DhtPort</code> (find peers, replicate fragments, request
+attestations, ping), <code>FragmentStore</code> (store/read/delete/list/verify fragments),
+and <code>ReciprocityLedger</code> (record storage exchanges, query balances, find best
+peers). Maximum tessera size is 1 GB.</p>
+<p><strong>tesseras-crypto</strong> (updated) — The existing <code>ReedSolomonCoder</code> now powers
+fragment encoding. Data is split into shards, parity shards are computed, and
+any combination of data shards can reconstruct the original — as long as the
+number of missing shards does not exceed the parity count.</p>
+<p><strong>tesseras-storage</strong> (updated) — Two new adapters:</p>
+<ul>
+<li><code>FsFragmentStore</code> — stores fragment data as files on disk
+(<code>{root}/{tessera_hash}/{index:03}.shard</code>) with a SQLite metadata index
+tracking tessera hash, shard index, shard count, checksum, and byte size.
+Verification recomputes the BLAKE3 hash and compares it to the stored
+checksum.</li>
+<li><code>SqliteReciprocityLedger</code> — bilateral storage accounting in SQLite. Each peer
+has a row tracking bytes stored for them and bytes they store for us. The
+<code>balance</code> column is a generated column
+(<code>bytes_they_store_for_us - bytes_stored_for_them</code>). UPSERT ensures atomic
+increment of counters.</li>
+</ul>
+<p>New migration (<code>002_replication.sql</code>) adds tables for fragments, fragment plans,
+holders, holder-fragment mappings, and reciprocity balances.</p>
+<p><strong>tesseras-dht</strong> (updated) — Four new message variants: <code>Replicate</code> (send a
+fragment envelope), <code>ReplicateAck</code> (confirm receipt), <code>AttestRequest</code> (ask a
+node to prove it holds a tessera's fragments), and <code>AttestResponse</code> (return
+attestation with checksums and timestamp). The engine handles these in its
+message dispatch loop.</p>
+<p><strong>tesseras-replication</strong> — The new crate, with five modules:</p>
+<ul>
+<li>
+<p><em>Fragment encoding</em> (<code>fragment.rs</code>): <code>encode_tessera()</code> selects the
+fragmentation tier based on size, then calls Reed-Solomon encoding for Medium
+and Large tiers. Three tiers:</p>
+<ul>
+<li><strong>Small</strong> (&lt; 4 MB): whole-file replication to r=7 peers, no erasure coding</li>
+<li><strong>Medium</strong> (4–256 MB): 16 data + 8 parity shards, distributed across r=7
+peers</li>
+<li><strong>Large</strong> (≥ 256 MB): 48 data + 24 parity shards, distributed across r=7
+peers</li>
+</ul>
+</li>
+<li>
+<p><em>Distribution</em> (<code>distributor.rs</code>): subnet diversity filtering limits peers per
+/24 IPv4 subnet (or /48 IPv6 prefix) to avoid correlated failures. If all your
+fragments land on the same rack, a single power outage kills them all.</p>
+</li>
+<li>
+<p><em>Service</em> (<code>service.rs</code>): <code>ReplicationService</code> is the orchestrator.
+<code>replicate_tessera()</code> encodes the data, finds the closest peers via DHT,
+applies subnet diversity, and distributes fragments round-robin.
+<code>receive_fragment()</code> validates the BLAKE3 checksum, checks reciprocity balance
+(rejects if the sender's deficit exceeds the configured threshold), stores the
+fragment, and updates the ledger. <code>handle_attestation_request()</code> lists local
+fragments and computes their checksums as proof of possession.</p>
+</li>
+<li>
+<p><em>Repair</em> (<code>repair.rs</code>): <code>check_tessera_health()</code> requests attestations from
+known holders, falls back to ping for unresponsive nodes, verifies local
+fragment integrity, and returns one of three actions: <code>Healthy</code>,
+<code>NeedsReplication { deficit }</code>, or <code>CorruptLocal { fragment_index }</code>. The
+repair loop runs every 24 hours (with 2-hour jitter) via <code>tokio::select!</code> with
+shutdown integration.</p>
+</li>
+<li>
+<p><em>Configuration</em> (<code>config.rs</code>): <code>ReplicationConfig</code> with defaults for repair
+interval (24h), jitter (2h), concurrent transfers (4), minimum free space (1
+GB), deficit allowance (256 MB), and per-peer storage limit (1 GB).</p>
+</li>
+</ul>
+<p><strong>tesd</strong> (updated) — The daemon now opens a SQLite database (<code>db/tesseras.db</code>),
+runs migrations, creates <code>FsFragmentStore</code>, <code>SqliteReciprocityLedger</code>, and
+<code>FsBlobStore</code> instances, wraps the DHT engine in a <code>DhtPortAdapter</code>, builds a
+<code>ReplicationService</code>, and spawns the repair loop as a background task with
+graceful shutdown.</p>
+<p><strong>Testing</strong> — 193 tests across the workspace:</p>
+<ul>
+<li>15 unit tests in tesseras-replication (fragment encoding tiers, checksum
+validation, subnet diversity, repair health checks, service receive/replicate
+flows)</li>
+<li>3 integration tests with real storage (full encode→distribute→receive cycle
+for medium tessera, small whole-file replication, tampered fragment rejection)</li>
+<li>Tests use in-memory SQLite + tempdir fragments with mockall mocks for DHT and
+BlobStore</li>
+<li>Zero clippy warnings, clean formatting</li>
+</ul>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Three-tier fragmentation</strong>: small files don't need erasure coding — the
+overhead isn't worth it. Medium and large files get progressively more parity
+shards. This avoids wasting storage on small tesseras while providing strong
+redundancy for large ones.</li>
+<li><strong>Owner-push distribution</strong>: the tessera owner encodes fragments and pushes
+them to peers, rather than peers pulling. This simplifies the protocol (no
+negotiation phase) and ensures fragments are distributed immediately.</li>
+<li><strong>Bilateral reciprocity without consensus</strong>: each node tracks its own balance
+with each peer locally. No global ledger, no token, no blockchain. If peer A
+stores 500 MB for peer B, peer B should store roughly 500 MB for peer A. Free
+riders lose redundancy gradually — their fragments are deprioritized for
+repair, but never deleted.</li>
+<li><strong>Subnet diversity</strong>: fragments are spread across different network subnets to
+survive correlated failures. A datacenter outage shouldn't take out all copies
+of a tessera.</li>
+<li><strong>Attestation-first health checks</strong>: the repair loop asks holders to prove
+possession (attestation with checksums) before declaring a tessera degraded.
+Only when attestation fails does it fall back to a simple ping. This catches
+silent data corruption, not just node departure.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 3: API and Apps</strong> — Flutter mobile/desktop app via
+flutter_rust_bridge, GraphQL API (async-graphql), WASM browser node</li>
+<li><strong>Phase 4: Resilience and Scale</strong> — ML-DSA post-quantum signatures, advanced
+NAT traversal, Shamir's Secret Sharing for heirs, packaging for
+Alpine/Arch/Debian/FreeBSD/OpenBSD, CI on SourceHut</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser, institutional
+curation, genealogy integration, physical media export</li>
+</ul>
+<p>Nodes can find each other and keep each other's memories alive. Next, we give
+people a way to hold their memories in their hands.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase2-replication/index.html.gz b/news/phase2-replication/index.html.gz
new file mode 100644
index 0000000..eb2ccb3
--- /dev/null
+++ b/news/phase2-replication/index.html.gz
Binary files differ
diff --git a/news/phase3-api-and-apps/index.html b/news/phase3-api-and-apps/index.html
new file mode 100644
index 0000000..1f0feab
--- /dev/null
+++ b/news/phase3-api-and-apps/index.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 3: Memories in Your Hands — Tesseras</title>
+ <meta name="description" content="Tesseras now has a Flutter app and an embedded Rust node — anyone can create and preserve memories from their phone.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 3: Memories in Your Hands">
+ <meta property="og:description" content="Tesseras now has a Flutter app and an embedded Rust node — anyone can create and preserve memories from their phone.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 3: Memories in Your Hands">
+ <meta name="twitter:description" content="Tesseras now has a Flutter app and an embedded Rust node — anyone can create and preserve memories from their phone.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase3-api-and-apps&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 3: Memories in Your Hands</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>People can now hold their memories in their hands. Phase 3 delivers what the
+previous phases built toward: a mobile app where someone downloads Tesseras,
+creates an identity, takes a photo, and that memory enters the preservation
+network. No cloud accounts, no subscriptions, no company between you and your
+memories.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>tesseras-embedded</strong> — A full P2P node that runs inside a mobile app. The
+<code>EmbeddedNode</code> struct owns a Tokio runtime, SQLite database, QUIC transport,
+Kademlia DHT engine, replication service, and tessera service — the same stack
+as the desktop daemon, compiled into a shared library. A global singleton
+pattern (<code>Mutex&lt;Option&lt;EmbeddedNode&gt;&gt;</code>) ensures one node per app lifecycle. On
+start, it opens the database, runs migrations, loads or generates an Ed25519
+identity with proof-of-work node ID, binds QUIC on an ephemeral port, wires up
+DHT and replication, and spawns the repair loop. On stop, it sends a shutdown
+signal and drains gracefully.</p>
+<p>Eleven FFI functions are exposed to Dart via flutter_rust_bridge: lifecycle
+(<code>node_start</code>, <code>node_stop</code>, <code>node_is_running</code>), identity (<code>create_identity</code>,
+<code>get_identity</code>), memories (<code>create_memory</code>, <code>get_timeline</code>, <code>get_memory</code>), and
+network status (<code>get_network_stats</code>, <code>get_replication_status</code>). All types
+crossing the FFI boundary are flat structs with only <code>String</code>, <code>Option&lt;String&gt;</code>,
+<code>Vec&lt;String&gt;</code>, and primitives — no trait objects, no generics, no lifetimes.</p>
+<p>Four adapter modules bridge core ports to concrete implementations:
+<code>Blake3HasherAdapter</code>, <code>Ed25519SignerAdapter</code>/<code>Ed25519VerifierAdapter</code> for
+cryptography, <code>DhtPortAdapter</code> for DHT operations, and
+<code>ReplicationHandlerAdapter</code> for incoming fragment and attestation RPCs.</p>
+<p>The <code>bundled-sqlite</code> feature flag compiles SQLite from source, required for
+Android and iOS where the system library may not be available. Cargokit
+configuration passes this flag automatically in both debug and release builds.</p>
+<p><strong>Flutter app</strong> — A Material Design 3 application with Riverpod state
+management, targeting Android, iOS, Linux, macOS, and Windows from a single
+codebase.</p>
+<p>The <em>onboarding flow</em> is three screens: a welcome screen explaining the project
+in one sentence ("Preserve your memories across millennia. No cloud. No
+company."), an identity creation screen that triggers Ed25519 keypair generation
+in Rust, and a confirmation screen showing the user's name and cryptographic
+identity.</p>
+<p>The <em>timeline screen</em> displays memories in reverse chronological order with
+image previews, context text, and chips for memory type and visibility.
+Pull-to-refresh reloads from the Rust node. A floating action button opens the
+<em>memory creation screen</em>, which supports photo selection from gallery or camera
+via <code>image_picker</code>, optional context text, memory type and visibility dropdowns,
+and comma-separated tags. Creating a memory calls the Rust FFI synchronously,
+then returns to the timeline.</p>
+<p>The <em>network screen</em> shows two cards: node status (peer count, DHT size,
+bootstrap state, uptime) and replication health (total fragments, healthy
+fragments, repairing fragments, replication factor). The <em>settings screen</em>
+displays the user's identity — name, truncated node ID, truncated public key,
+and creation date.</p>
+<p>Three Riverpod providers manage state: <code>nodeProvider</code> starts the embedded node
+on app launch using the app documents directory and stops it on dispose;
+<code>identityProvider</code> loads the existing profile or creates a new one;
+<code>timelineProvider</code> fetches the memory list with pagination.</p>
+<p><strong>Testing</strong> — 9 Rust unit tests in tesseras-embedded covering node lifecycle
+(start/stop without panic), identity persistence across restarts, restart cycles
+without SQLite corruption, network event streaming, stats retrieval, memory
+creation and timeline retrieval, and single memory lookup by hash. 2 Flutter
+tests: an integration test verifying Rust initialization and app startup, and a
+widget smoke test.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Embedded node, not client-server</strong>: the phone runs the full P2P stack, not a
+thin client talking to a remote daemon. This means memories are preserved even
+without internet. Users with a Raspberry Pi or VPS can optionally connect the
+app to their daemon via GraphQL for higher availability, but it's not
+required.</li>
+<li><strong>Synchronous FFI</strong>: all flutter_rust_bridge functions are marked
+<code>#[frb(sync)]</code> and block on the internal Tokio runtime. This simplifies the
+Dart side (no async bridge complexity) while the Rust side handles concurrency
+internally. Flutter's UI thread stays responsive because Riverpod wraps calls
+in async providers.</li>
+<li><strong>Global singleton</strong>: a <code>Mutex&lt;Option&lt;EmbeddedNode&gt;&gt;</code> global ensures the node
+lifecycle is predictable — one start, one stop, no races. Mobile platforms
+kill processes aggressively, so simplicity in lifecycle management is a
+feature.</li>
+<li><strong>Flat FFI types</strong>: no Rust abstractions leak across the FFI boundary. Every
+type is a plain struct with strings and numbers. This makes the auto-generated
+Dart bindings reliable and easy to debug.</li>
+<li><strong>Three-screen onboarding</strong>: identity creation is the only required step. No
+email, no password, no server registration. The app generates a cryptographic
+identity locally and is ready to use.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4: Resilience and Scale</strong> — Advanced NAT traversal (STUN/TURN),
+Shamir's Secret Sharing for heirs, sealed tesseras with time-lock encryption,
+performance tuning, security audits, OS packaging for
+Alpine/Arch/Debian/FreeBSD/OpenBSD</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — Public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>The infrastructure is complete. The network exists, replication works, and now
+anyone with a phone can participate. What remains is hardening what we have and
+opening it to the world.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase3-api-and-apps/index.html.gz b/news/phase3-api-and-apps/index.html.gz
new file mode 100644
index 0000000..4d367bc
--- /dev/null
+++ b/news/phase3-api-and-apps/index.html.gz
Binary files differ
diff --git a/news/phase4-encryption-sealed/index.html b/news/phase4-encryption-sealed/index.html
new file mode 100644
index 0000000..dd7b5eb
--- /dev/null
+++ b/news/phase4-encryption-sealed/index.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Encryption and Sealed Tesseras — Tesseras</title>
+ <meta name="description" content="Tesseras now supports private and sealed memories with hybrid post-quantum encryption — AES-256-GCM, X25519 + ML-KEM-768, and time-lock key publication.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Encryption and Sealed Tesseras">
+ <meta property="og:description" content="Tesseras now supports private and sealed memories with hybrid post-quantum encryption — AES-256-GCM, X25519 + ML-KEM-768, and time-lock key publication.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Encryption and Sealed Tesseras">
+ <meta name="twitter:description" content="Tesseras now supports private and sealed memories with hybrid post-quantum encryption — AES-256-GCM, X25519 + ML-KEM-768, and time-lock key publication.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-encryption-sealed&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Encryption and Sealed Tesseras</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>Some memories are not meant for everyone. A private journal, a letter to be
+opened in 2050, a family secret sealed until the grandchildren are old enough.
+Until now, every tessera on the network was open. Phase 4 changes that: Tesseras
+now encrypts private and sealed content with a hybrid cryptographic scheme
+designed to resist both classical and quantum attacks.</p>
+<p>The principle remains the same — encrypt as little as possible. Public memories
+need availability, not secrecy. But when someone creates a private or sealed
+tessera, the content is now locked behind AES-256-GCM encryption with keys
+protected by a hybrid key encapsulation mechanism combining X25519 and
+ML-KEM-768. Both algorithms must be broken to access the content.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>AES-256-GCM encryptor</strong> (<code>tesseras-crypto/src/encryption.rs</code>) — Symmetric
+content encryption with random 12-byte nonces and authenticated associated data
+(AAD). The AAD binds ciphertext to its context: for private tesseras, the
+content hash is included; for sealed tesseras, both the content hash and the
+<code>open_after</code> timestamp are bound into the AAD. This means moving ciphertext
+between tesseras with different open dates causes decryption failure — you
+cannot trick the system into opening a sealed memory early by swapping its
+ciphertext into a tessera with an earlier seal date.</p>
+<p><strong>Hybrid Key Encapsulation Mechanism</strong> (<code>tesseras-crypto/src/kem.rs</code>) — Key
+exchange using X25519 (classical elliptic curve Diffie-Hellman) combined with
+ML-KEM-768 (the NIST-standardized post-quantum lattice-based KEM, formerly
+Kyber). Both shared secrets are combined via <code>blake3::derive_key</code> with a fixed
+context string ("tesseras hybrid kem v1") to produce a single 256-bit content
+encryption key. This follows the same "dual from day one" philosophy as the
+project's dual signing (Ed25519 + ML-DSA): if either algorithm is broken in the
+future, the other still protects the content.</p>
+<p><strong>Sealed Key Envelope</strong> (<code>tesseras-crypto/src/sealed.rs</code>) — Wraps a content
+encryption key using the hybrid KEM, so only the tessera owner can recover it.
+The KEM produces a transport key, which is XORed with the content key to produce
+a wrapped key stored alongside the KEM ciphertext. On unsealing, the owner
+decapsulates the KEM ciphertext to recover the transport key, then XORs again to
+recover the content key.</p>
+<p><strong>Key Publication</strong> (<code>tesseras-crypto/src/sealed.rs</code>) — A standalone signed
+artifact for publishing a sealed tessera's content key after its <code>open_after</code>
+date has passed. The owner signs the content key, tessera hash, and publication
+timestamp with their dual keys (Ed25519, with ML-DSA placeholder). The manifest
+stays immutable — the key publication is a separate document. Other nodes verify
+the signature against the owner's public key before using the published key to
+decrypt the content.</p>
+<p><strong>EncryptionContext</strong> (<code>tesseras-core/src/enums.rs</code>) — A domain type that
+represents the AAD context for encryption. It lives in tesseras-core rather than
+tesseras-crypto because it's a domain concept (not a crypto implementation
+detail). The <code>to_aad_bytes()</code> method produces deterministic serialization: a tag
+byte (0x00 for Private, 0x01 for Sealed), followed by the content hash, and for
+Sealed, the <code>open_after</code> timestamp as little-endian i64.</p>
+<p><strong>Domain validation</strong> (<code>tesseras-core/src/service.rs</code>) —
+<code>TesseraService::create()</code> now rejects Sealed and Private tesseras that don't
+provide encryption keys. This is a domain-level validation: the service layer
+enforces that you cannot create a sealed memory without the cryptographic
+machinery to protect it. The error message is clear: "missing encryption keys
+for visibility sealed until 2050-01-01."</p>
+<p><strong>Core type updates</strong> — <code>TesseraIdentity</code> now includes an optional
+<code>encryption_public: Option&lt;HybridEncryptionPublic&gt;</code> field containing both the
+X25519 and ML-KEM-768 public keys. <code>KeyAlgorithm</code> gained <code>X25519</code> and <code>MlKem768</code>
+variants. The identity filesystem layout now supports <code>node.x25519.key</code>/<code>.pub</code>
+and <code>node.mlkem768.key</code>/<code>.pub</code>.</p>
+<p><strong>Testing</strong> — 8 unit tests for AES-256-GCM (roundtrip, wrong key, tampered
+ciphertext, wrong AAD, cross-context decryption failure, unique nonces, plus 2
+property-based tests for arbitrary payloads and nonce uniqueness). 5 unit tests
+for HybridKem (roundtrip, wrong keypair, tampered X25519, KDF determinism, plus
+1 property-based test). 4 unit tests for SealedKeyEnvelope and KeyPublication. 2
+integration tests covering the complete sealed and private tessera lifecycle:
+generate keys, create content key, encrypt, seal, unseal, decrypt, publish key,
+and verify — the full cycle.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Hybrid KEM from day one</strong>: X25519 + ML-KEM-768 follows the same philosophy
+as dual signing. We don't know which cryptographic assumptions will hold over
+millennia, so we combine classical and post-quantum algorithms. The cost is
+~1.2 KB of additional key material per identity — trivial compared to the
+photos and videos in a tessera.</li>
+<li><strong>BLAKE3 for KDF</strong>: rather than adding <code>hkdf</code> + <code>sha2</code> as new dependencies, we
+use <code>blake3::derive_key</code> with a fixed context string. BLAKE3's key derivation
+mode is specifically designed for this use case, and the project already
+depends on BLAKE3 for content hashing.</li>
+<li><strong>Immutable manifests</strong>: when a sealed tessera's <code>open_after</code> date passes, the
+content key is published as a separate signed artifact (<code>KeyPublication</code>), not
+by modifying the manifest. This preserves the append-only, content-addressed
+nature of tesseras. The manifest was signed at creation time and never
+changes.</li>
+<li><strong>AAD binding prevents ciphertext swapping</strong>: the <code>EncryptionContext</code> binds
+both the content hash and (for sealed tesseras) the <code>open_after</code> timestamp
+into the AES-GCM authenticated data. An attacker who copies encrypted content
+from a "sealed until 2050" tessera into a "sealed until 2025" tessera will
+find that decryption fails — the AAD no longer matches.</li>
+<li><strong>XOR key wrapping</strong>: the sealed key envelope uses a simple XOR of the content
+key with the KEM-derived transport key, rather than an additional layer of
+AES-GCM. Since the transport key is a fresh random value from the KEM and is
+used exactly once, XOR is information-theoretically secure for this specific
+use case and avoids unnecessary complexity.</li>
+<li><strong>Domain validation, not storage validation</strong>: the "missing encryption keys"
+check lives in <code>TesseraService::create()</code>, not in the storage layer. This
+follows the hexagonal architecture pattern: domain rules are enforced at the
+service boundary, not scattered across adapters.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued: Resilience and Scale</strong> — Shamir's Secret Sharing for heir
+key distribution, advanced NAT traversal (STUN/TURN), performance tuning,
+security audits, OS packaging</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — Public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>Sealed tesseras make Tesseras a true time capsule. A father can now record a
+message for his unborn grandchild, seal it until 2060, and know that the
+cryptographic envelope will hold — even if the quantum computers of the future
+try to break it open early.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-encryption-sealed/index.html.gz b/news/phase4-encryption-sealed/index.html.gz
new file mode 100644
index 0000000..817b650
--- /dev/null
+++ b/news/phase4-encryption-sealed/index.html.gz
Binary files differ
diff --git a/news/phase4-institutional-onboarding/index.html b/news/phase4-institutional-onboarding/index.html
new file mode 100644
index 0000000..cbd9ac3
--- /dev/null
+++ b/news/phase4-institutional-onboarding/index.html
@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Institutional Node Onboarding — Tesseras</title>
+ <meta name="description" content="Libraries, archives, and museums can now join the Tesseras network as verified institutional nodes with DNS-based identity, full-text search indexes, and configurable storage pledges.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Institutional Node Onboarding">
+ <meta property="og:description" content="Libraries, archives, and museums can now join the Tesseras network as verified institutional nodes with DNS-based identity, full-text search indexes, and configurable storage pledges.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Institutional Node Onboarding">
+ <meta name="twitter:description" content="Libraries, archives, and museums can now join the Tesseras network as verified institutional nodes with DNS-based identity, full-text search indexes, and configurable storage pledges.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-institutional-onboarding&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Institutional Node Onboarding</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>A P2P network of individuals is fragile. Hard drives die, phones get lost,
+people lose interest. The long-term survival of humanity's memories depends on
+institutions — libraries, archives, museums, universities — that measure their
+lifetimes in centuries. Phase 4 continues with institutional node onboarding:
+verified organizations can now pledge storage, run searchable indexes, and
+participate in the network with a distinct identity.</p>
+<p>The design follows a principle of trust but verify: institutions identify
+themselves via DNS TXT records (the same mechanism used by SPF, DKIM, and DMARC
+for email), pledge a storage budget, and receive reciprocity exemptions so they
+can store fragments for others without expecting anything in return. In
+exchange, the network treats their fragments as higher-quality replicas and
+limits over-reliance on any single institution through diversity constraints.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>Capability bits</strong> (<code>tesseras-core/src/network.rs</code>) — Two new flags added to
+the <code>Capabilities</code> bitfield: <code>INSTITUTIONAL</code> (bit 7) and <code>SEARCH_INDEX</code> (bit 8).
+A new <code>institutional_default()</code> constructor returns the full Phase 2 capability
+set plus these two bits and <code>RELAY</code>. Normal nodes advertise <code>phase2_default()</code>
+which lacks institutional flags. Serialization roundtrip tests verify the new
+bits survive MessagePack encoding.</p>
+<p><strong>Search types</strong> (<code>tesseras-core/src/search.rs</code>) — Three new domain types for
+the search subsystem:</p>
+<ul>
+<li><code>SearchFilters</code> — query parameters: <code>memory_type</code>, <code>visibility</code>, <code>language</code>,
+<code>date_range</code>, <code>geo</code> (bounding box), <code>page</code>, <code>page_size</code></li>
+<li><code>SearchHit</code> — a single result: content hash plus a <code>MetadataExcerpt</code> (title,
+description, memory type, creation date, visibility, language, tags)</li>
+<li><code>GeoFilter</code> — bounding box with <code>min_lat</code>, <code>max_lat</code>, <code>min_lon</code>, <code>max_lon</code> for
+spatial queries</li>
+</ul>
+<p>All types derive <code>Serialize</code>/<code>Deserialize</code> for wire transport and
+<code>Clone</code>/<code>Debug</code> for diagnostics.</p>
+<p><strong>Institutional daemon config</strong> (<code>tesd/src/config.rs</code>) — A new <code>[institutional]</code>
+TOML section with <code>domain</code> (the DNS domain to verify), <code>pledge_bytes</code> (storage
+commitment in bytes), and <code>search_enabled</code> (toggle for the FTS5 index). The
+<code>to_dht_config()</code> method now sets <code>Capabilities::institutional_default()</code> when
+institutional config is present, so institutional nodes advertise the right
+capability bits in Pong responses.</p>
+<p><strong>DNS TXT verification</strong> (<code>tesd/src/institutional.rs</code>) — Async DNS resolution
+using <code>hickory-resolver</code> to verify institutional identity. The daemon looks up
+<code>_tesseras.&lt;domain&gt;</code> TXT records and parses key-value fields: <code>v</code> (version),
+<code>node</code> (hex-encoded node ID), and <code>pledge</code> (storage pledge in bytes).
+Verification checks:</p>
+<ol>
+<li>A TXT record exists at <code>_tesseras.&lt;domain&gt;</code></li>
+<li>The <code>node</code> field matches the daemon's own node ID</li>
+<li>The <code>pledge</code> field is present and valid</li>
+</ol>
+<p>On startup, the daemon attempts DNS verification. If it succeeds, the node runs
+with institutional capabilities. If it fails, the node logs a warning and
+downgrades to a normal full node — no crash, no manual intervention.</p>
+<p><strong>CLI setup command</strong> (<code>tesseras-cli/src/institutional.rs</code>) — A new
+<code>institutional setup</code> subcommand that guides operators through onboarding:</p>
+<ol>
+<li>Reads the node's identity from the data directory</li>
+<li>Prompts for domain name and pledge size</li>
+<li>Generates the exact DNS TXT record to add:
+<code>v=tesseras1 node=&lt;hex&gt; pledge=&lt;bytes&gt;</code></li>
+<li>Writes the institutional section to the daemon's config file</li>
+<li>Prints next steps: add the TXT record, restart the daemon</li>
+</ol>
+<p><strong>SQLite search index</strong> (<code>tesseras-storage</code>) — A migration
+(<code>003_institutional.sql</code>) that creates three structures:</p>
+<ul>
+<li><code>search_content</code> — an FTS5 virtual table for full-text search over tessera
+metadata (title, description, creator, tags, language)</li>
+<li><code>geo_index</code> — an R-tree virtual table for spatial bounding-box queries over
+latitude/longitude</li>
+<li><code>geo_map</code> — a mapping table linking R-tree row IDs to content hashes</li>
+</ul>
+<p>The <code>SqliteSearchIndex</code> adapter implements the <code>SearchIndex</code> port trait with
+<code>index_tessera()</code> (insert/update) and <code>search()</code> (query with filters). FTS5
+queries support natural language search; geo queries use R-tree <code>INTERSECT</code> for
+bounding box lookups. Results are ranked by FTS5 relevance score.</p>
+<p>The migration also adds an <code>is_institutional</code> column to the <code>reciprocity</code> table,
+handled idempotently via <code>pragma_table_info</code> checks (SQLite's
+<code>ALTER TABLE ADD COLUMN</code> lacks <code>IF NOT EXISTS</code>).</p>
+<p><strong>Reciprocity bypass</strong> (<code>tesseras-replication/src/service.rs</code>) — Institutional
+nodes are exempt from reciprocity checks. When <code>receive_fragment()</code> is called,
+if the sender's node ID is marked as institutional in the reciprocity ledger,
+the balance check is skipped entirely. This means institutions can store
+fragments for the entire network without needing to "earn" credits first — their
+DNS-verified identity and storage pledge serve as their credential.</p>
+<p><strong>Node-type diversity constraint</strong> (<code>tesseras-replication/src/distributor.rs</code>) —
+A new <code>apply_institutional_diversity()</code> function limits how many replicas of a
+single tessera can land on institutional nodes. The cap is
+<code>ceil(replication_factor / 3.5)</code> — with the default <code>r=7</code>, at most 2 of 7
+replicas go to institutions. This prevents the network from becoming dependent
+on a small number of large institutions: if a university's servers go down, at
+least 5 replicas remain on independent nodes.</p>
+<p><strong>DHT message extensions</strong> (<code>tesseras-dht/src/message.rs</code>) — Two new message
+variants:</p>
+<table><thead><tr><th>Message</th><th>Purpose</th></tr></thead><tbody>
+<tr><td><code>Search</code></td><td>Client sends query string, filters, and page number</td></tr>
+<tr><td><code>SearchResult</code></td><td>Institutional node responds with hits and total count</td></tr>
+</tbody></table>
+<p>The <code>encode()</code> function was switched from positional to named MessagePack
+serialization (<code>rmp_serde::to_vec_named</code>) to handle <code>SearchFilters</code>' optional
+fields correctly — positional encoding breaks when <code>skip_serializing_if</code> omits
+fields.</p>
+<p><strong>Prometheus metrics</strong> (<code>tesd/src/metrics.rs</code>) — Eight institutional-specific
+metrics:</p>
+<ul>
+<li><code>tesseras_institutional_pledge_bytes</code> — configured storage pledge</li>
+<li><code>tesseras_institutional_stored_bytes</code> — actual bytes stored</li>
+<li><code>tesseras_institutional_pledge_utilization_ratio</code> — stored/pledged ratio</li>
+<li><code>tesseras_institutional_peers_served</code> — unique peers served fragments</li>
+<li><code>tesseras_institutional_search_index_total</code> — tesseras in the search index</li>
+<li><code>tesseras_institutional_search_queries_total</code> — search queries received</li>
+<li><code>tesseras_institutional_dns_verification_status</code> — 1 if DNS verified, 0
+otherwise</li>
+<li><code>tesseras_institutional_dns_verification_last</code> — Unix timestamp of last
+verification</li>
+</ul>
+<p><strong>Integration tests</strong> — Two tests in
+<code>tesseras-replication/tests/integration.rs</code>:</p>
+<ul>
+<li><code>institutional_peer_bypasses_reciprocity</code> — verifies that an institutional
+peer with a massive deficit (-999,999 balance) is still allowed to store
+fragments, while a non-institutional peer with the same deficit is rejected</li>
+<li><code>institutional_node_accepts_fragment_despite_deficit</code> — full async test using
+<code>ReplicationService</code> with mocked DHT, fragment store, reciprocity ledger, and
+blob store: sends a fragment from an institutional sender and verifies it's
+accepted</li>
+</ul>
+<p>322 tests pass across the workspace. Clippy clean with <code>-D warnings</code>.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>DNS TXT over PKI or blockchain</strong>: DNS is universally deployed, universally
+understood, and already used for domain verification (SPF, DKIM, Let's
+Encrypt). Institutions already manage DNS. No certificate authority, no token,
+no on-chain transaction — just a TXT record. If an institution loses control
+of their domain, the verification naturally fails on the next check.</li>
+<li><strong>Graceful degradation on DNS failure</strong>: if DNS verification fails at startup,
+the daemon downgrades to a normal full node instead of refusing to start. This
+prevents operational incidents — a DNS misconfiguration shouldn't take a node
+offline.</li>
+<li><strong>Diversity cap at <code>ceil(r / 3.5)</code></strong>: with <code>r=7</code>, at most 2 replicas go to
+institutions. This is conservative — it ensures the network never depends on
+institutions for majority quorum, while still benefiting from their storage
+capacity and uptime.</li>
+<li><strong>Named MessagePack encoding</strong>: switching from positional to named encoding
+adds ~15% overhead per message but eliminates a class of serialization bugs
+when optional fields are present. The DHT is not bandwidth-constrained at the
+message level, so the tradeoff is worth it.</li>
+<li><strong>Reciprocity exemption over credit grants</strong>: rather than giving institutions
+a large initial credit balance (which is arbitrary and needs tuning), we
+exempt them entirely. Their DNS-verified identity and public storage pledge
+replace the bilateral reciprocity mechanism.</li>
+<li><strong>FTS5 + R-tree in SQLite</strong>: full-text search and spatial indexing are built
+into SQLite as loadable extensions. No external search engine (Elasticsearch,
+Meilisearch) needed. This keeps the deployment a single binary with a single
+database file — critical for institutional operators who may not have a DevOps
+team.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued</strong> — storage deduplication (content-addressable store with
+BLAKE3 keying), security audits, OS packaging (Alpine, Arch, Debian, OpenBSD,
+FreeBSD)</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration
+(FamilySearch, Ancestry), physical media export (M-DISC, microfilm, acid-free
+paper with QR), AI-assisted context</li>
+</ul>
+<p>Institutional onboarding closes a critical gap in Tesseras' preservation model.
+Individual nodes provide grassroots resilience — thousands of devices across the
+globe, each storing a few fragments. Institutional nodes provide anchoring —
+organizations with professional infrastructure, redundant storage, and
+multi-decade operational horizons. Together, they form a network where memories
+can outlast both individual devices and individual institutions.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-institutional-onboarding/index.html.gz b/news/phase4-institutional-onboarding/index.html.gz
new file mode 100644
index 0000000..92beb89
--- /dev/null
+++ b/news/phase4-institutional-onboarding/index.html.gz
Binary files differ
diff --git a/news/phase4-nat-traversal/index.html b/news/phase4-nat-traversal/index.html
new file mode 100644
index 0000000..1d7748b
--- /dev/null
+++ b/news/phase4-nat-traversal/index.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Punching Through NATs — Tesseras</title>
+ <meta name="description" content="Tesseras nodes can now discover their NAT type via STUN, coordinate UDP hole punching through introducers, and fall back to transparent relay forwarding when direct connectivity fails.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Punching Through NATs">
+ <meta property="og:description" content="Tesseras nodes can now discover their NAT type via STUN, coordinate UDP hole punching through introducers, and fall back to transparent relay forwarding when direct connectivity fails.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Punching Through NATs">
+ <meta name="twitter:description" content="Tesseras nodes can now discover their NAT type via STUN, coordinate UDP hole punching through introducers, and fall back to transparent relay forwarding when direct connectivity fails.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-nat-traversal&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Punching Through NATs</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>Most people's devices sit behind a NAT — a network address translator that lets
+them reach the internet but prevents incoming connections. For a P2P network,
+this is an existential problem: if two nodes behind NATs can't talk to each
+other, the network fragments. Phase 4 continues with a full NAT traversal stack:
+STUN-based discovery, coordinated hole punching, and relay fallback.</p>
+<p>The approach follows the same pattern as most battle-tested P2P systems (WebRTC,
+BitTorrent, IPFS): try the cheapest option first, escalate only when necessary.
+Direct connectivity costs nothing. Hole punching costs a few coordinated
+packets. Relaying costs sustained bandwidth from a third party. Tesseras tries
+them in that order.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>NatType classification</strong> (<code>tesseras-core/src/network.rs</code>) — A new <code>NatType</code>
+enum (Public, Cone, Symmetric, Unknown) added to the core domain layer. This
+type is shared across the entire stack: the STUN client writes it, the DHT
+advertises it in Pong messages, and the punch coordinator reads it to decide
+whether hole punching is even worth attempting (Cone-to-Cone works ~80% of the
+time; Symmetric-to-Symmetric almost never works).</p>
+<p><strong>STUN client</strong> (<code>tesseras-net/src/stun.rs</code>) — A minimal STUN implementation
+(RFC 5389 Binding Request/Response) that discovers a node's external address.
+The codec encodes 20-byte binding requests with a random transaction ID and
+decodes XOR-MAPPED-ADDRESS responses. The <code>discover_nat()</code> function queries
+multiple STUN servers in parallel (Google, Cloudflare by default), compares the
+mapped addresses, and classifies the NAT type:</p>
+<ul>
+<li>Same IP and port from all servers → <strong>Public</strong> (no NAT)</li>
+<li>Same mapped address from all servers → <strong>Cone</strong> (hole punching works)</li>
+<li>Different mapped addresses → <strong>Symmetric</strong> (hole punching unreliable)</li>
+<li>No responses → <strong>Unknown</strong></li>
+</ul>
+<p>Retries with exponential backoff and configurable timeouts. 12 tests covering
+codec roundtrips, all classification paths, and async loopback queries.</p>
+<p><strong>Signed punch coordination</strong> (<code>tesseras-net/src/punch.rs</code>) — Ed25519 signing
+and verification for <code>PunchIntro</code>, <code>RelayRequest</code>, and <code>RelayMigrate</code> messages.
+Every introduction is signed by the initiator with a 30-second timestamp window,
+preventing reflection attacks (where an attacker replays an old introduction to
+redirect traffic). The payload format is <code>target || external_addr || timestamp</code>
+— changing any field invalidates the signature. 6 unit tests plus 3
+property-based tests with proptest (arbitrary node IDs, ports, and session
+tokens).</p>
+<p><strong>Relay session manager</strong> (<code>tesseras-net/src/relay.rs</code>) — Manages transparent
+UDP relay sessions between NATed peers. Each session has a random 16-byte token;
+peers prefix their packets with the token, the relay strips it and forwards.
+Features:</p>
+<ul>
+<li>Bidirectional forwarding (A→R→B and B→R→A)</li>
+<li>Rate limiting: 256 KB/s for reciprocal peers, 64 KB/s for non-reciprocal</li>
+<li>10-minute maximum duration for bootstrap (non-reciprocal) sessions</li>
+<li>Address migration: when a peer's IP changes (Wi-Fi to cellular), a signed
+<code>RelayMigrate</code> updates the session without tearing it down</li>
+<li>Idle cleanup with configurable timeout</li>
+<li>8 unit tests plus 2 property-based tests</li>
+</ul>
+<p><strong>DHT message extensions</strong> (<code>tesseras-dht/src/message.rs</code>) — Seven new message
+variants added to the DHT protocol:</p>
+<table><thead><tr><th>Message</th><th>Purpose</th></tr></thead><tbody>
+<tr><td><code>PunchIntro</code></td><td>"I want to connect to node X, here's my signed external address"</td></tr>
+<tr><td><code>PunchRequest</code></td><td>Introducer forwards the request to the target</td></tr>
+<tr><td><code>PunchReady</code></td><td>Target confirms readiness, sends its external address</td></tr>
+<tr><td><code>RelayRequest</code></td><td>"Create a relay session to node X"</td></tr>
+<tr><td><code>RelayOffer</code></td><td>Relay responds with its address and session token</td></tr>
+<tr><td><code>RelayClose</code></td><td>Tear down a relay session</td></tr>
+<tr><td><code>RelayMigrate</code></td><td>Update session after network change</td></tr>
+</tbody></table>
+<p>The <code>Pong</code> message was extended with NAT metadata: <code>nat_type</code>,
+<code>relay_slots_available</code>, and <code>relay_bandwidth_used_kbps</code>. All new fields use
+<code>#[serde(default)]</code> for backward compatibility — old nodes ignore what they
+don't recognize, new nodes fall back to defaults. 9 new serialization roundtrip
+tests.</p>
+<p><strong>NatHandler trait and dispatch</strong> (<code>tesseras-dht/src/engine.rs</code>) — A new
+<code>NatHandler</code> async trait (5 methods) injected into the DHT engine, following the
+same dependency injection pattern as the existing <code>ReplicationHandler</code>. The
+engine's message dispatch loop now routes all punch/relay messages to the
+handler. This keeps the DHT engine protocol-agnostic while allowing the NAT
+traversal logic to live in <code>tesseras-net</code>.</p>
+<p><strong>Mobile reconnection types</strong> (<code>tesseras-embedded/src/reconnect.rs</code>) — A
+three-phase reconnection state machine for mobile devices:</p>
+<ol>
+<li><strong>QuicMigration</strong> (0-2s) — try QUIC connection migration for all active peers</li>
+<li><strong>ReStun</strong> (2-5s) — re-discover external address via STUN</li>
+<li><strong>ReEstablish</strong> (5-10s) — reconnect peers that migration couldn't save</li>
+</ol>
+<p>Peers are reconnected in priority order: bootstrap nodes first, then nodes
+holding our fragments, then nodes whose fragments we hold, then general DHT
+neighbors. A new <code>NetworkChanged</code> event variant was added to the FFI event
+stream so the Flutter app can show reconnection progress.</p>
+<p><strong>Daemon NAT configuration</strong> (<code>tesd/src/config.rs</code>) — A new <code>[nat]</code> section in
+the TOML config with STUN server list, relay toggle, max relay sessions,
+bandwidth limits (reciprocal vs bootstrap), and idle timeout. All fields have
+sensible defaults; relay is disabled by default.</p>
+<p><strong>Prometheus metrics</strong> (<code>tesseras-net/src/metrics.rs</code>) — 16 metrics across four
+subsystems:</p>
+<ul>
+<li><strong>STUN</strong>: requests, failures, latency histogram</li>
+<li><strong>Punch</strong>: attempts/successes/failures (by NAT type pair), latency histogram</li>
+<li><strong>Relay</strong>: active sessions, total sessions, bytes forwarded, idle timeouts,
+rate limit hits</li>
+<li><strong>Reconnect</strong>: network changes, attempts/successes by phase, duration
+histogram</li>
+</ul>
+<p>6 tests verifying registration, increment, label cardinality, and
+double-registration detection.</p>
+<p><strong>Integration tests</strong> — Two end-to-end tests using <code>MemTransport</code> (in-memory
+simulated network):</p>
+<ul>
+<li><code>punch_integration.rs</code> — Full 3-node hole-punch flow: A sends signed
+<code>PunchIntro</code> to introducer I, I verifies and forwards <code>PunchRequest</code> to B, B
+verifies the original signature and sends <code>PunchReady</code> back, A and B exchange
+messages directly. Also tests that a bad signature is correctly rejected.</li>
+<li><code>relay_integration.rs</code> — Full 3-node relay flow: A requests relay from R, R
+creates session and sends <code>RelayOffer</code> to both peers, A and B exchange
+token-prefixed packets through R, A migrates to a new address mid-session, A
+closes the session, and the test verifies the session is torn down and further
+forwarding fails.</li>
+</ul>
+<p><strong>Property tests</strong> — 7 proptest-based tests covering: signature round-trips for
+all three signed message types (arbitrary node IDs, ports, tokens), NAT
+classification determinism (same inputs always produce same output), STUN
+binding request validity, session token uniqueness, and relay rejection of
+too-short packets.</p>
+<p><strong>Justfile targets</strong> — <code>just test-nat</code> runs all NAT traversal tests across
+<code>tesseras-net</code> and <code>tesseras-dht</code>. <code>just test-chaos</code> is a placeholder for future
+Docker Compose chaos tests with <code>tc netem</code>.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>STUN over TURN</strong>: we implement STUN (discovery) and custom relay rather than
+full TURN. TURN requires authenticated allocation and is designed for media
+relay; our relay is simpler — token-prefixed UDP forwarding with rate limits.
+This keeps the protocol minimal and avoids depending on external TURN servers.</li>
+<li><strong>Signatures on introductions</strong>: every <code>PunchIntro</code> is signed by the
+initiator. Without this, an attacker could send forged introductions to
+redirect a node's hole-punch attempts to an attacker-controlled address (a
+reflection attack). The 30-second timestamp window limits replay.</li>
+<li><strong>Reciprocal bandwidth tiers</strong>: relay nodes give 4x more bandwidth (256 vs 64
+KB/s) to peers with good reciprocity scores. This incentivizes nodes to store
+fragments for others — if you contribute, you get better relay service when
+you need it.</li>
+<li><strong>Backward-compatible Pong extension</strong>: new NAT fields in <code>Pong</code> use
+<code>#[serde(default)]</code> and <code>Option&lt;T&gt;</code>. Old nodes that don't understand these
+fields simply skip them during deserialization. No protocol version bump
+needed.</li>
+<li><strong>NatHandler as async trait</strong>: the NAT traversal logic is injected into the
+DHT engine via a trait, just like <code>ReplicationHandler</code>. This keeps the DHT
+engine focused on routing and peer management, and allows the NAT
+implementation to be swapped or disabled without touching core DHT code.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued</strong> — performance tuning (connection pooling, fragment
+caching, SQLite WAL), security audits, institutional node onboarding, OS
+packaging</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>With NAT traversal, Tesseras can connect nodes regardless of their network
+topology. Public nodes talk directly. Cone-NATed nodes punch through with an
+introducer's help. Symmetric-NATed or firewalled nodes relay through willing
+peers. The network adapts to the real world, where most devices are behind a NAT
+and network conditions change constantly.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-nat-traversal/index.html.gz b/news/phase4-nat-traversal/index.html.gz
new file mode 100644
index 0000000..57b93d2
--- /dev/null
+++ b/news/phase4-nat-traversal/index.html.gz
Binary files differ
diff --git a/news/phase4-performance-tuning/index.html b/news/phase4-performance-tuning/index.html
new file mode 100644
index 0000000..5426996
--- /dev/null
+++ b/news/phase4-performance-tuning/index.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Performance Tuning — Tesseras</title>
+ <meta name="description" content="SQLite WAL mode with centralized pragma configuration, LRU fragment caching, QUIC connection pool lifecycle management, and attestation hot path optimization.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Performance Tuning">
+ <meta property="og:description" content="SQLite WAL mode with centralized pragma configuration, LRU fragment caching, QUIC connection pool lifecycle management, and attestation hot path optimization.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Performance Tuning">
+ <meta name="twitter:description" content="SQLite WAL mode with centralized pragma configuration, LRU fragment caching, QUIC connection pool lifecycle management, and attestation hot path optimization.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-performance-tuning&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Performance Tuning</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>A P2P network that can traverse NATs but chokes on its own I/O is not much use.
+Phase 4 continues with performance tuning: centralizing database configuration,
+caching fragment blobs in memory, managing QUIC connection lifecycles, and
+eliminating unnecessary disk reads from the attestation hot path.</p>
+<p>The guiding principle was the same as the rest of Tesseras: do the simplest
+thing that actually works. No custom allocators, no lock-free data structures,
+no premature complexity. A centralized <code>StorageConfig</code>, an LRU cache, a
+connection reaper, and a targeted fix to avoid re-reading blobs that were
+already checksummed.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>Centralized SQLite configuration</strong> (<code>tesseras-storage/src/database.rs</code>) — A
+new <code>StorageConfig</code> struct and <code>open_database()</code> / <code>open_in_memory()</code> functions
+that apply all SQLite pragmas in one place: WAL journal mode, foreign keys,
+synchronous mode (NORMAL by default, FULL for unstable hardware like RPi + SD
+card), busy timeout, page cache size, and WAL autocheckpoint interval.
+Previously, each call site opened a connection and applied pragmas ad hoc. Now
+the daemon, CLI, and tests all go through the same path. 7 tests covering
+foreign keys, busy timeout, journal mode, migrations, synchronous modes, and
+on-disk WAL file creation.</p>
+<p><strong>LRU fragment cache</strong> (<code>tesseras-storage/src/cache.rs</code>) — A
+<code>CachedFragmentStore</code> that wraps any <code>FragmentStore</code> with a byte-aware LRU
+cache. Fragment blobs are cached on read and invalidated on write or delete.
+When the cache exceeds its configured byte limit, the least recently used
+entries are evicted. The cache is transparent: it implements <code>FragmentStore</code>
+itself, so the rest of the stack doesn't know it's there. Optional Prometheus
+metrics track hits, misses, and current byte usage. 3 tests: cache hit avoids
+inner read, store invalidates cache, eviction when over max bytes.</p>
+<p><strong>Prometheus storage metrics</strong> (<code>tesseras-storage/src/metrics.rs</code>) — A
+<code>StorageMetrics</code> struct with three counters/gauges: <code>fragment_cache_hits</code>,
+<code>fragment_cache_misses</code>, and <code>fragment_cache_bytes</code>. Registered with the
+Prometheus registry and wired into the fragment cache via <code>with_metrics()</code>.</p>
+<p><strong>Attestation hot path fix</strong> (<code>tesseras-replication/src/service.rs</code>) — The
+attestation flow previously read every fragment blob from disk and recomputed
+its BLAKE3 checksum. Since <code>list_fragments()</code> already returns <code>FragmentId</code> with
+a stored checksum, the fix is trivial: use <code>frag.checksum</code> instead of
+<code>blake3::hash(&amp;data)</code>. This eliminates one disk read per fragment during
+attestation — for a tessera with 100 fragments, that's 100 fewer reads. A test
+with <code>expect_read_fragment().never()</code> verifies no blob reads happen during
+attestation.</p>
+<p><strong>QUIC connection pool lifecycle</strong> (<code>tesseras-net/src/quinn_transport.rs</code>) — A
+<code>PoolConfig</code> struct controlling max connections, idle timeout, and reaper
+interval. <code>PooledConnection</code> wraps each <code>quinn::Connection</code> with a <code>last_used</code>
+timestamp. When the pool reaches capacity, the oldest idle connection is evicted
+before opening a new one. A background reaper task (Tokio spawn) periodically
+closes connections that have been idle beyond the timeout. 4 new pool metrics:
+<code>tesseras_conn_pool_size</code>, <code>pool_hits_total</code>, <code>pool_misses_total</code>,
+<code>pool_evictions_total</code>.</p>
+<p><strong>Daemon integration</strong> (<code>tesd/src/config.rs</code>, <code>main.rs</code>) — A new <code>[performance]</code>
+section in the TOML config with fields for SQLite cache size, synchronous mode,
+busy timeout, fragment cache size, max connections, idle timeout, and reaper
+interval. The daemon's <code>main()</code> now calls <code>open_database()</code> with the configured
+<code>StorageConfig</code>, wraps <code>FsFragmentStore</code> with <code>CachedFragmentStore</code>, and binds
+QUIC with the configured <code>PoolConfig</code>. The direct <code>rusqlite</code> dependency was
+removed from the daemon crate.</p>
+<p><strong>CLI migration</strong> (<code>tesseras-cli/src/commands/init.rs</code>, <code>create.rs</code>) — Both
+<code>init</code> and <code>create</code> commands now use <code>tesseras_storage::open_database()</code> with
+the default <code>StorageConfig</code> instead of opening raw <code>rusqlite</code> connections. The
+<code>rusqlite</code> dependency was removed from the CLI crate.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>Decorator pattern for caching</strong>: <code>CachedFragmentStore</code> wraps
+<code>Box&lt;dyn FragmentStore&gt;</code> and implements <code>FragmentStore</code> itself. This means
+caching is opt-in, composable, and invisible to consumers. The daemon enables
+it; tests can skip it.</li>
+<li><strong>Byte-aware eviction</strong>: the LRU cache tracks total bytes, not entry count.
+Fragment blobs vary wildly in size (a 4KB text fragment vs a 2MB photo shard),
+so counting entries would give a misleading picture of memory usage.</li>
+<li><strong>No connection pool crate</strong>: instead of pulling in a generic pool library,
+the connection pool is a thin wrapper around
+<code>DashMap&lt;SocketAddr, PooledConnection&gt;</code> with a Tokio reaper. QUIC connections
+are multiplexed, so the "pool" is really about lifecycle management (idle
+cleanup, max connections) rather than borrowing/returning.</li>
+<li><strong>Stored checksums over re-reads</strong>: the attestation fix is intentionally
+minimal — one line changed, one disk read removed per fragment. The checksums
+were already stored in SQLite by <code>store_fragment()</code>, they just weren't being
+used.</li>
+<li><strong>Centralized pragma configuration</strong>: a single <code>StorageConfig</code> struct replaces
+scattered <code>PRAGMA</code> calls. The <code>sqlite_synchronous_full</code> flag exists
+specifically for Raspberry Pi deployments where the kernel can crash and lose
+un-checkpointed WAL transactions.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued</strong> — Shamir's Secret Sharing for heirs, sealed tesseras
+(time-lock encryption), security audits, institutional node onboarding,
+storage deduplication, OS packaging</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>With performance tuning in place, Tesseras handles the common case efficiently:
+fragment reads hit the LRU cache, attestation skips disk I/O, idle QUIC
+connections are reaped automatically, and SQLite is configured consistently
+across the entire stack. The next steps focus on cryptographic features (Shamir,
+time-lock) and hardening for production deployment.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-performance-tuning/index.html.gz b/news/phase4-performance-tuning/index.html.gz
new file mode 100644
index 0000000..98c6079
--- /dev/null
+++ b/news/phase4-performance-tuning/index.html.gz
Binary files differ
diff --git a/news/phase4-shamir-heir-recovery/index.html b/news/phase4-shamir-heir-recovery/index.html
new file mode 100644
index 0000000..3acf79a
--- /dev/null
+++ b/news/phase4-shamir-heir-recovery/index.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing — Tesseras</title>
+ <meta name="description" content="Tesseras now lets you split your cryptographic identity into shares distributed to trusted heirs — any threshold of them can reconstruct your keys, but fewer reveal nothing.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing">
+ <meta property="og:description" content="Tesseras now lets you split your cryptographic identity into shares distributed to trusted heirs — any threshold of them can reconstruct your keys, but fewer reveal nothing.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing">
+ <meta name="twitter:description" content="Tesseras now lets you split your cryptographic identity into shares distributed to trusted heirs — any threshold of them can reconstruct your keys, but fewer reveal nothing.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-shamir-heir-recovery&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Heir Key Recovery with Shamir&#x27;s Secret Sharing</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>What happens to your memories when you die? Until now, Tesseras could preserve
+content across millennia — but the private and sealed keys died with their
+owner. Phase 4 continues with a solution: Shamir's Secret Sharing, a
+cryptographic scheme that lets you split your identity into shares and
+distribute them to the people you trust most.</p>
+<p>The math is elegant: you choose a threshold T and a total N. Any T shares
+reconstruct the full secret; T-1 shares reveal absolutely nothing. This is not
+"almost nothing" — it is information-theoretically secure. An attacker with one
+fewer share than the threshold has exactly zero bits of information about the
+secret, no matter how much computing power they have.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>GF(256) finite field arithmetic</strong> (<code>tesseras-crypto/src/shamir/gf256.rs</code>) —
+Shamir's Secret Sharing requires arithmetic in a finite field. We implement
+GF(256) using the same irreducible polynomial as AES (x^8 + x^4 + x^3 + x + 1),
+with compile-time lookup tables for logarithm and exponentiation. All operations
+are constant-time via table lookups — no branches on secret data. The module
+includes Horner's method for polynomial evaluation and Lagrange interpolation at
+x=0 for secret recovery. 233 lines, exhaustively tested: all 256 elements for
+identity/inverse properties, commutativity, and associativity.</p>
+<p><strong>ShamirSplitter</strong> (<code>tesseras-crypto/src/shamir/mod.rs</code>) — The core
+split/reconstruct API. <code>split()</code> takes a secret byte slice, a configuration
+(threshold T, total N), and the owner's Ed25519 public key. For each byte of the
+secret, it constructs a random polynomial of degree T-1 over GF(256) with the
+secret byte as the constant term, then evaluates it at N distinct points.
+<code>reconstruct()</code> takes T or more shares and recovers the secret via Lagrange
+interpolation. Both operations include extensive validation: threshold bounds,
+session consistency, owner fingerprint matching, and BLAKE3 checksum
+verification.</p>
+<p><strong>HeirShare format</strong> — Each share is a self-contained, serializable artifact
+with:</p>
+<ul>
+<li>Format version (v1) for forward compatibility</li>
+<li>Share index (1..N) and threshold/total metadata</li>
+<li>Session ID (random 8 bytes) — prevents mixing shares from different split
+sessions</li>
+<li>Owner fingerprint (first 8 bytes of BLAKE3 hash of the Ed25519 public key)</li>
+<li>Share data (the Shamir y-values, same length as the secret)</li>
+<li>BLAKE3 checksum over all preceding fields</li>
+</ul>
+<p>Shares are serialized in two formats: <strong>MessagePack</strong> (compact binary, for
+programmatic use) and <strong>base64 text</strong> (human-readable, for printing and physical
+storage). The text format includes a header with metadata and delimiters:</p>
+<pre><code>--- TESSERAS HEIR SHARE ---
+Format: v1
+Owner: a1b2c3d4e5f6a7b8 (fingerprint)
+Share: 1 of 3 (threshold: 2)
+Session: 9f8e7d6c5b4a3210
+Created: 2026-02-15
+
+&lt;base64-encoded MessagePack data&gt;
+--- END HEIR SHARE ---
+</code></pre>
+<p>This format is designed to be printed on paper, stored in a safe deposit box, or
+engraved on metal. The header is informational — only the base64 payload is
+parsed during reconstruction.</p>
+<p><strong>CLI integration</strong> (<code>tesseras-cli/src/commands/heir.rs</code>) — Three new
+subcommands:</p>
+<ul>
+<li><code>tes heir create</code> — splits your Ed25519 identity into heir shares. Prompts for
+confirmation (your full identity is at stake), generates both <code>.bin</code> and
+<code>.txt</code> files for each share, and writes <code>heir_meta.json</code> to your identity
+directory.</li>
+<li><code>tes heir reconstruct</code> — loads share files (auto-detects binary vs text
+format), validates consistency, reconstructs the secret, derives the Ed25519
+keypair, and optionally installs it to <code>~/.tesseras/identity/</code> (with automatic
+backup of the existing identity).</li>
+<li><code>tes heir info</code> — displays share metadata and verifies the checksum without
+exposing any secret material.</li>
+</ul>
+<p><strong>Secret blob format</strong> — Identity keys are serialized into a versioned blob
+before splitting: a version byte (0x01), a flags byte (0x00 for Ed25519-only),
+followed by the 32-byte Ed25519 secret key. This leaves room for future
+expansion when X25519 and ML-KEM-768 private keys are integrated into the heir
+share system.</p>
+<p><strong>Testing</strong> — 20 unit tests for ShamirSplitter (roundtrip, all share
+combinations, insufficient shares, wrong owner, wrong session, threshold-1
+boundary, large secrets up to ML-KEM-768 key size). 7 unit tests for GF(256)
+arithmetic (exhaustive field properties). 3 property-based tests with proptest
+(arbitrary secrets up to 5000 bytes, arbitrary T-of-N configurations,
+information-theoretic security verification). Serialization roundtrip tests for
+both MessagePack and base64 text formats. 2 integration tests covering the
+complete heir lifecycle: generate identity, split into shares, serialize,
+deserialize, reconstruct, verify keypair, and sign/verify with reconstructed
+keys.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>GF(256) over GF(prime)</strong>: we use GF(256) rather than a prime field because
+it maps naturally to bytes — each element is a single byte, each share is the
+same length as the secret. No big-integer arithmetic, no modular reduction, no
+padding. This is the same approach used by most real-world Shamir
+implementations including SSSS and Hashicorp Vault.</li>
+<li><strong>Compile-time lookup tables</strong>: the LOG and EXP tables for GF(256) are
+computed at compile time using <code>const fn</code>. This means zero runtime
+initialization cost and constant-time operations via table lookups rather than
+loops.</li>
+<li><strong>Session ID prevents cross-session mixing</strong>: each call to <code>split()</code> generates
+a fresh random session ID. If an heir accidentally uses shares from two
+different split sessions (e.g., before and after a key rotation),
+reconstruction fails cleanly with a validation error rather than producing
+garbage output.</li>
+<li><strong>BLAKE3 checksums detect corruption</strong>: each share includes a BLAKE3 checksum
+over its contents. This catches bit rot, transmission errors, and accidental
+truncation before any reconstruction attempt. A share printed on paper and
+scanned back via OCR will fail the checksum if a single character is wrong.</li>
+<li><strong>Owner fingerprint for identification</strong>: shares include the first 8 bytes of
+BLAKE3(Ed25519 public key) as a fingerprint. This lets heirs verify which
+identity a share belongs to without revealing the full public key. During
+reconstruction, the fingerprint is cross-checked against the recovered key.</li>
+<li><strong>Dual format for resilience</strong>: both binary (MessagePack) and text (base64)
+formats are generated because physical media has different failure modes than
+digital storage. A USB drive might fail; paper survives. A QR code might be
+unreadable; base64 text can be manually typed.</li>
+<li><strong>Blob versioning</strong>: the secret is wrapped in a versioned blob (version +
+flags + key material) so future versions can include additional keys (X25519,
+ML-KEM-768) without breaking backward compatibility with existing shares.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued: Resilience and Scale</strong> — advanced NAT traversal
+(STUN/TURN), performance tuning (connection pooling, fragment caching, SQLite
+WAL), security audits, institutional node onboarding, OS packaging</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>With Shamir's Secret Sharing, Tesseras closes the last critical gap in long-term
+preservation. Your memories survive infrastructure failures through erasure
+coding. Your privacy survives quantum computers through hybrid encryption. And
+now, your identity survives you — passed on to the people you chose, requiring
+their cooperation to unlock what you left behind.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-shamir-heir-recovery/index.html.gz b/news/phase4-shamir-heir-recovery/index.html.gz
new file mode 100644
index 0000000..f4b1598
--- /dev/null
+++ b/news/phase4-shamir-heir-recovery/index.html.gz
Binary files differ
diff --git a/news/phase4-storage-deduplication/index.html b/news/phase4-storage-deduplication/index.html
new file mode 100644
index 0000000..d499b4a
--- /dev/null
+++ b/news/phase4-storage-deduplication/index.html
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Storage Deduplication — Tesseras</title>
+ <meta name="description" content="A new content-addressable storage layer eliminates duplicate data across tesseras, reducing disk usage and enabling automatic garbage collection.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Storage Deduplication">
+ <meta property="og:description" content="A new content-addressable storage layer eliminates duplicate data across tesseras, reducing disk usage and enabling automatic garbage collection.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Storage Deduplication">
+ <meta name="twitter:description" content="A new content-addressable storage layer eliminates duplicate data across tesseras, reducing disk usage and enabling automatic garbage collection.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-storage-deduplication&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Storage Deduplication</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>When multiple tesseras share the same photo, the same audio clip, or the same
+fragment data, the old storage layer kept separate copies of each. On a node
+storing thousands of tesseras for the network, this duplication adds up fast.
+Phase 4 continues with storage deduplication: a content-addressable store (CAS)
+that ensures every unique piece of data is stored exactly once on disk,
+regardless of how many tesseras reference it.</p>
+<p>The design is simple and proven: hash the content with BLAKE3, use the hash as
+the filename, and maintain a reference count in SQLite. When two tesseras
+include the same 5 MB photo, one file exists on disk with a refcount of 2. When
+one tessera is deleted, the refcount drops to 1 and the file stays. When the
+last reference is released, a periodic sweep cleans up the orphan.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>CAS schema migration</strong> (<code>tesseras-storage/migrations/004_dedup.sql</code>) — Three
+new tables:</p>
+<ul>
+<li><code>cas_objects</code> — tracks every object in the store: BLAKE3 hash (primary key),
+byte size, reference count, and creation timestamp</li>
+<li><code>blob_refs</code> — maps logical blob identifiers (tessera hash + memory hash +
+filename) to CAS hashes, replacing the old filesystem path convention</li>
+<li><code>fragment_refs</code> — maps logical fragment identifiers (tessera hash + fragment
+index) to CAS hashes, replacing the old <code>fragments/</code> directory layout</li>
+</ul>
+<p>Indexes on the hash columns ensure O(1) lookups during reads and reference
+counting.</p>
+<p><strong>CasStore</strong> (<code>tesseras-storage/src/cas.rs</code>) — The core content-addressable
+storage engine. Files are stored under a two-level prefix directory:
+<code>&lt;root&gt;/&lt;2-char-hex-prefix&gt;/&lt;full-hash&gt;.blob</code>. The store provides five
+operations:</p>
+<ul>
+<li><code>put(hash, data)</code> — writes data to disk if not already present, increments
+refcount. Returns whether a dedup hit occurred.</li>
+<li><code>get(hash)</code> — reads data from disk by hash</li>
+<li><code>release(hash)</code> — decrements refcount. If it reaches zero, the on-disk file is
+deleted immediately.</li>
+<li><code>contains(hash)</code> — checks existence without reading</li>
+<li><code>ref_count(hash)</code> — returns the current reference count</li>
+</ul>
+<p>All operations are atomic within a single SQLite transaction. The refcount is
+the source of truth — if the refcount says the object exists, the file must be
+on disk.</p>
+<p><strong>CAS-backed FsBlobStore</strong> (<code>tesseras-storage/src/blob.rs</code>) — Rewritten to
+delegate all storage to the CAS. When a blob is written, its BLAKE3 hash is
+computed and passed to <code>cas.put()</code>. A row in <code>blob_refs</code> maps the logical path
+(tessera + memory + filename) to the CAS hash. Reads look up the CAS hash via
+<code>blob_refs</code> and fetch from <code>cas.get()</code>. Deleting a tessera releases all its blob
+references in a single transaction.</p>
+<p><strong>CAS-backed FsFragmentStore</strong> (<code>tesseras-storage/src/fragment.rs</code>) — Same
+pattern for erasure-coded fragments. Each fragment's BLAKE3 checksum is already
+computed during Reed-Solomon encoding, so it's used directly as the CAS key.
+Fragment verification now checks the CAS hash instead of recomputing from
+scratch — if the CAS says the data is intact, it is.</p>
+<p><strong>Sweep garbage collector</strong> (<code>cas.rs:sweep()</code>) — A periodic GC pass that handles
+three edge cases the normal refcount path can't:</p>
+<ol>
+<li><strong>Orphan files</strong> — files on disk with no corresponding row in <code>cas_objects</code>.
+Can happen after a crash mid-write. Files younger than 1 hour are skipped
+(grace period for in-flight writes); older orphans are deleted.</li>
+<li><strong>Leaked refcounts</strong> — rows in <code>cas_objects</code> with refcount zero that weren't
+cleaned up (e.g., if the process died between decrementing and deleting).
+These rows are removed.</li>
+<li><strong>Idempotent</strong> — running sweep twice produces the same result.</li>
+</ol>
+<p>The sweep is wired into the existing repair loop in <code>tesseras-replication</code>, so
+it runs automatically every 24 hours alongside fragment health checks.</p>
+<p><strong>Migration from old layout</strong> (<code>tesseras-storage/src/migration.rs</code>) — A
+copy-first migration strategy that moves data from the old directory-based
+layout (<code>blobs/&lt;tessera&gt;/&lt;memory&gt;/&lt;file&gt;</code> and
+<code>fragments/&lt;tessera&gt;/&lt;index&gt;.shard</code>) into the CAS. The migration:</p>
+<ol>
+<li>Checks the storage version in <code>storage_meta</code> (version 1 = old layout, version
+2 = CAS)</li>
+<li>Walks the old <code>blobs/</code> and <code>fragments/</code> directories</li>
+<li>Computes BLAKE3 hashes and inserts into CAS via <code>put()</code> — duplicates are
+automatically deduplicated</li>
+<li>Creates corresponding <code>blob_refs</code> / <code>fragment_refs</code> entries</li>
+<li>Removes old directories only after all data is safely in CAS</li>
+<li>Updates the storage version to 2</li>
+</ol>
+<p>The migration runs on daemon startup, is idempotent (safe to re-run), and
+reports statistics: files migrated, duplicates found, bytes saved.</p>
+<p><strong>Prometheus metrics</strong> (<code>tesseras-storage/src/metrics.rs</code>) — Ten new metrics for
+observability:</p>
+<table><thead><tr><th>Metric</th><th>Description</th></tr></thead><tbody>
+<tr><td><code>cas_objects_total</code></td><td>Total unique objects in the CAS</td></tr>
+<tr><td><code>cas_bytes_total</code></td><td>Total bytes stored</td></tr>
+<tr><td><code>cas_dedup_hits_total</code></td><td>Number of writes that found an existing object</td></tr>
+<tr><td><code>cas_bytes_saved_total</code></td><td>Bytes saved by deduplication</td></tr>
+<tr><td><code>cas_gc_refcount_deletions_total</code></td><td>Objects deleted when refcount reached zero</td></tr>
+<tr><td><code>cas_gc_sweep_orphans_cleaned_total</code></td><td>Orphan files removed by sweep</td></tr>
+<tr><td><code>cas_gc_sweep_leaked_refs_cleaned_total</code></td><td>Leaked refcount rows cleaned</td></tr>
+<tr><td><code>cas_gc_sweep_skipped_young_total</code></td><td>Young orphans skipped (grace period)</td></tr>
+<tr><td><code>cas_gc_sweep_duration_seconds</code></td><td>Time spent in sweep GC</td></tr>
+</tbody></table>
+<p><strong>Property-based tests</strong> — Two proptest tests verify CAS invariants under random
+inputs:</p>
+<ul>
+<li><code>refcount_matches_actual_refs</code> — after N random put/release operations, the
+refcount always matches the actual number of outstanding references</li>
+<li><code>cas_path_is_deterministic</code> — the same hash always produces the same
+filesystem path</li>
+</ul>
+<p><strong>Integration test updates</strong> — All integration tests across <code>tesseras-core</code>,
+<code>tesseras-replication</code>, <code>tesseras-embedded</code>, and <code>tesseras-cli</code> updated for the
+new CAS-backed constructors. Tamper-detection tests updated to work with the CAS
+directory layout.</p>
+<p>347 tests pass across the workspace. Clippy clean with <code>-D warnings</code>.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>BLAKE3 as CAS key</strong>: the content hash we already compute for integrity
+verification doubles as the deduplication key. No additional hashing step —
+the hash computed during <code>create</code> or <code>replicate</code> is reused as the CAS address.</li>
+<li><strong>SQLite refcount over filesystem reflinks</strong>: we considered using
+filesystem-level copy-on-write (reflinks on btrfs/XFS), but that would tie
+Tesseras to specific filesystems. SQLite refcounting works on any filesystem,
+including FAT32 on cheap USB drives and ext4 on Raspberry Pis.</li>
+<li><strong>Two-level hex prefix directories</strong>: storing all CAS objects in a flat
+directory would slow down filesystems with millions of entries. The
+<code>&lt;2-char prefix&gt;/</code> split limits any single directory to ~65k entries before a
+second prefix level is needed. This matches the approach used by Git's object
+store.</li>
+<li><strong>Grace period for orphan files</strong>: the sweep GC skips files younger than 1
+hour to avoid deleting objects that are being written by a concurrent
+operation. This is a pragmatic choice — it trades a small window of potential
+orphans for crash safety without requiring fsync or two-phase commit.</li>
+<li><strong>Copy-first migration</strong>: the migration copies data to CAS before removing old
+directories. If the process is interrupted, the old data is still intact and
+migration can be re-run. This is slower than moving files but guarantees no
+data loss.</li>
+<li><strong>Sweep in repair loop</strong>: rather than adding a separate GC timer, the CAS
+sweep piggybacks on the existing 24-hour repair loop. This keeps the daemon
+simple — one background maintenance cycle handles both fragment health and
+storage cleanup.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4 continued</strong> — security audits, OS packaging (Alpine, Arch, Debian,
+OpenBSD, FreeBSD)</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration
+(FamilySearch, Ancestry), physical media export (M-DISC, microfilm, acid-free
+paper with QR), AI-assisted context</li>
+</ul>
+<p>Storage deduplication completes the storage efficiency story for Tesseras. A
+node that stores fragments for thousands of users — common for institutional
+nodes and always-on full nodes — now pays the disk cost of unique data only.
+Combined with Reed-Solomon erasure coding (which already minimizes redundancy at
+the network level), the system achieves efficient storage at both the local and
+distributed layers.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-storage-deduplication/index.html.gz b/news/phase4-storage-deduplication/index.html.gz
new file mode 100644
index 0000000..7df051e
--- /dev/null
+++ b/news/phase4-storage-deduplication/index.html.gz
Binary files differ
diff --git a/news/phase4-wasm-browser-verification/index.html b/news/phase4-wasm-browser-verification/index.html
new file mode 100644
index 0000000..571e094
--- /dev/null
+++ b/news/phase4-wasm-browser-verification/index.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Phase 4: Verify Without Installing Anything — Tesseras</title>
+ <meta name="description" content="Tesseras now compiles to WebAssembly — anyone can verify a tessera&#x27;s integrity and authenticity directly in the browser, with no software to install.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Phase 4: Verify Without Installing Anything">
+ <meta property="og:description" content="Tesseras now compiles to WebAssembly — anyone can verify a tessera&#x27;s integrity and authenticity directly in the browser, with no software to install.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Phase 4: Verify Without Installing Anything">
+ <meta name="twitter:description" content="Tesseras now compiles to WebAssembly — anyone can verify a tessera&#x27;s integrity and authenticity directly in the browser, with no software to install.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;phase4-wasm-browser-verification&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Phase 4: Verify Without Installing Anything</h2>
+ <p class="news-date">2026-02-15</p>
+ <p>Trust shouldn't require installing software. If someone sends you a tessera — a
+bundle of preserved memories — you should be able to verify it's genuine and
+unmodified without downloading an app, creating an account, or trusting a
+server. That's what <code>tesseras-wasm</code> delivers: drag a tessera archive into a web
+page, and cryptographic verification happens entirely in your browser.</p>
+<h2 id="what-was-built">What was built</h2>
+<p><strong>tesseras-wasm</strong> — A Rust crate that compiles to WebAssembly via wasm-pack,
+exposing four stateless functions to JavaScript. The crate depends on
+<code>tesseras-core</code> for manifest parsing and calls cryptographic primitives directly
+(blake3, ed25519-dalek) rather than depending on <code>tesseras-crypto</code>, which pulls
+in C-based post-quantum libraries that don't compile to
+<code>wasm32-unknown-unknown</code>.</p>
+<p><code>parse_manifest</code> takes raw MANIFEST bytes (UTF-8 plain text, not MessagePack),
+delegates to <code>tesseras_core::manifest::Manifest::parse()</code>, and returns a JSON
+string with the creator's Ed25519 public key, signature file paths, and a list
+of files with their expected BLAKE3 hashes, sizes, and MIME types. Internal
+structs (<code>ManifestJson</code>, <code>CreatorPubkey</code>, <code>SignatureFiles</code>, <code>FileEntry</code>) are
+serialized with serde_json. The ML-DSA public key and signature file fields are
+present in the JSON contract but set to <code>null</code> — ready for when post-quantum
+signing is implemented on the native side.</p>
+<p><code>hash_blake3</code> computes a BLAKE3 hash of arbitrary bytes and returns a
+64-character hex string. It's called once per file in the tessera to verify
+integrity against the MANIFEST.</p>
+<p><code>verify_ed25519</code> takes a message, a 64-byte signature, and a 32-byte public key,
+constructs an <code>ed25519_dalek::VerifyingKey</code>, and returns whether the signature
+is valid. Length validation returns descriptive errors ("Ed25519 public key must
+be 32 bytes") rather than panicking.</p>
+<p><code>verify_ml_dsa</code> is a stub that returns an error explaining ML-DSA verification
+is not yet available. This is deliberate: the <code>ml-dsa</code> crate on crates.io is
+v0.1.0-rc.7 (pre-release), and <code>tesseras-crypto</code> uses <code>pqcrypto-dilithium</code>
+(C-based CRYSTALS-Dilithium) which is byte-incompatible with FIPS 204 ML-DSA.
+Both sides need to use the same pure Rust implementation before
+cross-verification works. Ed25519 verification is sufficient — every tessera is
+Ed25519-signed.</p>
+<p>All four functions use a two-layer pattern for testability: inner functions
+return <code>Result&lt;T, String&gt;</code> and are tested natively, while thin <code>#[wasm_bindgen]</code>
+wrappers convert errors to <code>JsError</code>. This avoids <code>JsError::new()</code> panicking on
+non-WASM targets during testing.</p>
+<p>The compiled WASM binary is 109 KB raw and 44 KB gzipped — well under the 200 KB
+budget. wasm-opt applies <code>-Oz</code> optimization after wasm-pack builds with
+<code>opt-level = "z"</code>, LTO, and single codegen unit.</p>
+<p><strong>@tesseras/verify</strong> — A TypeScript npm package (<code>crates/tesseras-wasm/js/</code>)
+that orchestrates browser-side verification. The public API is a single
+function:</p>
+<pre><code data-lang="typescript">async function verifyTessera(
+ archive: Uint8Array,
+ onProgress?: (current: number, total: number, file: string) =&gt; void
+): Promise&lt;VerificationResult&gt;
+</code></pre>
+<p>The <code>VerificationResult</code> type provides everything a UI needs: overall validity,
+tessera hash, creator public keys, signature status (valid/invalid/missing for
+both Ed25519 and ML-DSA), per-file integrity results with expected and actual
+hashes, a list of unexpected files not in the MANIFEST, and an errors array.</p>
+<p>Archive unpacking (<code>unpack.ts</code>) handles three formats: gzip-compressed tar
+(detected by <code>\x1f\x8b</code> magic bytes, decompressed with fflate then parsed as
+tar), ZIP (<code>PK\x03\x04</code> magic, unpacked with fflate's <code>unzipSync</code>), and raw tar
+(<code>ustar</code> at offset 257). A <code>normalizePath</code> function strips the leading
+<code>tessera-&lt;hash&gt;/</code> prefix so internal paths match MANIFEST entries.</p>
+<p>Verification runs in a Web Worker (<code>worker.ts</code>) to keep the UI thread
+responsive. The worker initializes the WASM module, unpacks the archive, parses
+the MANIFEST, verifies the Ed25519 signature against the creator's public key,
+then hashes each file with BLAKE3 and compares against expected values. Progress
+messages stream back to the main thread after each file. If any signature is
+invalid, verification stops early without hashing files — failing fast on the
+most critical check.</p>
+<p>The archive is transferred to the worker with zero-copy
+(<code>worker.postMessage({ type: "verify", archive }, [archive.buffer])</code>) to avoid
+duplicating potentially large tessera files in memory.</p>
+<p><strong>Build pipeline</strong> — Three new justfile targets: <code>wasm-build</code> runs wasm-pack
+with <code>--target web --release</code> and optimizes with wasm-opt; <code>wasm-size</code> reports
+raw and gzipped binary size; <code>test-wasm</code> runs the native test suite.</p>
+<p><strong>Tests</strong> — 9 native unit tests cover BLAKE3 hashing (empty input, known value),
+Ed25519 verification (valid signature, invalid signature, wrong key, bad key
+length), and MANIFEST parsing (valid manifest, invalid UTF-8, garbage input). 3
+WASM integration tests run in headless Chrome via
+<code>wasm-pack test --headless --chrome</code>, verifying that <code>hash_blake3</code>,
+<code>verify_ed25519</code>, and <code>parse_manifest</code> work correctly when compiled to
+<code>wasm32-unknown-unknown</code>.</p>
+<h2 id="architecture-decisions">Architecture decisions</h2>
+<ul>
+<li><strong>No tesseras-crypto dependency</strong>: the WASM crate calls blake3 and
+ed25519-dalek directly. <code>tesseras-crypto</code> depends on <code>pqcrypto-kyber</code> (C-based
+ML-KEM via pqcrypto-traits) which requires a C compiler toolchain and doesn't
+target wasm32. By depending only on pure Rust crates, the WASM build has zero
+C dependencies and compiles cleanly to WebAssembly.</li>
+<li><strong>ML-DSA deferred, not faked</strong>: rather than silently skipping post-quantum
+verification, the stub returns an explicit error. This ensures that if a
+tessera contains an ML-DSA signature, the verification result will report
+<code>ml_dsa: "missing"</code> rather than pretending it was checked. The JS orchestrator
+handles this gracefully — a tessera is valid if Ed25519 passes and ML-DSA is
+missing (not yet implemented on either side).</li>
+<li><strong>Inner function pattern</strong>: <code>JsError</code> cannot be constructed on non-WASM
+targets (it panics). Splitting each function into
+<code>foo_inner() -&gt; Result&lt;T, String&gt;</code> and <code>foo() -&gt; Result&lt;T, JsError&gt;</code> lets the
+native test suite exercise all logic without touching JavaScript types. The
+WASM integration tests in headless Chrome test the full <code>#[wasm_bindgen]</code>
+surface.</li>
+<li><strong>Web Worker isolation</strong>: cryptographic operations (especially BLAKE3 over
+large media files) can take hundreds of milliseconds. Running in a Worker
+prevents UI jank. The streaming progress protocol
+(<code>{ type: "progress", current, total, file }</code>) lets the UI show a progress bar
+during verification of tesseras with many files.</li>
+<li><strong>Zero-copy transfer</strong>: <code>archive.buffer</code> is transferred to the Worker, not
+copied. For a 50 MB tessera archive, this avoids doubling memory usage during
+verification.</li>
+<li><strong>Plain text MANIFEST, not MessagePack</strong>: the WASM crate parses the same
+plain-text MANIFEST format as the CLI. This is by design — the MANIFEST is the
+tessera's Rosetta Stone, readable by anyone with a text editor. The
+<code>rmp-serde</code> dependency in the Cargo.toml is not used and will be removed.</li>
+</ul>
+<h2 id="what-comes-next">What comes next</h2>
+<ul>
+<li><strong>Phase 4: Resilience and Scale</strong> — OS packaging (Alpine, Arch, Debian,
+FreeBSD, OpenBSD), CI on SourceHut and GitHub Actions, security audits,
+browser-based tessera explorer at tesseras.net using @tesseras/verify</li>
+<li><strong>Phase 5: Exploration and Culture</strong> — Public tessera browser by
+era/location/theme/language, institutional curation, genealogy integration,
+physical media export (M-DISC, microfilm, acid-free paper with QR)</li>
+</ul>
+<p>Verification no longer requires trust in software. A tessera archive dropped
+into a browser is verified with the same cryptographic rigor as the CLI — same
+BLAKE3 hashes, same Ed25519 signatures, same MANIFEST parser. The difference is
+that now anyone can do it.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/phase4-wasm-browser-verification/index.html.gz b/news/phase4-wasm-browser-verification/index.html.gz
new file mode 100644
index 0000000..41c46ac
--- /dev/null
+++ b/news/phase4-wasm-browser-verification/index.html.gz
Binary files differ
diff --git a/news/reed-solomon/index.html b/news/reed-solomon/index.html
new file mode 100644
index 0000000..e47a9f2
--- /dev/null
+++ b/news/reed-solomon/index.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Reed-Solomon: How Tesseras Survives Data Loss — Tesseras</title>
+ <meta name="description" content="A deep dive into Reed-Solomon erasure coding — what it is, why Tesseras uses it, and the challenges of keeping memories alive across centuries.">
+ <!-- Open Graph -->
+ <meta property="og:type" content="article">
+ <meta property="og:title" content="Reed-Solomon: How Tesseras Survives Data Loss">
+ <meta property="og:description" content="A deep dive into Reed-Solomon erasure coding — what it is, why Tesseras uses it, and the challenges of keeping memories alive across centuries.">
+ <meta property="og:image" content="https://tesseras.net/images/social.jpg">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:site_name" content="Tesseras">
+ <!-- Twitter Card -->
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Reed-Solomon: How Tesseras Survives Data Loss">
+ <meta name="twitter:description" content="A deep dive into Reed-Solomon erasure coding — what it is, why Tesseras uses it, and the challenges of keeping memories alive across centuries.">
+ <meta name="twitter:image" content="https://tesseras.net/images/social.jpg">
+ <link rel="stylesheet" href="https://tesseras.net/style.css?h=21f0f32121928ee5c690">
+
+
+ <link rel="alternate" type="application/atom+xml" title="Tesseras" href="https://tesseras.net/atom.xml">
+
+
+ <link rel="icon" type="image/png" sizes="32x32" href="https://tesseras.net/images/favicon.png?h=be4e123a23393b1a027d">
+
+</head>
+<body>
+ <header>
+ <h1>
+ <a href="https:&#x2F;&#x2F;tesseras.net/">
+ <img src="https://tesseras.net/images/logo-64.png?h=c1b8d0c4c5f93b49d40b" alt="Tesseras" width="40" height="40" class="logo">
+ Tesseras
+ </a>
+ </h1>
+ <nav>
+
+ <a href="https://tesseras.net/about/">About</a>
+ <a href="https://tesseras.net/news/">News</a>
+ <a href="https://tesseras.net/releases/">Releases</a>
+ <a href="https://tesseras.net/faq/">FAQ</a>
+ <a href="https://tesseras.net/subscriptions/">Subscriptions</a>
+ <a href="https://tesseras.net/contact/">Contact</a>
+
+ </nav>
+ <nav class="lang-switch">
+
+ <strong>English</strong> | <a href="/pt-br&#x2F;news&#x2F;reed-solomon&#x2F;">Português</a>
+
+ </nav>
+ </header>
+
+ <main>
+
+<article>
+ <h2>Reed-Solomon: How Tesseras Survives Data Loss</h2>
+ <p class="news-date">2026-02-14</p>
+ <p>Your hard drive will die. Your cloud provider will pivot. The RAID array in your
+closet will outlive its controller but not its owner. If a memory is stored in
+exactly one place, it has exactly one way to be lost forever.</p>
+<p>Tesseras is a network that keeps human memories alive through mutual aid. The
+core survival mechanism is <strong>Reed-Solomon erasure coding</strong> — a technique
+borrowed from deep-space communication that lets us reconstruct data even when
+pieces go missing.</p>
+<h2 id="what-is-reed-solomon">What is Reed-Solomon?</h2>
+<p>Reed-Solomon is a family of error-correcting codes invented by Irving Reed and
+Gustave Solomon in 1960. The original use case was correcting errors in data
+transmitted over noisy channels — think Voyager sending photos from Jupiter, or
+a CD playing despite scratches.</p>
+<p>The key insight: if you add carefully computed redundancy to your data <em>before</em>
+something goes wrong, you can recover the original even after losing some
+pieces.</p>
+<p>Here's the intuition. Suppose you have a polynomial of degree 2 — a parabola.
+You need 3 points to define it uniquely. But if you evaluate it at 5 points, you
+can lose any 2 of those 5 and still reconstruct the polynomial from the
+remaining 3. Reed-Solomon generalizes this idea to work over finite fields
+(Galois fields), where the "polynomial" is your data and the "evaluation points"
+are your fragments.</p>
+<p>In concrete terms:</p>
+<ol>
+<li><strong>Split</strong> your data into <em>k</em> data shards</li>
+<li><strong>Compute</strong> <em>m</em> parity shards from the data shards</li>
+<li><strong>Distribute</strong> all <em>k + m</em> shards across different locations</li>
+<li><strong>Reconstruct</strong> the original data from any <em>k</em> of the <em>k + m</em> shards</li>
+</ol>
+<p>You can lose up to <em>m</em> shards — any <em>m</em>, data or parity, in any combination —
+and still recover everything.</p>
+<h2 id="why-not-just-make-copies">Why not just make copies?</h2>
+<p>The naive approach to redundancy is replication: make 3 copies, store them in 3
+places. This gives you tolerance for 2 failures at the cost of 3x your storage.</p>
+<p>Reed-Solomon is dramatically more efficient:</p>
+<table><thead><tr><th>Strategy</th><th style="text-align: right">Storage overhead</th><th style="text-align: right">Failures tolerated</th></tr></thead><tbody>
+<tr><td>3x replication</td><td style="text-align: right">200%</td><td style="text-align: right">2 out of 3</td></tr>
+<tr><td>Reed-Solomon (16,8)</td><td style="text-align: right">50%</td><td style="text-align: right">8 out of 24</td></tr>
+<tr><td>Reed-Solomon (48,24)</td><td style="text-align: right">50%</td><td style="text-align: right">24 out of 72</td></tr>
+</tbody></table>
+<p>With 16 data shards and 8 parity shards, you use 50% extra storage but can
+survive losing a third of all fragments. To achieve the same fault tolerance
+with replication alone, you'd need 3x the storage.</p>
+<p>For a network that aims to preserve memories across decades and centuries, this
+efficiency isn't a nice-to-have — it's the difference between a viable system
+and one that drowns in its own overhead.</p>
+<h2 id="how-tesseras-uses-reed-solomon">How Tesseras uses Reed-Solomon</h2>
+<p>Not all data deserves the same treatment. A 500-byte text memory and a 100 MB
+video have very different redundancy needs. Tesseras uses a three-tier
+fragmentation strategy:</p>
+<p><strong>Small (&lt; 4 MB)</strong> — Whole-file replication to 7 peers. For small tesseras, the
+overhead of erasure coding (encoding time, fragment management, reconstruction
+logic) outweighs its benefits. Simple copies are faster and simpler.</p>
+<p><strong>Medium (4–256 MB)</strong> — 16 data shards + 8 parity shards = 24 total fragments.
+Each fragment is roughly 1/16th of the original size. Any 16 of the 24 fragments
+reconstruct the original. Distributed across 7 peers.</p>
+<p><strong>Large (≥ 256 MB)</strong> — 48 data shards + 24 parity shards = 72 total fragments.
+Higher shard count means smaller individual fragments (easier to transfer and
+store) and higher absolute fault tolerance. Also distributed across 7 peers.</p>
+<p>The implementation uses the <code>reed-solomon-erasure</code> crate operating over GF(2⁸) —
+the same Galois field used in QR codes and CDs. Each fragment carries a BLAKE3
+checksum so corruption is detected immediately, not silently propagated.</p>
+<pre><code>Tessera (120 MB photo album)
+ ↓ encode
+16 data shards (7.5 MB each) + 8 parity shards (7.5 MB each)
+ ↓ distribute
+24 fragments across 7 peers (subnet-diverse)
+ ↓ any 16 fragments
+Original tessera recovered
+</code></pre>
+<h2 id="the-challenges">The challenges</h2>
+<p>Reed-Solomon solves the mathematical problem of redundancy. The engineering
+challenges are everything around it.</p>
+<h3 id="fragment-tracking">Fragment tracking</h3>
+<p>Every fragment needs to be findable. Tesseras uses a Kademlia DHT for peer
+discovery and fragment-to-peer mapping. When a node goes offline, its fragments
+need to be re-created and distributed to new peers. This means tracking which
+fragments exist, where they are, and whether they're still intact — across a
+network with no central authority.</p>
+<h3 id="silent-corruption">Silent corruption</h3>
+<p>A fragment that returns wrong data is worse than one that's missing — at least a
+missing fragment is honestly absent. Tesseras addresses this with
+attestation-based health checks: the repair loop periodically asks fragment
+holders to prove possession by returning BLAKE3 checksums. If a checksum doesn't
+match, the fragment is treated as lost.</p>
+<h3 id="correlated-failures">Correlated failures</h3>
+<p>If all 24 fragments of a tessera land on machines in the same datacenter, a
+single power outage kills them all. Reed-Solomon's math assumes independent
+failures. Tesseras enforces <strong>subnet diversity</strong> during distribution: no more
+than 2 fragments per /24 IPv4 subnet (or /48 IPv6 prefix). This spreads
+fragments across different physical infrastructure.</p>
+<h3 id="repair-speed-vs-network-load">Repair speed vs. network load</h3>
+<p>When a peer goes offline, the clock starts ticking. Lost fragments need to be
+re-created before more failures accumulate. But aggressive repair floods the
+network. Tesseras balances this with a configurable repair loop (default: every
+24 hours with 2-hour jitter) and concurrent transfer limits (default: 4
+simultaneous transfers). The jitter prevents repair storms where every node
+checks its fragments at the same moment.</p>
+<h3 id="long-term-key-management">Long-term key management</h3>
+<p>Reed-Solomon protects against data loss, not against losing access. If a tessera
+is encrypted (private or sealed visibility), you need the decryption key to make
+the recovered data useful. Tesseras separates these concerns: erasure coding
+handles availability, while Shamir's Secret Sharing (a future phase) will handle
+key distribution among heirs. The project's design philosophy — encrypt as
+little as possible — keeps the key management problem small.</p>
+<h3 id="galois-field-limitations">Galois field limitations</h3>
+<p>The GF(2⁸) field limits the total number of shards to 255 (data + parity
+combined). For Tesseras, this is not a practical constraint — even the Large
+tier uses only 72 shards. But it does mean that extremely large files with
+thousands of fragments would require either a different field or a layered
+encoding scheme.</p>
+<h3 id="evolving-codec-compatibility">Evolving codec compatibility</h3>
+<p>A tessera encoded today must be decodable in 50 years. Reed-Solomon over GF(2⁸)
+is one of the most widely implemented algorithms in computing — it's in every CD
+player, every QR code scanner, every deep-space probe. This ubiquity is itself a
+survival strategy. The algorithm won't be forgotten because half the world's
+infrastructure depends on it.</p>
+<h2 id="the-bigger-picture">The bigger picture</h2>
+<p>Reed-Solomon is a piece of a larger puzzle. It works in concert with:</p>
+<ul>
+<li><strong>Kademlia DHT</strong> for finding peers and routing fragments</li>
+<li><strong>BLAKE3 checksums</strong> for integrity verification</li>
+<li><strong>Bilateral reciprocity</strong> for fair storage exchange (no blockchain needed)</li>
+<li><strong>Subnet diversity</strong> for failure independence</li>
+<li><strong>Automatic repair</strong> for maintaining redundancy over time</li>
+</ul>
+<p>No single technique makes memories survive. Reed-Solomon ensures that data <em>can</em>
+be recovered. The DHT ensures fragments <em>can be found</em>. Reciprocity ensures
+peers <em>want to help</em>. Repair ensures none of this degrades over time.</p>
+<p>A tessera is a bet that the sum of these mechanisms, running across many
+independent machines operated by many independent people, is more durable than
+any single institution. Reed-Solomon is the mathematical foundation of that bet.</p>
+
+</article>
+
+ </main>
+
+ <footer>
+ <p>&copy; 2026 Tesseras Project. <a href="/atom.xml">News Feed</a> · <a href="https://git.sr.ht/~ijanc/tesseras">Source</a></p>
+ </footer>
+</body>
+</html>
diff --git a/news/reed-solomon/index.html.gz b/news/reed-solomon/index.html.gz
new file mode 100644
index 0000000..3e76a2e
--- /dev/null
+++ b/news/reed-solomon/index.html.gz
Binary files differ