# Building a messenger on Midnight: identity on-chain, messages off it

**URL:** <https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339>\
**Category:** Ecosystem\
**Tags:** ecosytem\
**Created:** [September 20, 2026, 10:15pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339 "2026-09-20T22:15:07Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![lucifer](https://avatars.discourse-cdn.com/v4/letter/l/ad7895/32.png) [@lucifer](https://forum.midnight.network/u/lucifer)\
**Post date:** [September 20, 2026, 10:15pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/1 "2026-09-20T22:15:07Z")

</div>

We have been building a private messenger on Midnight for a few months, and this is the first time any of it has been public. It is on Preprod, it is not open to the public yet, and we would rather show the on-chain parts and talk about what was hard than make an announcement.

**The idea**

Sign up with nothing. No phone number, no email, no wallet, no seed phrase to fund. You pick a username, and you can message someone else by their username. Messages are end to end encrypted and never touch the chain.

The split is the whole design:

**On-chain** :

proof that you control an identity. A commitment, its status, and the public key your device signs chat messages with. No usernames, no profiles, no message content, nothing that says who you are.

**Off-chain** :

the messages themselves, end to end encrypted with MLS (RFC 9420), passing through a relay that only ever sees cipher text.

**What is actually on Preprod**

The identity contract, four circuits, register, prove control, rotate and revoke:

> **[Preprod | Midnight Explorer](https://preprod.midnightexplorer.com/contracts/feab00bb69fc260340b63c0ceeac395b1ee2846b61a13987003ffee20dcb12c3)**
>
> The leading search, API & analytics platform for the Preprod Midnight blockchain.

Two real registrations, each one a different person’s device registering itself:

> **[Preprod | Midnight Explorer](https://preprod.midnightexplorer.com/transactions/692066ed5c5b7d31fb02258e17dd08a6a1452680ce35cbc4127228fb6730db91)**
>
> The leading search, API & analytics platform for the Preprod Midnight blockchain.

> **[Preprod | Midnight Explorer](https://preprod.midnightexplorer.com/transactions/1c585fde80e76363dccb8fa7c56b121b3fe3661eed403aa894774f3af3e9db40)**
>
> The leading search, API & analytics platform for the Preprod Midnight blockchain.

Both are `registerIdentity` calls. What the chain shows is a commitment and a signing key. What it does not show is who either of those people are, and there is nothing in the contract that could.

**Things that were harder than expected**

- **Nobody should have to hold tokens to sign up.**

Registration is a real transaction, and a real transaction costs DUST. So the user signs a transaction that pays no fee, hands it to a separate service, and that service pays out of its own balance and broadcasts it. The person never sees a wallet. This took longer to get right than anything else, and most of that time went on a single wrong assumption: the wallet reported itself ready long before its DUST state had actually synced, so every attempt failed with insufficient funds while the real cause was that it had barely started. The fix was to wait for the right stream, not a faster machine.

- **Usernames without a directory of usernames.**

People need to find each other by name, but a server holding a list of everyone’s names is exactly the thing this is supposed to avoid. So the server stores a hash of the username, not the name. Exact lookup works, browsing the list does not. The hash reuses the contract’s own `persistentHash` through a pure circuit, with a different domain prefix and a different arity from the identity commitment, so the two can never collide. A pure circuit turned out to be free to add: no new keys, no redeploy, and we verified that by diffing the compiled output of the existing circuits before and after, then landing a real sponsored transaction on the already-deployed contract.

- **A commitment you cannot reproduce is a commitment you have lost.**

The witness values behind an identity were originally random and held only in memory, which meant every registration silently created a throwaway identity and a restart could never prove control of the old one. They are now derived from the device seed, so the same seed always reproduces the same commitment. The test that convinced us was not a unit test: it was registering twice from the same device and watching the contract reject the second attempt with its own “identity already registered” assertion. A generic failure would have proved nothing. That specific one proved the commitment reproduced byte for byte.

- **Messages that arrive while you are away.**

MLS state lived in memory, so closing the app threw away the keys needed to read anything sent afterwards. Keeping it means the relay can hold a message for someone who is offline and hand it over when they return, then delete it. Getting that wrong is invisible in tests and obvious in practice: our first version handed the stored messages over before the client had finished loading its keys, and separately confirmed receipt before the message had actually been handled, so the server deleted messages that never arrived. Both only showed up when we ran it against the deployed build rather than a test harness.

**What we are not claiming**

It is on Preprod, not mainnet. The relay has no authentication yet, so anyone who learns a conversation id can join it, and that is the next real piece of work. A storing relay learns timing and pairing even though it cannot read anything, which is the ordinary cost of offline delivery and worth saying out loud rather than glossing over. Proving happens against a prover the user does not run, which is a genuine trust boundary and is documented as one rather than described as fully local.

**Where it goes**

Native iOS and Android are the real products. The web build exists to find bugs, and it has been doing that job well. Push notifications belong with the native apps, since the parts underneath, held messages and persistent keys, are already in place.

_Happy to answer questions about any of it._

---

<div class="post-metadata">

**Author:** ![lucifer](https://avatars.discourse-cdn.com/v4/letter/l/ad7895/32.png) [@lucifer](https://forum.midnight.network/u/lucifer)\
**Post date:** [September 21, 2026, 2:41am UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/2 "2026-09-21T02:41:45Z")

</div>

[![](https://canada1.discourse-cdn.com/flex010/uploads/midnightnetwork/original/1X/e9316235a589cc1d0a2f3fc535ff4496a1a75911.jpeg "midnight messager") ](https://www.youtube.com/watch?v=ibqLR1UqGtU)

---

<div class="post-metadata">

**Author:** ![Jay-Albert](https://avatars.discourse-cdn.com/v4/letter/j/4af34b/32.png) [@Jay-Albert](https://forum.midnight.network/u/Jay-Albert)\
**Post date:** [September 21, 2026, 6:38pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/3 "2026-09-21T18:38:57Z")

</div>

This is awesome, for the usernames, have you considered using something like [https://midnight.domains/](https://midnight.domains/) ?

It’s an interesting dapp. Let me know how you would like the foundation to best support you.

---

<div class="post-metadata">

**Author:** ![lucifer](https://avatars.discourse-cdn.com/v4/letter/l/ad7895/32.png) [@lucifer](https://forum.midnight.network/u/lucifer)\
**Post date:** [September 21, 2026, 7:05pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/4 "2026-09-21T19:05:16Z")

</div>

Hey Jay,

I haven’t gotten in touch with the Foundation yet, but yeah, it would be cool to get a subdomain from Midnight. `https://midnight.messanger` would be pretty cool.

---

<div class="post-metadata">

**Author:** ![Jay-Albert](https://avatars.discourse-cdn.com/v4/letter/j/4af34b/32.png) [@Jay-Albert](https://forum.midnight.network/u/Jay-Albert)\
**Post date:** [September 21, 2026, 7:21pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/5 "2026-09-21T19:21:14Z")

</div>

We wouldn’t be able to provide a subdomain since we don’t own the code and won’t maintain it.

Here are some ways to amplify your reach.

1. Add the topics Midnightntwrk and compact to your repo to appear in our developer reporting.
2. Share it with our Discord community: [Midnight Network](https://discord.gg/q7EmdKyM4U)
3. Submit a PR to Awesome dapps: [GitHub - midnightntwrk/midnight-awesome-dapps: Midnight Awesome Dapps · GitHub](https://github.com/midnightntwrk/midnight-awesome-dapps)
4. Claim your points on Zealy by submitting your dapp: [https://zealy.io/cw/midnightnetwork/questboard/85ce0bb0-d027-42a6-a1e6-444589f66f66/d6cd1b7d-28bb-4702-9273-872db33dd525](https://zealy.io/cw/midnightnetwork/questboard/85ce0bb0-d027-42a6-a1e6-444589f66f66/d6cd1b7d-28bb-4702-9273-872db33dd525)
5. Ready to turn your project into a business? Apply to the Build Club: [https://midnight.network/build-club](https://midnight.network/build-club)

If we can help with anything outside of the links provided above, feel free to reach out, and we’ll see how we can support you.

---

<div class="post-metadata">

**Author:** ![lucifer](https://avatars.discourse-cdn.com/v4/letter/l/ad7895/32.png) [@lucifer](https://forum.midnight.network/u/lucifer)\
**Post date:** [September 21, 2026, 7:46pm UTC](https://forum.midnight.network/t/building-a-messenger-on-midnight-identity-on-chain-messages-off-it/1339/6 "2026-09-21T19:46:25Z")

</div>

Sounds good, thank you.
