Freenet Microblogging - Decentralized Social on Freenet
August 22, 2026 · View on GitHub
Freenet Microblogging is a decentralized Twitter/X-like application built on Freenet, designed to provide censorship-resistant social networking where users own their data. It features a web-based interface built with Svelte 5 and Vite, Rust/WASM per-user/per-thread shard contracts for posts, social graph, replies, likes and notifications, a singleton global-index contract for the public timeline, and an ML-DSA-65 (post-quantum) identity delegate for cryptographic signing. A stable "facade" contract gives users a bookmarkable URL that survives every release.

Roadmap
- User shard: owner-writes posts (windowed, content-addressed), profile (LWW), follows (per-key seq merge)
- Thread shard: anyone-writes replies, likes, quotes (each record self-verifying), per root post
- Inbox shard: anyone-writes notifications, owner-prunes via signed ops
- Global-index shard: singleton public-timeline contract (opt-in share), merge-not-replace reconcile
- ML-DSA-65 (post-quantum) identity delegate with keypair generation and signing
- Svelte 5 web UI with feed, compose box, profile, sidebar, dark/light mode
- Onboarding flow with identity creation and import
- Real-time post updates via contract subscription
- Wire posts + likes + reposts/quotes to shard contracts (user shard / thread shard)
- Public timeline: landing feed + Discover tab backed by the global-index shard
- Facade contract — stable bookmarkable URL that survives release contract-id rotation
- Wire follows UI to the user shard (sign →
ShardDelta::Op→ aggregated Following feed) - Wire replies UI + delegate reply signing (
SignReply) to the thread shard (#12; nested threads /thread_rootstill TODO) - Wire notifications UI to the inbox shard (contract done; UI renders mock records)
- Handle registry: handle → pubkey resolution (no registry contract yet)
- Real "who to follow" + search backed by the social graph / an index (currently mock/local-filter)
- Retract a post — author-signed withdrawal from the user shard and the public timeline
- Local mute list (per-reader, unreplicated)
- In-app roadmap page (voting layer planned)
- Media attachments
- Optional per-post translation, badged as machine-translated
- GhostKey support for anonymous posting
Joining the network
You do not need an account, an email address, or anyone's permission. You need a Freenet node and a browser.
1. Run a node
The app is served by the network, so a local node is how you reach it:
cargo install freenet
freenet network
Leave it running. It connects to peers on its own and serves contracts over HTTP
on port 7509 by default (FREENET_NODE_PORT overrides it).
2. Open the app
Open the URL printed when the web app is published — the node prints the full
address, of the form http://127.0.0.1:7509/v1/contract/web/<id>/.
Bookmark the facade address rather than that one. The facade is a small contract holding a signed pointer to the current app, so its address survives every release, while the app's own contract id changes each time it ships (the id is a hash of the bytes, so any rebuild moves it).
3. Create an identity
Type a display name and press Join. That generates an ML-DSA-65 post-quantum keypair inside your node — it is never sent anywhere, and no server is asked for permission.
Three things follow from that, and they are worth understanding before you start rather than after:
- The key is the account. There is no password, no reset, and no support address. Lose it and the identity is gone; nobody can restore it for you.
- Posts are public and permanent. Anyone can read them. You can withdraw a post — your shard and the public timeline will stop serving it and will not re-accept it — but nobody can reach a copy someone already fetched or holds offline. Withdrawal is not deletion, and the app does not claim otherwise.
- Your follow list is public too. It lives in your own shard, which anyone can read.
4. Back up your key — do this now, not later
Settings → Export key shows a 64-character secret. Copy it somewhere safe and offline. Anyone holding it controls your identity; anyone without it cannot recover yours, including you.
To use the same identity on another device, choose Import existing identity during onboarding and paste that secret.
5. Find people
- Discover shows the public timeline — posts whose authors opted into sharing them.
- Follow anyone from their post. Following is by verifying key, not by
handle: handles are self-declared and there is no registry yet, so two
accounts can display the same
@name. The key is what is real. - Your Following feed merges your own posts with everyone you follow.
6. When someone is a problem
Mute hides an author's posts and replies for you. It is local, it is not announced, and nothing leaves your browser — which also means it does not stop them writing, and it does not follow you to another device yet.
There is no report button and no moderator, which follows from the design: the network has no central operator, so there is nobody to appeal to. Writer credentials are the next major piece of work — see the in-app Roadmap for where that sits.
7. Shape what gets built
The app has a Roadmap page listing what is shipped, what is committed, and what is still undecided, with the trade-offs for each. A signed one-vote-per- identity mechanism for the undecided items is planned — until it lands, the buttons are visible but inert, and the honest place to argue for something is the issue tracker.
Getting Started (developers)
Building the app yourself, rather than using a published one.
Building and Running
-
Install dependencies:
# Install Rust with wasm target curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-unknown-unknown # Install Node.js (v18+) # See https://nodejs.org/en/download # Install Freenet tools cargo install freenet cargo install fdev # Install BLAKE3 hash tool (used for delegate key computation) cargo install b3sum -
Clone and set up:
# Clone the repository git clone git@github.com:freenet/freenet-microblogging.git cd freenet-microblogging # Clone freenet-stdlib as a sibling directory (required for TypeScript SDK) cd .. && git clone git@github.com:nicobao/freenet-stdlib.git && cd freenet-microblogging # Install web dependencies cd web && npm install && cd .. -
Build and publish:
# Set target directory (required by Makefile.toml) export CARGO_TARGET_DIR=$(pwd)/target # Full build: contracts + delegate + web app + publish all cargo make build -
Run the node:
cargo make run-node -
Open the web app URL printed during publish (e.g.
http://127.0.0.1:7509/contract/web/<hash>/)
Key Development Commands
# Rebuild just the web app
cargo make build-ui publish-webapp-test
# Build the shard contracts (parameterized — instantiated per owner/thread at
# runtime by the web app, not published globally)
cargo make build-contracts
# Rebuild identity delegate
cargo make publish-identity
# Run all tests (Rust + web)
cargo make test
# Type check everything
cargo make check
# Reset node data (required when republishing contracts)
cargo make clean-node
# Vite dev server (without Freenet)
cd web && npm run dev
Technical Details
Project Structure
- contracts/user-shard: per-owner shard — posts, profile, follows (owner-writes)
- contracts/thread-shard: per-root-post shard — replies, likes, quotes (anyone-writes)
- contracts/inbox-shard: per-owner shard — notifications (anyone-writes, owner-prunes)
- contracts/global-index-shard: singleton — public timeline / Discover feed (opt-in share)
- contracts/facade: stable bookmarkable URL; signed pointer to the current webapp contract id
- delegates/identity: ML-DSA-65 identity delegate (keygen, post/like/repost/quote signing)
- web: Svelte 5 + Vite web application
Architecture
The system is built using:
- Freenet Contracts: Rust/WASM contracts with commutative merge for conflict-free replication
- Freenet Delegates: Client-side WASM modules for identity and cryptographic operations
- freenet-stdlib: TypeScript SDK for WebSocket communication with the Freenet node
- Svelte 5 + Vite: reactive UI (runes) over a framework-agnostic WS/contract store layer, built and served as a webapp contract
- ML-DSA-65 (FIPS 204): post-quantum signatures for identity and all signed records
- BLAKE3: content-addressable post ids, contract key derivation, delegate key computation
Contract Architecture (ADR-0001 sharding)
State is split into per-owner / per-thread shards instead of global contracts. Each
shard is a commutative (CRDT) Rust/WASM contract parameterized by an owner key or root
post id, so its contract key is blake3(code_hash || parameters) and it is instantiated
on demand by the web app (no global publish). See docs/adr/0001-implementation-notes.md.
- user-shard: owner-writes — posts (windowed, content-addressed, ML-DSA-signed), profile (LWW), follows (per-key seq merge). Parameter: owner VK bytes.
- thread-shard: anyone-writes — replies, likes, quotes; each record self-verifying. Parameter: root post id (UTF-8).
- inbox-shard: anyone-writes notifications, owner-prunes via signed ops. Parameter: owner VK bytes.
Identity Delegate
The identity delegate runs client-side in the Freenet node and manages ML-DSA-65 (post-quantum)
keypairs. It is the single trusted encoder of canonical signing payloads — it builds each payload
in Rust via the common crate and signs it, so the browser never assembles signed bytes itself.
It supports:
- CreateIdentity: Generate a new keypair with display name
- GetIdentity: Retrieve the current identity
- SignPost: Sign post content for authenticity (assigns the content-addressed post id)
- SignLike: Sign a like/unlike record bound to a thread's root post id
- SignReply: Sign a reply (a
Postwith a non-emptyreply_to, optionalquoted_post) bound to a thread's root post id;SignPoststays byte-identical for top-level posts - ExportIdentity / ImportIdentity: Transfer identity between devices (64-hex seed)
Notification delivery is not yet wired from the UI — the inbox shard accepts notifications, but the client cutover for that surface lands in a later ADR-0001 Phase 4 slice.
The delegate key is computed as BLAKE3(BLAKE3(wasm_bytes)) with empty parameters, and the code
hash as BLAKE3(wasm_bytes). Both are required for the node to locate the delegate in its store.
Privacy Model
- Posts are currently public and readable by anyone with the contract address
- Identity keys are stored locally in the node's delegate store
- Post signatures provide authenticity but not confidentiality
- Future versions may support encrypted posts and private feeds
License
Licensed under the GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later). See
COPYING for the full GPL text and COPYING.LESSER for the LGPL
additional terms.
Twitter resurrected by EXAEETH and Zuli, on Freenet — but better.