Nullchat

Chat that forgets.

End-to-end-encrypted messaging for iPhone, Android, web and Mac. No account, no phone number, and no server keeping your messages. Turn on a dark chat and every message burns a few seconds after it’s read.

FreeAGPL-3.0Runs on Nostr relaysNo ads, no analytics

mika dark chat, burns 5s after read
  1. Dark chat is on. Earlier messages were cleared on both phones.
  2. wifi pw for the cabin is moss-quartz-77 read, burns in 3s
Live demo. Read receipts start the timer; when it hits zero the message is deleted on both phones.

nullchat --help

--e2ee
Every message is sealed with your key, then gift-wrapped under a throwaway key (NIP-44 v2, NIP-59) before it leaves the phone.
--no-server
Relays hold ciphertext only, for at most 7 days, and are told to delete it the moment it’s delivered. Your history lives on your devices.
--dark
Ghost mode. Messages self-destruct 5, 10 or 30 seconds after being read. Screenshots are blocked, and reported if they slip through.
--view-once
Photos that open once, then are gone from both phones. Never shown in previews, notifications or on linked devices.
--avatars
Pick one of 24 avatars and a name. No profile photo uploads, no phone number, no email.
--groups
Up to 32 people. Each member gets their own encrypted copy, so there’s no group server and no group tag for relays to see.
--link
Your phone is your identity. Link up to 4 browsers or Macs by scanning a QR code, each with its own key you can revoke.
--interop
Text messages are plain NIP-17, so 0xchat, Amethyst and other Nostr clients can DM you.
--self-host
Point the app at your own relay and push worker. Or use the defaults; nobody has to run anything.

How a message travels

Nostr relays are used as a dumb mailbox. They pass along sealed envelopes they can’t open, and forget them.

Message flow Your phone nests the message inside a seal and a gift wrap, publishes the wrap to three relays, and the recipient’s phone fetches and opens it. After a delivered receipt, your phone asks the relays to delete the wrap. Relays never store plaintext. your phone keys stay here gift wrap (kind 1059) signed by a one-time key seal (kind 13) signed by you “see you at 8” relay.ditto.pubciphertext, expires in 7d relay.primal.netciphertext, expires in 7d your own relayoptional, AUTH-gated their phone opens it locally delivered receipt, then the wrap is deleted Message flow: your phone, gift wrap, relays, their phone; the relays delete the wrap after delivery. your phone keys stay here gift wrap (kind 1059) signed by a one-time key seal (kind 13) signed by you “see you at 8” relay.ditto.pubciphertext, expires 7d relay.primal.netciphertext, expires 7d your own relayoptional, AUTH-gated their phone opens it locally delivered receipt, then the wrap is deleted from every relay
  1. Seal. Your phone signs the message with your key and encrypts it to the recipient’s device key.
  2. Wrap. The seal goes inside a gift wrap signed by a key used once and thrown away. Its timestamp is fuzzed up to two days into the past, and the payload is padded.
  3. Relay. Three relays hold the wrap. They see a recipient key, an expiry and an opaque blob. Not who sent it, not what’s in it.
  4. Forget. Their phone unwraps it and sends a delivered receipt. Your phone then tells the relays to delete the wrap. If anything is missed, it expires on its own.

What it protects, and what it doesn’t

Nullchat is Phase 1 software and hasn’t been audited. Here is exactly what a relay receives for one of your messages:

A wrap as stored on a relay
{
  "kind": 1059,
  // throwaway key, used once
  "pubkey": "9f2c…e71a",
  // fuzzed up to 2 days into the past
  "created_at": 1790812244,
  "tags": [
    // recipient device key: visible
    ["p", "4be0…03c9"],
    ["expiration", "1791417044"]
  ],
  // sender, text and kind: sealed inside
  "content": "AqG8x1Vb…kT0=",
  "sig": "c4d1…88f2"
}

Protected

  • Message text, photos, group names and membership
  • Who sent a message, and its real send time
  • Message type and length class (padded)
  • Your history at rest: SQLCipher on phones, AES-GCM in the browser
  • Your contacts’ push handles, which are sealed and only shared with contacts

Not protected

  • No forward secrecy yet. Keys are static. If a device key leaks, any wrap still on a relay or captured in transit can be decrypted. A ratchet or MLS is planned for Phase 2.
  • Metadata. Relays see which device key receives wraps, roughly how many and when, and your IP address. Use a VPN or Tor, or your own AUTH-gated relay.
  • Disappearing is a courtesy. Honest clients delete on time. A modified app or a second camera can keep anything.
  • Browser keys live in memory. The web companion ships with a strict CSP and no third-party scripts, but any script on its origin could use the key.

Run your own

Nothing here requires us. Both pieces are optional and stateless enough to run on a free tier.

Relay

Any Nostr relay that accepts 256 KB events and supports NIP-40 works. strfry is one line:

$ docker run -d -p 7777:7777 \
    -v strfry-data:/app/strfry-db \
    ghcr.io/hoytech/strfry

# strfry.conf: maxEventSize = 262144
# accept kinds 1059, 10050, 10410, 5

Then add wss://your.host in Settings → Relays. The app checks its limits and republishes your inbox list.

Push worker

A Cloudflare Worker that turns a sealed handle into a content-free “New message” push. It stores nothing.

$ cd apps/push-worker
$ node scripts/gen-key.mjs \
    | npx wrangler secret put HANDLE_KEY
$ npx wrangler secret put APNS_KEY_P8
$ npx wrangler secret put FCM_SERVICE_ACCOUNT
$ npm run deploy

Paste the worker URL in Settings → Notifications. Leave it empty and the app falls back to background polling.

Get Nullchat

Phase 1 is being built in the open. Builds land here first; star the repo to hear about it.