Private messages that disappear on their own. Built on the Arkiv Tiramisu testnet for ETHRome 2026.
Two people share one passphrase. Each message is encrypted in the browser, written to Arkiv as an entity with a lifetime in blocks, and shown to the other person over a websocket. When the lifetime ends, the message stops existing for both of them. No server, no cron job, no delete call.
- Send. Your text is encrypted with a key made from the passphrase (AES-256-GCM, PBKDF2). Arkiv stores only the ciphertext and a few queryable attributes:
app,type,room,sender,seq. - Expire. Every message carries an expiry in blocks. Demo mode uses 10 blocks, about 20 seconds when the chain is busy. Normal mode uses 900 blocks, about 30 minutes at the nominal 2 s per block. The screen drops a message the moment the block head passes its expiry.
- Edit. A patch on the payload. Same key, same owner, same expiry.
- Extend. Sets a new expiry the same number of blocks from now.
- Delete. One signed transaction. The other screen drops it on the delete event.
- Live. Each tab holds one websocket subscription with no start block. Nothing polls. If the socket drops, the header says so.
Every action is one transaction signed in MetaMask by the wallet that owns the message.
browser (React, Vite)
reads + subscription --ws--> nginx (adds X-API-KEY) --wss--> Arkiv Tiramisu RPC
writes --MetaMask-------------------------------> Arkiv Tiramisu RPC
src/arkiv.tsthe read client onwebSocket(), the room query, and the write helpers.src/crypto.tspassphrase to room id (SHA-256) and to key (PBKDF2). WebCrypto only.src/main.tsxone page: connect, open room, chat. Subscriptions live in one effect.src/styles.cssa Signal-style messenger, mobile first, dark follows the OS.nginx/nginx.confanddocker-compose.ymlthe proxy that keeps the access key out of the browser.arkiv/query.test.tsthe real SDK query builder run against a fixture.
Two entity types:
| Type | Attributes | Payload | Lifetime |
|---|---|---|---|
msg |
app, type, room (bytes32), sender (addr), seq (u64) |
ciphertext | 10 or 900 blocks |
conv |
app, type, room, opener, openedseq |
empty | 7 days |
The query the chat screen depends on:
app = str('tiramisu-chat') AND type = str('msg') AND room = bytes32(<room>) AND seq > u64(<last seen>)
<room> is sha256("tiramisu-chat/room/" + passphrase). The passphrase never goes on chain.
Needs Node 22+, Docker, and MetaMask with test GLM from https://hub.arkiv.network/faucet.
cp .env.example .env # ARKIV_KEY from https://hub.arkiv.network/access-keys (Tiramisu)
docker compose up -d # nginx on :8080 adds the key to the websocket
npm install
npm run dev # http://localhost:5173/?demo
Open it in two browser profiles, each with its own funded wallet. Type the same passphrase in both.
?demosets a 10-block lifetime so expiry is visible in seconds. Drop it for 900 blocks.?previewrenders fake messages with no wallet, for screenshots or a dead network.- Without nginx:
VITE_ARKIV_WSS=wss://rpc.tiramisu.db-chain.testnet.arkiv.network npm run dev.
Tests: npm test. Typecheck: npm run typecheck.
In Postgres, "delete after 30 minutes" is a cron job that someone has to run, monitor and trust. Here the lifetime is part of the row, so nothing runs and nothing can forget to run. In Postgres the sender column is whatever the server chose to write. Here the owner is the wallet that signed the transaction, so a forged sender needs the private key. In Postgres a delete is a row the operator can skip, fake or restore from backup without anyone knowing. Here only the owner can delete, and the delete is a signed public event the other tab receives over the socket. Postgres can hide rows from people without access, and it can really erase bytes. Arkiv can do neither, which is why the payload is encrypted and why no personal data is written.
- No real key exchange. The key comes from a shared passphrase. Anyone with the passphrase reads the room.
- No erasure. Expiry removes a message from queries, not from chain history. The ciphertext stays public forever.
- No hiding who talks. Sender addresses and the room id are public attributes.
- No paging. One page of at most 200 live messages per room.
arkiv/schema.mddata model, queries, what stays off Arkivarkiv/submission.mdmissions, requirements, how to run the checkarkiv/friction.mddated bullets on what broke or confused usarkiv/query.test.tsfixture test with the real query builder
Product context is in PRODUCT.md. The visual system is in DESIGN.md.
Tiramisu, chain id 7738577. SDK @arkiv-network/sdk@0.8.0, pinned. On 2026-09-08 it became the npm latest; before that latest was an older generation that could not talk to this network. Block cadence measured between 2 and 20 seconds per block, so all lifetimes are in blocks and the receipt's expiresAt is the truth.