Ansible is not a fork and not somebody else's server rebuilt. Compatibility at the protocol-family level is there so existing client libraries work, but the server, the transport, the storage and the whole product side are written from scratch. Below is what differs from the thing we resemble from the outside.
The transport and crypto layer is a separate OTP application
(bh-mvsy): TL codecs, handshake, sessions, encryption. The business
server (bh-backend) depends on it and knows nothing about the wire.
The split is not cosmetic — it is what lets the transport change without touching
~330 RPC methods.
Encryption is AES-IGE over a session key from a Diffie — Hellman handshake.
Verified live: a marker placed into the plaintext at L0 is not found on the wire,
neither raw nor after de-obfuscation. What does leak is metadata:
auth_key_id, packet lengths, timings. That is a property of any
delivery network, and we prefer to say so plainly.
A client can connect four ways, and all four converge on one handler:
Obfuscation does not give privacy — encryption does. It removes the signal by which a connection can be filtered out as "this is a messenger".
One data centre, three Kubernetes masters behind a shared VIP. Multi-DC is
present in the routing (bh_config_dcs, authorisation export/import,
storage routing), but production is one region so far.
An expensive lesson: with PostgreSQL, Redis and MinIO placed away from the backend geographically, the floor of any operation was ~11 ms of network RTT — not the code. No query optimisation buys that back. The only cure is co-location, which is what we did.
The original route /progressive/photo<id>?h=<access_hash>
works, but a CDN cannot be put in front of it, for three independent reasons:
content-type on its own.Hence /file/<token>.<ext>. The token is 20 bytes:
epoch, kind, file_id and an HMAC-SHA256 truncated to 80
bits. A plain GET gets 200 and the whole body; Range still works.
The token is deterministic, with no timestamp — and that is not an
oversight. A cache only helps if a file has one URL forever; an expiry inside the
token would mint a new address on every render and the hit rate would be zero.
Mass revocation is the epoch byte in the config.
my.ansible.su issues
api_id/api_hash. Login is a code inside the
messenger, not over SMS: the portal can issue credentials and delete an
account, so holding the number is not enough — the account has to be reachable.
Before this, api_id was a value the server only wrote into
encoded authorisations and never checked, which meant there was nothing to switch
an abusive client off with. The check now sits at the single RPC entry point —
all three transports converge there, so a refusal needs no edits in each.
Separately: an unknown api_id is allowed on purpose. The
registry only holds applications the portal issued, and first-wave clients carry
ids that predate it; "unknown means refuse" would have thrown everyone off the
network the moment it shipped. Only an explicit ban refuses.
A link to a group, a channel or a username opened in an ordinary browser used to land in the site's SPA, which had no such route, and the URL was silently replaced with the homepage — the link lost what it pointed at. It is now answered by a separate service with server-side rendering: OG tags in the first response, and a jump into the app before any framework loads.
The resolver is on the backend, and it answers identically to "does not exist", "revoked" and "expired". Distinguishing them would hand out an oracle for enumerating invite hashes.
The wallet is a separate service with its own database; the messenger owns collectible objects (gifts, real phone numbers and usernames), the wallet owns the balance. They talk server to server. Atomicity of a P2P-market purchase comes from a single CTE rather than a sequence of queries: otherwise there is a window between the debit and the transfer.
End-to-end, with the key derived on the devices. The server is a blind relay: it passes bytes without holding the key and without taking part in the Diffie — Hellman exchange. The limitation follows from that: a secret chat's history cannot be restored on a new device, because the server has nothing to restore it from.
The TL-schema is covered to 98.2% of what is published. What remains are methods no subsystem of ours corresponds to (some passport, some advertising). Every implemented method is checked by a pair of end-to-end tests, "our client vs a reference client" — byte-for-byte equality, not "seems to work".
The section without which the rest would read like advertising.
WHERE username = $1 while a DNS name is lowercased, so
@Support and @support can exist at once. The fix is
a unique index on lower(username); it needs a migration.