Sitelet https://github.com/marcos-golem/tiramisu-chat
Skip to content

Repository files navigation

tiramisu-chat

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.

What it does

  • 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.

How it is built

browser (React, Vite)
  reads + subscription  --ws-->  nginx (adds X-API-KEY)  --wss-->  Arkiv Tiramisu RPC
  writes                --MetaMask------------------------------->  Arkiv Tiramisu RPC
  • src/arkiv.ts the read client on webSocket(), the room query, and the write helpers.
  • src/crypto.ts passphrase to room id (SHA-256) and to key (PBKDF2). WebCrypto only.
  • src/main.tsx one page: connect, open room, chat. Subscriptions live in one effect.
  • src/styles.css a Signal-style messenger, mobile first, dark follows the OS.
  • nginx/nginx.conf and docker-compose.yml the proxy that keeps the access key out of the browser.
  • arkiv/query.test.ts the 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.

Run it

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.

  • ?demo sets a 10-block lifetime so expiry is visible in seconds. Drop it for 900 blocks.
  • ?preview renders 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.

Why not Postgres

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.

What it does not do

  • 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 bounty files

  • arkiv/schema.md data model, queries, what stays off Arkiv
  • arkiv/submission.md missions, requirements, how to run the check
  • arkiv/friction.md dated bullets on what broke or confused us
  • arkiv/query.test.ts fixture test with the real query builder

Product context is in PRODUCT.md. The visual system is in DESIGN.md.

Network facts

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.

About

Private messages that expire on their own. Arkiv Tiramisu, ETHRome 2026.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages