GET /v1/register/challenge issues a stateless challenge (version, difficulty,
expiry and a random nonce, HMAC-signed with a per-process key). The client
finds a counter so that SHA-256(domain || challenge || identity_key ||
counter) has the requested number of leading zero bits and sends it in the
X-Sepot-Pow header of POST /v1/register. Verification is one HMAC and one
SHA-256; used challenges are kept in memory until they expire.
SEPOT_REGISTER_POW=off|optional|required (default optional): in optional
mode registrations with a valid solution get a separate, more generous
per-IP bucket (SEPOT_RL_REGISTER_POW_*), registrations without one keep the
existing limit; both share the global cap. required answers 428 without a
valid solution. Difficulty: SEPOT_REGISTER_POW_BITS (18), raised by one bit
per doubling of the registration rate over SEPOT_REGISTER_POW_ADAPT_PER_MIN
up to SEPOT_REGISTER_POW_MAX_BITS (22). server-info advertises the mode.
The paused-registration 503 now carries Retry-After.
- Reject envelopes for addresses without a device (404) instead of
queueing them for 30 days; random addresses were the cheapest way
to fill the relay's disk.
- Per-sender hourly quota on stored envelopes (count and bytes, 429)
and a count cap on each recipient's queue next to the byte cap.
- Per-account attachment quota: bytes per day and bytes stored within
the TTL, kept in memory only (no account-to-blob link is stored).
- Disk watermarks: below a configurable share of free space the relay
refuses new attachments, then new offline envelopes (507), before
Postgres runs out of space.
- WebSocket buffers 4 KiB read / unbuffered write: ~16.6 KiB per
connection instead of ~137 KiB (10 500 connections measured at
~180 MiB including the DB pool).
- reqwest with HTTP/2: APNs only accepts HTTP/2, so no iOS wake-up
could ever be delivered. APNs registration with only a VoIP token
is stored instead of deleted.
- All limits configurable via environment (limits.rs). Defaults keep
previous behaviour except registration per IP (50 + 1/10 s, for
carrier-grade NAT) guarded by a global cap (40/s), separate Tor
buckets (SEPOT_TOR_PEERS) and a rate limit on opening WebSockets.
SEPOT_REGISTRATION_PAUSED=1 pauses new registrations (503).
- Envelopes whose encoding exceeds the queue column limit are 400
before they count against the sender quota.
Rate limiting moves from tower_governor to governor directly, so Tor
and clearnet can use different quotas on the same route.
- stop counting ACK and Pong frames toward the per-connection frame window;
draining a large queue used to trip the 429 limit, disconnect the client
and, after eight rounds, drop the remaining envelopes
- deliver to online recipients with try_send: a recipient that stops reading
its socket no longer pins the sender's task and memory
- throttle call wake-ups per recipient (1 per 10 s, 6 per 10 min) and
ordinary wake-ups (1 per 5 s); collapse ids for APNs and FCM
- POST /v1/register accepts X-Sepot-Identity-Proof and lets a new device take
over an existing identity (restore from the recovery phrase)
- POST /v1/keys uploads one-time prekeys and rotates signed/Kyber prekeys;
PrekeysLow is sent after AUTH when fewer than 20 remain
- GET /v1/keys hands out at most 30 one-time prekeys per address per hour
- reqwest errors are logged without the URL; UnifiedPush POSTs pin the
addresses the SSRF guard resolved; NAT64/6to4 embedded addresses are checked
- absolute AUTH deadline, at most 3 AUTH attempts, length limits on push
endpoints and VoIP tokens
- TURN credentials live 12 h and are reissued per connection every 10 min
- global attachment storage cap (SEPOT_ATTACHMENTS_TOTAL_BYTES), nosniff and
attachment disposition on downloads
- SEPOT_MAX_CONNS_PER_IP, /v1/ready backed by the database, graceful shutdown,
RLIMIT_NOFILE raised to the hard limit, server-info reports source and revision
- Dockerfile: rust 1.99, GIT_SHA build arg for the image label and
SEPOT_REVISION; Tor image on alpine 3.24
Protocol and relay groundwork for typing indicators and 1:1 calls.
- ContentHint.EPHEMERAL: delivered only to an online recipient, never
queued or pushed; the sender gets Receipt.STATUS_DROPPED otherwise.
- ContentHint.CALL: a call offer is queued like a normal message, but an
offline recipient is woken immediately — a PushKit VoIP push on iOS
(priority 10, 45 s expiry) or a short-lived FCM data message on
Android. The relay learns that a device is being called, not by whom.
- RegisterPush.voip_token stores the iOS VoIP token next to the regular
APNs token (migration 0004).
- TurnRequest/TurnCredentials: logged-in devices receive time-limited
coturn credentials (use-auth-secret REST scheme, HMAC-SHA1), so the
TURN server is never an open relay. Configured with SEPOT_TURN_URLS and
SEPOT_TURN_SECRET or SEPOT_TURN_SECRET_FILE; without them calls fall
back to direct connections and STUN.
Rate limiting and the per-IP WebSocket cap trusted X-Forwarded-For and
X-Real-Ip unconditionally. Onion traffic bypasses the reverse proxy, so a
client could set the header itself and slip past every limit. Forwarded
headers are now honoured only from peers inside SEPOT_TRUSTED_PROXIES;
everyone else is keyed by peer address, and onion clients share one bucket.
The queue flush also read the whole backlog into memory at once, and the
certificate authority kept the trust-root private key in the database even
though it is only ever needed offline.
Initial public release. Anonymous on-device identity (no phone number,
e-mail, or password), Signal-protocol cryptography (X3DH + PQXDH, Double
Ratchet, sealed sender) via libsignal, a metadata-minimizing relay,
group chats, disappearing messages, encrypted local storage, optional
Tor transport, and a UI localized into Czech, English, German, Russian,
and Italian.
Monorepo: Flutter client (app/), Rust relay + wire types (rust/),
protobuf definitions (proto/), self-host deploy (deploy/).
Licensed under AGPL-3.0 with an app-store distribution exception (§7).