Post Protocol
A post in Hashiverse is HTML — authored in the browser with a rich-text editor, DOMPurify sanitized before display. Between authorship and display is a pipeline of compression, encryption, signing, two-phase submission, DHT routing, and bundle aggregation. This page follows a post through that pipeline.
The encoded post
EncodedPostV1 is the wire format for a single post. It contains:
- header: verification key bytes, post-quantum commitment bytes, timestamp, post length, and
linked_base_ids— references to prior posts this post is replying to or building on. - signature: Ed25519 (or PQ) signature over the header and post hashes.
- post: the HTML content, Brotli-compressed and encrypted.
- post_id: Blake3 hash of the signature — a stable, verifiable content address.
Encryption uses multiple passphrases — one per context the post appears in — via a custom
multi-key scheme inspired by the age encryption format. A post that is both in
a user's timeline and under a hashtag is encrypted with both the user's public ID and the
hashtag string as passphrases. Either key decrypts it. Servers holding the encrypted bytes
cannot read either.
Submission: Claim then Commit
Post submission is a two-phase protocol:
- SubmitClaim: The client sends a claim request with PoW. The server validates the PoW and, if the post ID is not already present, issues a token granting permission to commit.
- SubmitCommit: The client sends the actual post bytes with the token. The server stores the post and returns a confirmation signature.
Separating claim from commit prevents payload-flooding attacks: a server can reject bad-faith claims cheaply (just verify PoW) before accepting any large payload. The confirmation signature from the server is the client's proof that the post was accepted.
Source: hashiverse-server/src/server/handlers/
Bundles and buckets
Posts are not stored or fetched individually — they are grouped into
EncodedPostBundleV1 objects, one bundle per bucket per server. A bundle
contains around 20 posts from a particular location ID (a specific time window for a
specific user, hashtag, or reply context), but this number can be larger with healing and eventual consistency delays.
Bundles are signed by the serving peer.
Fetching a user's timeline means traversing the bucket hierarchy recursively: start with
the monthly bucket, drill into the weekly, daily, hourly, and finer buckets where posts
exist. The RecursiveBucketVisitor handles this with a callback that decides
at each level whether to recurse into finer granularity or skip, allowing efficient
pagination even across sparse timelines.
Source: encoded_post_bundle.rs,
hashiverse-lib/src/client/timeline/
Bucket types
Four (at the time of writing) bucket types determine how posts are indexed and discovered:
- User (0): the author's personal timeline
- Hashtag (1): posts containing a given hashtag
- Mention (2): posts mentioning a specific user
- ReplyToPost (3): posts replying to a specific post
A single post may appear in multiple buckets simultaneously, each encrypted with the appropriate passphrase for that bucket.
Source: buckets.rs
Storage
On the server, post bundles are persisted to disk, while their metadata is persisted to fjall — a pure-Rust embedded key-value store with automatic compaction and good write throughput. fjall was chosen over redb and sled for its maintenance track record and operational simplicity. Posts are stored as Brotli-compressed, encrypted bytes.
On the browser client, posts are cached in IndexedDB via
indexed_db_futures, with an in-memory stub for testing. Each category of
client-side data is encrypted at rest with a different key, chosen to match its access model.
Source: hashiverse-server/src/environment/,
wasm_client_storage.rs