Stade is a private messenger built on a simple promise: a message belongs only to the two people having the conversation. No company sits in the middle, no server stores your chats, and no account ties your identity to a phone number or an email address.
What makes Stade different
Most messaging apps route your messages through their servers. Even when those messages are encrypted, the provider still sees who you talk to, when, and from where. Stade removes that middleman entirely:
No servers. Two devices connect directly to each other. The app is the whole network.
No accounts. You are identified by a cryptographic key you generate on your own device. Nothing is registered anywhere.
End-to-end encryption by default. Every message is sealed so only the recipient's device can open it. Keys never leave your device.
Post-quantum cryptography. Stade combines classical elliptic-curve cryptography with NIST's post-quantum algorithms (ML-KEM and ML-DSA), so conversations stay protected even against a future quantum adversary.
Metadata resistance. Conversations can travel over Tor, hiding not just what you say but who you talk to and where you are.
What you can do with it
Exchange one-to-one text messages with full end-to-end encryption.
Create and participate in group chats.
Broadcast to an open audience with a Stadium, a public channel where only the admin posts.
Connect over your local network (fast, zero-internet) or over Tor (anonymous, works across the internet).
Verify a contact's identity with a safety number, so you know there is no impostor in the middle.
Lock the app behind a password that encrypts your entire message database at rest.
Where it runs
Stade is a single application that runs on Android 8.0 (API 26) and newer, and on the desktop — Windows, Linux and macOS. The same cryptography, identity format and wire protocol are shared across every platform, so an Android user and a desktop user are simply two peers talking to each other.
Because there is no server, Stade cannot read, log, recover or reset anything for you. That is the point — but it also means you hold the keys. Lose your device and your backup, and that identity and its conversations are gone.
Installation
Stade ships as a native application for Android and for the desktop. You can install a prebuilt package or build it yourself from source.
Android
Requirements: Android 8.0 (API level 26) or newer.
Download the Stade APK (composeApp-debug.apk for development builds, or a signed release APK).
Allow installation from your browser or file manager (Settings → Apps → Special access → Install unknown apps).
Open the APK and tap Install.
The embedded Tor binaries for every Android ABI (arm64-v8a, armeabi-v7a, x86_64, x86) are bundled inside the APK, so Tor works out of the box with no extra download.
Desktop
Stade produces native installers per operating system:
OS
Package format
Windows
.exe installer
Linux
.deb and AppImage
macOS
.dmg
Install the package the same way you would any other desktop app. On first launch the app extracts its bundled Tor runtime into a private application directory. On Linux it installs as stade, categorized under Network.
Building from source
Stade is a Kotlin Multiplatform + Compose Multiplatform project built with Gradle.
Requirements:
JDK 17 (the project pins a JVM toolchain to 17).
The Android SDK for Android builds, with compileSdk 35.
An internet connection on the first build — Gradle downloads the official Tor Expert Bundle (version 13.5.6) for each target and stages it into the app's resources.
./gradlew :composeApp:assembleDebug # debug APK
./gradlew :composeApp:assembleRelease # release APK (needs a keystore)
./gradlew :composeApp:run # run the desktop app
./gradlew :composeApp:packageDistributionForCurrentOS # native desktop installer
Tor binaries are hash-checked at build time. You can pin each platform's expected SHA-256 via gradle.properties; an unpinned hash logs a warning, a mismatched pinned hash fails the build.
Native desktop packaging is host-only.jpackage builds an installer only for the OS it runs on — build the .deb/AppImage on Linux, the .exe on Windows, the .dmg on macOS.
Cloud-synced folders are redirected. Inside OneDrive, Dropbox, Google Drive, iCloud, pCloud or Box, build output relocates to ~/.stade-build/<project-name>/… so the sync client doesn't fight Gradle.
Release signing (Android)
To produce a signed release, add your keystore to local.properties:
If no keystore is configured, release builds fall back to the debug signing key so the build still succeeds for testing.
Quick Start
This guide takes you from a fresh install to your first encrypted message.
1. Create your identity
The first time you open Stade, you are asked for a nickname. Type one and tap Create. That is the entire signup — Stade generates a fresh set of cryptographic keys on your device and derives your Stade ID from them:
STADE-XXXX-XXXX-XXXX
This is your public address. It is safe to share; it contains no private information.
2. Share your invite
To let someone add you, share your invite — a self-contained, signed bundle carrying your nickname, public keys and current reachable addresses (LAN and/or Tor). Share it as a STADE2-… text code, a QR code, or a .stadeid file. It is signed twice — classical and post-quantum — so the recipient's app can confirm it really came from your keys.
3. Add a contact
Open Add Contact and paste their STADE2-… code, scan their QR, or open their .stadeid file. Give the contact a name and confirm. At this point Stade has parsed and verified the invite locally, but the contact is not saved yet — it first needs to connect to the peer and complete a live handshake.
4. Wait for the connection
LAN — if you are both on the same network, this is nearly instant.
Tor — works across the internet, but the first connection can take a few seconds to a couple of minutes while Tor bootstraps and the hidden-service descriptor propagates. This is normal.
Both peers must be online and running the app for the handshake to complete. The connection manager keeps retrying with backoff, so you don't have to time it perfectly.
5. Send a message
Open the conversation, type and send. The first message establishes a Double Ratchet session; every message after that uses a fresh key, so even if one key were ever exposed, your other messages stay protected.
6. Verify your contact (recommended)
Open the contact's profile and tap Verify. Compare the safety number shown on both devices — read it aloud or compare in person. If they match, you have cryptographic proof there is no impostor in the middle.
That's it — you now have a serverless, end-to-end-encrypted conversation. If a connection won't establish, see Troubleshooting.
Creating Your Identity
In Stade there is no signup server, no username database and no password recovery. Your identity is a set of cryptographic keys generated locally the moment you choose a nickname.
What gets generated
When you create an identity, Stade generates four key pairs and stores them in your encrypted local database:
Purpose
Algorithm
Family
Signing (identity)
Ed25519
Classical
Key agreement (DH)
X25519
Classical
Key encapsulation (KEM)
ML-KEM-768
Post-quantum
Signing (PQ)
ML-DSA-65
Post-quantum
The Ed25519 and ML-DSA pairs prove that messages and invites really came from you — Stade signs with both, so an attacker would have to break a classical and a post-quantum signature to impersonate you. The X25519 and ML-KEM pairs establish shared secrets with a contact; combining them means the session key is secure unless both are broken. The private halves never leave your device.
How your Stade ID is derived
Your Stade ID is a short, human-readable fingerprint of your public identity keys:
Concatenate a domain label (stade-id-v1), your Ed25519 public signing key and your ML-DSA public key.
Hash that with BLAKE2b-256.
Take 60 bits of the digest as the identity value, plus a 4-bit checksum.
Encode in Crockford Base32 (no ambiguous I, L, O, U) — 12 characters grouped STADE-XXXX-XXXX-XXXX.
Because the ID is derived from your keys, it cannot be forged: anyone who receives your invite recomputes the ID and checks it matches. The format is also typo-tolerant — input is uppercased, spaces/dashes stripped, and look-alikes (O→0, I→1, L→1) remapped before the checksum is validated.
What is safe to share
Your Stade ID and invite are public. They contain only public keys and reachability hints — sharing them is how people add you.
Your private keys are never shared, exported or transmitted. They live only in your encrypted database.
No server holds a copy of anything. Lose your device and your backup of the app's data directory, and the identity and its conversations are unrecoverable. A new identity means a new Stade ID, and contacts would need to add you again.
Architecture Overview
Stade is one application that runs on Android and desktop from a single shared codebase. This is the map: the major pieces, how they layer, and how a message travels from your keyboard to your contact's screen.
One codebase, many platforms
Stade is written in Kotlin Multiplatform (KMP) with a Compose Multiplatform UI, organized into layered source sets:
commonMain — the bulk of the app: UI, identity, contacts, messaging, the handshake, the Double Ratchet, the sync protocol and transport interfaces. Platform-agnostic.
jvmCommonMain — shared by Android and desktop: the BouncyCastle-backed cryptography, the LAN and Tor transports, and the encrypted vault.
androidMain / desktopMain — thin platform layers: the Android Activity/Service, the desktop main(), Tor binary loading and file pickers.
Platform-specific behavior is declared in commonMain as an expect and implemented per platform with actual — for example platformCrypto() and platformPq() resolve to the JVM/BouncyCastle implementations.
The layers
UI Compose screens, view state
App services ContactManager / MessageManager, ChatService / GroupManager, SyncEngine
Cryptography CryptoApi (Ed25519, X25519, AEAD, HKDF), PqCrypto (ML-KEM-768, ML-DSA-65),
HandshakeService, DoubleRatchet
Transport ConnectionManager (dial/retry/backoff), LanTransport, TorTransport
Storage SQLDelight / SQLite, Vault (password-encrypted DB at rest)
The single object that wires everything together is the AppContainer — it constructs the crypto providers, the database, the managers, the transports and the sync engine, and exposes them to the UI.
How a message flows
SyncEngine.queueOutgoing asks the Double Ratchet session to seal the plaintext, producing a ratchet frame with a fresh message key.
The sealed frame is wrapped in a MessagePayload, serialized and placed in the Outbox so it survives until acknowledged.
The active session sends the frame over whichever transport is connected, using length-prefixed framing (FrameCodec: a one-byte record type + four-byte length + payload).
The peer's Double Ratchet opens it, the message is stored and shown, and an ACK comes back to mark it delivered.
A background pinger keeps the connection warm, and the ConnectionManager continuously dials un-connected contacts with exponential backoff until a session is established.
Where state lives
Messages, contacts, groups and transport settings live in a local SQLite database accessed through SQLDelight (package dev.stade.db). At rest that database is encrypted by the Vault under a key derived from your password; while unlocked a decrypted working copy exists, and when you lock or close it is re-encrypted and the plaintext securely wiped.
Design principles
No server, ever. Every peer is symmetric; there is no privileged node.
Defense in depth via hybrid crypto. Classical and post-quantum algorithms are combined so a break in either family alone does not compromise a session.
Fail closed. Handshakes that don't match expected keys, sizes, protocol versions or transcript commitments are rejected rather than downgraded.
Peer-to-Peer Model
Stade has no servers. There is no central node that relays, stores or brokers your messages. Every device runs the same code and is a full, equal peer.
Two peers, one direct connection
A conversation is a direct connection between two devices. Each device both listens for incoming connections and dials out to its contacts. When two contacts are online and reachable, one side's dial meets the other's listener and a session is born. Because the link is direct:
Your messages are never uploaded to a third party.
There is no operator to be subpoenaed, hacked, or to go out of business and take your data.
Delivery requires both peers online at overlapping times. Stade keeps an Outbox and retries, but a message only arrives once a connection is actually established.
How peers find each other
A peer can be reached at one or more addresses, each tagged by transport:
lan://192.168.1.42:5901 a local-network address
tor://<onion-address>:<port> a Tor hidden-service address
Addresses are exchanged inside your invite and refreshed inside the HELLO at the start of every handshake, so contacts learn your current addresses automatically. On the LAN, peers also broadcast their presence over UDP so they can discover each other with nothing typed in.
The connection manager
The ConnectionManager turns "I have a contact" into "I have a live session." It runs continuously and:
Tries each known address of any contact not currently connected, through the matching transport.
Applies backoff so an offline peer isn't hammered.
Runs a separate pending-dial loop for freshly added invite addresses.
Skips your own addresses and de-duplicates parallel attempts.
Every attempt is recorded as a diagnostic (TRYING → CONNECT_OK → HANDSHAKE_OK, or the matching failure), which powers the connection-status detail in the UI.
Establishing a session
Connecting at the socket level is only step one. Before any message flows, the two peers run a mutual handshake that exchanges identities, proves possession of both the classical and post-quantum private keys, checks each Stade ID matches its keys, rejects version/size mismatches and downgrades, and derives a shared root key from a hybrid Diffie–Hellman + ML-KEM exchange. Only then is the contact considered connected — and, if it was new, actually saved.
Resilience
Self-healing transports. The Tor transport restarts its process if it dies; the LAN transport falls back across a range of ports.
Multiple addresses, raced in parallel. If a peer is reachable on both LAN and Tor, both are attempted and the first handshake wins.
Idempotent delivery. Messages carry IDs; a peer that already has one just re-acknowledges instead of storing a duplicate.
Because no server aggregates data, no party — not even Stade's developers — has a contact graph, message logs, timestamps or address books. That information exists only on the two devices in a conversation.
Encryption
Stade's encryption has one job: make sure only the two devices in a conversation can read it — and keep that true even against an attacker who records everything today and owns a quantum computer tomorrow.
Hybrid by design
Every secret-establishing and authentication step is hybrid: it combines a well-trusted classical algorithm with a NIST post-quantum one.
The shared secret feeding a session is the concatenation of the classical and post-quantum secrets, run through a KDF. An attacker must break both families to compromise a session.
Stage 1 — The handshake (PQXDH-style)
HELLO ↔ HELLO. Each side sends its identity (Stade ID, nickname, all four public keys), a random 32-byte nonce, its addresses and a transcript commitment hashing the protocol version and all public keys. Each side checks the peer's Stade ID derives from those keys, that sizes and versions match, and that the commitment matches what it recomputes. A mismatch is treated as a possible downgrade attack and rejected.
AUTH ↔ AUTH. Each side signs a message built from the peer's nonce and both commitments using both Ed25519 and ML-DSA private keys; the peer verifies both. The nonce prevents replay.
KEM_OFFER. The peers are ordered deterministically (lexicographically by Stade ID) into initiator and responder; the initiator encapsulates a secret to the responder's ML-KEM key and the responder decapsulates it.
Root key derivation. Each side concatenates the X25519 secret with the ML-KEM secret and runs it through HKDF, salted with a hash of the full transcript, to produce the 32-byte root key.
Because the root key is bound to the entire transcript, both peers derive the same root key only if they saw the exact same, untampered handshake.
Stage 2 — The Double Ratchet
The root key seeds a post-quantum Double Ratchet that protects the ongoing conversation:
Forward secrecy — each message uses a unique key from a one-way chain, so compromising today's key does not reveal yesterday's messages.
Post-compromise security (self-healing) — the session periodically performs a fresh key exchange, locking an attacker back out.
Each DH ratchet step performs a fresh X25519 exchange and generates a fresh ML-KEM key pair, mixing the post-quantum secret back into the root key continuously.
Message keys derive a separate AEAD key and nonce; the plaintext is sealed with ChaCha20-Poly1305, with the header authenticated as associated data.
Out-of-order messages are handled by caching skipped keys (bounded window of 128). Decryption is transactional — state rolls back if authentication fails, so a forged frame can't poison the session.
Stage 3 — Encryption at rest
Your message database is stored as a file encrypted with AES-256-GCM under a random data-encryption key (DEK).
That DEK is itself encrypted with a key derived from your password via PBKDF2-HMAC-SHA256 (210,000 iterations).
On unlock the DEK is recovered and the database decrypted into a working copy; on lock or quit it is re-encrypted and the plaintext securely overwritten and deleted.
What an attacker on the network sees
An adversary capturing Stade traffic sees length-prefixed ciphertext frames — no contents, no ability to forge or inject without the private keys. Over Tor they additionally cannot tell who is talking to whom or where. The one thing encryption alone can't prevent is a man-in-the-middle substituting keys at first contact — which is what Verifying a Contact catches.
Identity & Keys
Stade replaces accounts with keys. This describes the keys that make up an identity, how your Stade ID and invite are built from them, and how each key is used.
The four key pairs
Key pair
Algorithm
Used for
Signing
Ed25519
Authenticating you (classical signatures)
Handshake (DH)
X25519
Classical Diffie–Hellman key agreement
KEM
ML-KEM-768
Post-quantum key encapsulation
PQ signing
ML-DSA-65
Authenticating you (post-quantum signatures)
The public halves are packed into your invite; the private halves never leave the device. A LocalIdentity holds all four pairs plus your nickname and creation time; a contact's RemoteIdentity holds the same four public keys with no private material.
The Stade ID
Your Stade ID is a compact, self-certifying fingerprint of your public identity, formatted STADE-XXXX-XXXX-XXXX. It hashes (BLAKE2b-256) a domain label with your Ed25519 and ML-DSA public keys, takes 60 bits plus a 4-bit checksum, and encodes in Crockford Base32. Anyone with your invite recomputes it — if the keys were swapped, the ID wouldn't match. Because both signing keys feed the hash, the ID commits you to both.
The invite
Where the Stade ID is a fingerprint, the invite is the full public bundle. It is a binary record, Base32-encoded and prefixed STADE2-, containing a magic marker and version, your nickname, your four public keys, your reachable LAN/Tor addresses, and two signatures (one Ed25519, one ML-DSA). On parse, the app checks magic/version/sizes, verifies both signatures, and recomputes the Stade ID. Only a fully valid, doubly-signed invite is accepted. Invites can also be a QR code (chunked when large) or a .stadeid file.
How each key is used at runtime
Ed25519 + ML-DSA — sign your invite and each handshake's AUTH message; verified on every connection.
X25519 — the classical half of the handshake secret, and the ratcheting key that advances the Double Ratchet.
ML-KEM-768 — the post-quantum half of the handshake secret, with a fresh ML-KEM key pair generated at each ratchet step to keep re-injecting post-quantum security.
Stade never relies on a single algorithm for either secrecy or authenticity. A future break of either the elliptic-curve or the lattice family — but not both — still leaves your identity and conversations protected.
Transport Layers
A transport is how two Stade peers actually move bytes between each other. The cryptography is identical no matter which transport carries it — a transport only has to deliver opaque, length-prefixed frames from one device to another.
Two transports
Transport
Address scheme
Best for
LAN
lan://<ip>:<port>
Same network — fast, offline
Tor
tor://<onion>:<port>
Across the internet — anonymous
Both are enabled by default and can be toggled individually in the app's transport settings.
The common transport contract
Every transport implements the same small interface: start (listen and hand connections to the sync engine), connect(address) (dial a peer), selfAddress / selfAddresses (report current reachability for invites and HELLO), and stop. A connection is just send(frame), receive() and close(), with a 4-byte length prefix per frame. Each transport also publishes a live status (TransportInfo) the UI surfaces, e.g. "SOCKS5 ✓ · onion ✓" for Tor.
How a transport is chosen
Addresses are self-describing by scheme: lan://… routes to the LAN transport, tor://… to the Tor transport. If the relevant transport is disabled or not yet running, the attempt is queued and retried once it's up. A peer advertising both a LAN and a Tor address is dialed on both in parallel, and the first to complete a handshake wins — which is why two people on the same network connect almost instantly while still being reachable over Tor when apart.
Why two transports
LAN is fast and works with no internet, but only when both peers share a network, and it reveals local IPs to peers on that network.
Tor works anywhere there's internet and hides both endpoints, at the cost of higher latency and a bootstrap delay.
Having both lets Stade be quick when it can and private when it must, without you having to think about it. Read on: LAN · Tor.
LAN Transport
The LAN transport connects two peers directly over a shared local network — the same Wi-Fi, switch or hotspot. It is the fastest way for two nearby devices to talk and needs no internet at all.
TCP 5901UDP discovery 5902offline
Addressing
A LAN address is just a local IP and a TCP port:
lan://192.168.1.42:5901
Stade advertises a LAN address for every non-loopback, site-local IPv4 interface it finds (10.x, 172.x, 192.168.x), so a peer can reach you on whichever interface you share.
Listening
On start, the transport binds a TCP server socket on port 5901. If busy, it walks forward through 5901–5910 until it finds a free one and reports the actual bound port. Incoming connections are handed straight to the sync engine for the handshake.
Local discovery
LAN peers find each other automatically via a lightweight UDP discovery service:
It broadcasts a presence beacon — STADE|<node-id>|<tcp-port> — every 15 seconds.
It listens on UDP port 5902 for other beacons, ignoring its own.
Discovered peers expire after 60 seconds of silence, so the list reflects who is present right now.
This is what lets two people on the same network connect with essentially zero setup. Frames use the same 4-byte big-endian length prefix as every Stade connection, capped at 4 MB.
Strengths and limits
Strengths: near-instant connection on the same network; fully offline; auto-discovery means addresses often don't need sharing.
Limits: only works when both peers share a network; your local IP is visible to peers on that network (content stays end-to-end encrypted, but LAN is not anonymous); some networks block UDP broadcast or use "AP isolation," disabling discovery. If LAN isn't possible, Stade falls back to Tor.
Tor Transport
The Tor transport lets two peers reach each other across the internet while hiding who they are and where they are. Each device runs as a Tor hidden (onion) service, so neither side learns the other's IP and an observer can't tell they're communicating.
v3 onionembedded TorSOCKS5
Addressing
tor://<onion-address>:<port>
The onion address is your device's hidden-service identity, advertised in your invite and in the HELLO at the start of each handshake so contacts always have your current address.
Embedded Tor — no Orbot, no setup
Stade bundles its own Tor runtime (the official Tor Expert Bundle, version 13.5.6, downloaded and hash-verified at build time) for every platform. On desktop the correct binary is extracted into a private directory on first run; on Android the binaries for all ABIs (arm64-v8a, armeabi-v7a, x86_64, x86) ship inside the APK with geoip data as assets. You do not need Orbot or a system Tor.
How it works
Binds a local TCP listener on a free loopback port (where your onion service forwards incoming connections).
Launches the embedded Tor process, which bootstraps (you'll see progress like "Tor boot 80% · Loading relay descriptors") and publishes an onion service pointing at that port.
Exposes a local SOCKS5 proxy for outgoing connections. To dial a peer, Stade performs the SOCKS5 handshake itself and asks Tor to connect to the peer's onion address.
Its status reads, e.g., SOCKS5 ✓ (127.0.0.1:…) · onion :<port> ✓.
Self-healing & external Tor
A health monitor checks roughly every 10 seconds that Tor is alive and brings it back up automatically (rate-limited) if it exits. If you prefer a system Tor or Orbot, the transport can run in external mode, probing common SOCKS ports (9050 and 9150) and auto-selecting one that responds. The default embedded mode needs no configuration.
Strengths and limits
Strengths: works across the internet through NAT and firewalls with no port forwarding; hides both endpoints' IPs and the existence of the conversation; no external dependencies.
Limits: higher latency than LAN and a bootstrap delay; the first connection to a new peer can take from seconds to a couple of minutes while Tor bootstraps and the descriptor propagates (expected — Stade keeps retrying); a few networks actively block Tor.
Stadium
A Stadium is a public, one-to-many broadcast channel inside Stade — one admin posts, and anyone with the link can subscribe and read. Use it for announcements, updates, or broadcasting to an open audience, rather than a specific set of contacts.
What a Stadium is
A Stadium works like the "channel" feature in other messaging apps: a single admin — its creator — posts, and everyone else can only read. There is exactly one admin per Stadium, and it cannot be shared or transferred.
It differs from a group chat in one important way: joining is public and one-step. A Stadium's invite is a single shareable link, and anyone who has it can join directly — you do not need to already be connected to the admin first.
Creating a Stadium
Open the + / new-chat menu and choose New Stadium.
Give it a name.
Post whenever you like — there's nothing else to configure.
Joining a Stadium
Open the + menu and choose Join Stadium.
Paste the invite link.
That's it — no prior connection to the admin is needed, unlike adding a contact or being added to a group.
Posting and the subscriber feed
Only the admin can post: text, photos and voice messages, using the same crop-and-draw photo editor as regular chats. Everyone else sees a read-only feed with no compose box at all — subscribers cannot post, reply or react.
Notifications and muting
New posts notify subscribers the same way a normal message does. Each Stadium can be muted independently — long-press it on mobile or right-click it on desktop — without muting anything else.
Privacy and encryption
Posts are end-to-end encrypted the same way 1:1 and group messages are — nothing about a Stadium's content, member list, or who subscribed to what is ever visible to anyone but the people in it.
No one can ever see who else has subscribed to a Stadium — not other subscribers, and not even the admin. The only visible signal is a live subscriber count shown next to the Stadium's name.
Managing your Stadium
From the Stadium's management screen (the gear icon), the admin can:
Copy the invite link to share anywhere — social media, other chats, wherever.
Rename the Stadium.
See the live subscriber count.
Delete the Stadium.
Deletion is local to the admin's device: subscribers keep their own copy of everything posted before it, they just stop receiving new posts.
Common questions
Can a Stadium have more than one admin?
No. Each Stadium has exactly one admin: whoever created it.
Can subscribers react or reply?
Not currently. The feed is read-only for everyone except the admin.
Can I see who joined my Stadium?
No — not even as the admin. Only the live subscriber count is shown.
Verifying a Contact
End-to-end encryption guarantees only the holder of a contact's keys can read your messages. What it can't tell you on its own is whether the keys you received at first contact really belong to the right person. Verifying a contact closes that gap.
The safety number
For every contact, Stade computes a safety number: a 60-digit value derived by hashing both participants' Ed25519 signing public keys (sorted, so both devices compute the identical number) with BLAKE2b-256 and formatting the result in groups of five digits.
It is symmetric — you and your contact see the same number for each other.
It is bound to the keys — if either side's keys were substituted, the numbers would not match.
How to verify
Open the contact and choose Verify.
You'll see the contact's avatar, nickname, Stade ID and the safety number.
Have your contact open the same screen for you.
Compare the safety numbers through a trusted channel — read them aloud in person or over a voice call.
If they match exactly, tap Mark as Verified (you'll see a "Verified" badge). If they don't match, do not mark them — treat it as a red flag and re-exchange invites through a trusted channel.
Verification is a local action: it records your own confidence and does not notify anyone or change the encryption.
When to verify
Always, ideally, for any contact whose conversations matter.
Especially if you exchanged invites over an untrusted channel — that initial exchange is the one moment a man-in-the-middle could strike.
Again if a safety number ever changes unexpectedly — that means the contact's identity keys changed (e.g. they reinstalled), so re-verify before trusting it.
Verification confirms you and your contact hold matching keys — it doesn't prove their real-world identity beyond the out-of-band channel you used, and it doesn't add encryption (your messages were already end-to-end encrypted; this confirms to whom).
Locking the App
Stade can lock itself behind a password that encrypts your entire message database on disk. When locked, your conversations exist only as ciphertext — without your password there is no way for you, an attacker, or anyone to read them.
How the lock works
Stade uses a layered key model so your password protects everything without ever being stored:
Your password → a key (KEK). Stretched with PBKDF2-HMAC-SHA256, 210,000 iterations and a random salt, producing a key-encryption key.
A random data key (DEK) encrypts the database with AES-256-GCM.
The KEK encrypts the DEK and stores it (encrypted) alongside a small verifier used to check whether an entered password is correct.
So your password never touches disk — only the salt, the encrypted DEK and the verifier do. On unlock, Stade derives the KEK, checks the verifier, then decrypts the DEK and the database into a working copy. On lock or close, it re-encrypts the database and securely deletes the plaintext copy by overwriting with zeros and flushing to disk; the key is also zeroed in memory.
Protection against guessing
To resist password attempts on a seized device, Stade enforces an escalating lockout: after a threshold of failures (5) the required wait grows step by step — seconds, then minutes, then up to a full day. The counter resets on a successful unlock.
Session timeout
You choose how quickly Stade re-locks when idle or backgrounded: Immediately, or after 30 seconds, 1 minute, 5 minutes, 15 minutes, 1 hour, or Never. "Immediately" locks as soon as the app leaves the foreground.
Extra hardening
Scramble keypad — randomizes the on-screen PIN layout so an onlooker can't learn your entry from finger position.
Screenshot blocking — prevents screenshots and keeps Stade out of the app switcher / thumbnails (on by default).
Changing your password & wiping
Changing your password re-derives a fresh KEK and re-wraps the existing DEK — messages are never re-encrypted from scratch and no data is lost. Wiping securely deletes the encrypted database, any plaintext copy, the vault metadata and the session file.
There is no recovery. The database can only be decrypted with a key derived from your password, and that key is never stored. Forget the password and the messages are unrecoverable — by anyone. That's the guarantee, not a limitation.
Troubleshooting
Because Stade is fully peer-to-peer, "it won't connect" almost always comes down to reachability — one peer can't currently reach the other — rather than a server being down.
A contact won't connect
Both peers must be online and running the app. A session only forms when both sides are reachable at the same time; Stade keeps retrying with backoff, so leave both apps open. Then, in order:
Are the transports up? Check transport settings — for Tor you want SOCKS and onion both ✓; for LAN a bound IP/port.
Same network? Try LAN. It connects in seconds; if it doesn't, the network may block UDP broadcast or isolate clients.
Apart? Give Tor time. The first connection to a new peer can take seconds to a couple of minutes while Tor bootstraps and the descriptor propagates.
Stale addresses. If a contact's advertised address is old and you've never connected, re-exchange invites so current addresses propagate.
Adding a contact "does nothing"
An invite is parsed and verified locally, but the contact is only saved after a live handshake completes. So if the other side isn't reachable yet, it can look like nothing happened. Make sure both fields (invite code and a contact name) are filled, the other peer is online, and watch the diagnostics — you should see TRYING → CONNECT_OK → HANDSHAKE_OK. If you only ever see TRYING or CONNECT_FAIL, it's a reachability problem, not a key problem.
Reading Tor SOCKS errors
Message
Likely cause
SOCKS5 unavailable
Tor isn't running yet, or an external Tor/Orbot isn't up.
host unreachable (onion descriptor)
The peer's onion descriptor hasn't propagated yet — wait.
connection refused
The peer is reachable but not listening on its onion port.
network unreachable
Local network can't reach Tor at all.
Most onion-descriptor and "host unreachable" cases resolve themselves as Tor finishes publishing the hidden-service descriptor.
Tor seems stuck "bootstrapping"
The status shows progress like Tor boot 45% · …; on a slow or restricted network the first bootstrap can take a while. If the process dies, Stade's health monitor restarts it (rate-limited) — you may briefly see "Tor process exited — restarting…". A few networks block Tor entirely; try a different network, or LAN if you're on the same one as your contact.
LAN auto-discovery isn't finding peers
Both devices must be on the same subnet, and the network must allow UDP broadcast and client-to-client traffic. Public Wi-Fi and many corporate networks enable "AP/client isolation," which blocks peer-to-peer connections and discovery. Guest networks and VPNs can also separate clients — try a phone hotspot or home network to confirm.
Port already in use (LAN)
The LAN transport prefers TCP port 5901 but falls back through 5901–5910, reporting the port it bound. An "all ports in use" message means you should free the range or close other instances.
Desktop app won't launch
Ensure you're running a JDK 17 runtime. If launching from an IDE run configuration, make sure the configured main class is current (dev.stade.MainKt) — a stale main class from an older build causes an immediate exit.
I forgot my password
There is no recovery. The database is encrypted with a key derived from your password, and that key is never stored. See Locking the App.
Cryptographic Primitives
A reference for the exact algorithms, parameters and constructions Stade uses. Cryptography is provided by BouncyCastle on the JVM (shared by Android and desktop) behind two interfaces: CryptoApi (classical) and PqCrypto (post-quantum).
Algorithm summary
Role
Algorithm
Notes
Hash
BLAKE2b-256
IDs, safety numbers, KDF base
Classical signatures
Ed25519
32-byte key, 64-byte signature
Classical key agreement
X25519 (ECDH)
32-byte keys
AEAD (messages)
ChaCha20-Poly1305
128-bit tag, 96-bit nonce
Key derivation
HKDF / BLAKE2b-256
Domain-separated by info labels
Post-quantum KEM
ML-KEM-768
Public key 1184 B, ciphertext 1088 B
Post-quantum signatures
ML-DSA-65
Public key 1952 B, signature 3309 B
At-rest password KDF
PBKDF2-HMAC-SHA256
210,000 iterations, 256-bit output
At-rest encryption
AES-256-GCM
128-bit tag, 96-bit nonce
Classical & post-quantum primitives
The classical CryptoApi provides BLAKE2b hashing, Ed25519 signing, X25519 agreement (raw 32-byte shared secret), hkdf(secret, salt, info, length) over BLAKE2b-256 with domain-separation labels (e.g. stade-dr-chain, stade-dr-key, stade-dr-nonce), ChaCha20-Poly1305 AEAD (aeadOpen returns null on any auth failure rather than throwing), and SecureRandom. The post-quantum PqCrypto provides ML-KEM-768 (FIPS 203: encapsulate/decapsulate) and ML-DSA-65 (FIPS 204: sign/verify, returning false on error).
Identity derivation
material = "stade-id-v1\0" || ed25519_pub || mldsa_pub
digest = BLAKE2b-256(material)
id60 = first 60 bits of digest
crc4 = 4-bit checksum from the digest
value = (id60 << 4) | crc4
StadeID = "STADE-" + CrockfordBase32(value) // 12 chars, grouped 4-4-4
The Crockford Base32 alphabet is 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U); input normalization maps O→0, I→1, L→1.
lo/hi denote the two parties' values in canonical (sorted) order so both derive the identical transcript. Authentication signs "stade-auth-v2" || peer_nonce || own_commitment || peer_commitment with both Ed25519 and ML-DSA; a transcript commitment BLAKE2b-256("stade-tc-v2" || version || sign_pub || handshake_pub || mlkem_pub || mldsa_pub) is exchanged and checked to detect downgrade/tampering.
Double Ratchet construction
Chain step:HKDF(chainKey, info="stade-dr-chain", len=64) → next chain key (32 B) + message key (32 B).
DH ratchet step: fresh X25519 secret + fresh ML-KEM key pair folded into the root via HKDF(dh || kem_ss, salt=rootKey, info="stade-pqdr-step-v2", 64).
KEM mix: an arriving ML-KEM ciphertext mixes via HKDF(kem_ss, salt=rootKey, info="stade-pqdr-mix-v2", 32).
AEAD: ChaCha20-Poly1305 with the ratchet header authenticated as associated data.
Skipped keys bounded to a window of 128; decrypt is transactional with rollback on auth failure.
Wire framing & at-rest vault
[ 1 byte record type ][ 4 byte big-endian length ][ payload ] max 4 MiB
Record types include HELLO, AUTH, KEM_OFFER, MESSAGE, ACK, PING and BYE. The at-rest vault uses PBKDF2-HMAC-SHA256 (210,000 iterations, 16-byte salt), a random 256-bit DEK encrypted with AES-256-GCM, a verifier to detect wrong passwords, secure zero-overwrite deletion, and an escalating lockout (threshold 5; delays from seconds to up to 24 hours). Version-tagged labels (-v2) and explicit record-type codes mean the app's package name can change without breaking on-the-wire compatibility.
Glossary
Terms used throughout the Stade documentation.
Term
Meaning
AEAD
Authenticated Encryption with Associated Data — encryption that also detects tampering. Stade uses ChaCha20-Poly1305 for messages and AES-256-GCM at rest.
AppContainer
The central object that constructs and wires Stade's services: crypto, database, managers, transports and the sync engine.
Backoff
The growing wait between repeated connection attempts to an unreachable peer.
BLAKE2b-256
The hash function used for Stade IDs, safety numbers, and as the basis for HKDF.
ConnectionManager
Continuously dials un-connected contacts and freshly added invite addresses, with backoff and diagnostics.
Crockford Base32
A Base32 encoding omitting I, L, O, U; used to format the Stade ID.
DEK
Data Encryption Key — the random key that encrypts your database, itself encrypted by a password-derived key.
Double Ratchet
The protocol protecting an ongoing conversation with a fresh key per message; Stade's variant adds post-quantum ML-KEM at each step.
Ed25519
The classical signature algorithm authenticating identities and handshakes.
Forward secrecy
Compromising a current key does not expose past messages.
Handshake
The mutual authenticated key exchange peers run before messaging (HELLO → AUTH → KEM_OFFER → root key).
HKDF
A key-derivation function turning shared secrets and chain keys into keys/nonces, domain-separated by an info label.
Hidden / onion service
A Tor feature letting a device accept connections without revealing its IP. Each Stade peer runs one.
Hybrid cryptography
Combining a classical and a post-quantum algorithm so breaking one family isn't enough.
Invite
A signed, Base32-encoded bundle (STADE2-…) carrying your nickname, public keys and addresses. Also a QR code or .stadeid file.
KEK
Key Encryption Key — derived from your password via PBKDF2, it encrypts the DEK.
ML-DSA-65
The NIST post-quantum signature algorithm (FIPS 204), used alongside Ed25519.
ML-KEM-768
The NIST post-quantum key-encapsulation mechanism (FIPS 203), used alongside X25519.
Outbox
The local queue of unacknowledged messages, re-sent until delivery is confirmed.
PBKDF2
The password-based KDF (HMAC-SHA256, 210,000 iterations) that turns your password into the KEK.
Peer
Any device running Stade. All peers are equal; there are no servers.
PQXDH
Post-Quantum Extended Diffie–Hellman — the handshake style deriving a root key from a hybrid X25519 + ML-KEM exchange.
Safety number
A 60-digit value from both contacts' signing keys; comparing it out of band confirms there's no man-in-the-middle.
Stade ID
Your public address, STADE-XXXX-XXXX-XXXX, a checksummed fingerprint of your signing keys.
SOCKS5
The proxy protocol Stade speaks to the local Tor process for outgoing onion connections.
Sync engine
Runs the handshake on each new connection, then manages the per-contact message session.
Transcript commitment
A hash binding a peer's protocol version and public keys, checked during the handshake to detect tampering.
Vault
The at-rest encryption layer: password-encrypts the database and handles unlock, lock, lockout and secure deletion.
X25519
The classical Diffie–Hellman algorithm for the classical half of key exchange and the ratcheting key.
Frequently Asked Questions
Quick answers to common questions about how Stade works.
Is Stade really serverless?
Yes. There is no Stade server, relay or account service. Two devices connect directly over the local network or Tor — the app is the entire network. Nothing about your conversations is stored anywhere except on the two devices in them.
Do I need an account, phone number or email?
No. You create an identity locally by choosing a nickname; Stade generates your keys on the device and derives your Stade ID from them. Nothing is registered.
How do I add someone, and why not immediately?
Exchange invites (a STADE2-… code, QR, or .stadeid file). The invite is verified locally, but the contact is only saved once a live handshake completes — both peers must be online and reachable.
Why is the first Tor connection slow, and do I need Orbot?
Tor has to bootstrap and the peer's onion descriptor has to propagate, so the first connection can take seconds to a couple of minutes — leave both apps open. You do not need Orbot: Stade bundles its own Tor runtime (it can also use an external one if you prefer).
What does "post-quantum" mean here?
Stade combines classical cryptography (X25519, Ed25519) with NIST post-quantum algorithms (ML-KEM-768, ML-DSA-65) in a hybrid scheme. An attacker would have to break both to compromise a conversation — so it stays secure even against a future quantum computer, including "harvest now, decrypt later" attacks.
Can someone impersonate a contact?
The handshake authenticates both sides with two independent signature schemes, so a network attacker can't inject themselves. The one residual risk is key substitution during your first invite exchange — which is exactly what safety-number verification catches.
What happens to my messages if I'm offline?
Outgoing messages wait in an Outbox and are re-sent until acknowledged. Delivery still requires both peers online at some overlapping time, since there's no server to hold messages.
Are my messages encrypted on disk, and can I recover a lost password?
Locally they're in a SQLite database; with a password set it's encrypted at rest with AES-256-GCM under a PBKDF2-derived key (210k iterations). There is no recovery — the key is derived from your password and never stored, so a forgotten password means the data can't be decrypted by anyone.
Which platforms, and are group chats supported?
Android 8.0+ and desktop (Windows, Linux, macOS), all from one shared codebase. Group messaging is supported in addition to one-to-one conversations.
Can I broadcast to a public audience instead of specific contacts?
Yes — that's what a Stadium is for: a public channel with one admin posting and anyone with the link able to join and read. See the Stadium guide.
Who can see who I'm talking to?
On LAN, peers on your local network can see your device's local IP (but not your messages). Over Tor, the endpoints' locations and the existence of the conversation are hidden. In neither case can anyone read message contents — and because there's no server, no third party (including Stade's developers) has any contact graph or logs.