~/potatohd.ruRU
← All posts
Jul 14, 2026·9 min read

Opengram: a Telegram-compatible messenger built from scratch, 355 of 781 methods deep

GoMTProtoE2EEScyllaDB

781 callable RPC methods is the entire real Telegram protocol, counted the same way the code itself counts it: by reflecting on tg.Client in gotd/td, an existing open-source Go client library. The library already enumerates every method a real client can call; counting them was the easy part. Opengram is a self-hosted messenger that speaks that protocol for real, built from scratch in Go — ScyllaDB for message storage, a separate sequencer service alongside it, and a VLESS+Reality proxy sitting in front of the whole stack for censorship circumvention.

Every one of the 781 methods lives in a manifest, in one of three states: implemented, intentionally_unsupported, planned. Nothing moves between states without a real test behind it, and that discipline is what keeps the completion number honest. A number nobody has to prove would just get typed into a README and left to rot. 355 are implemented so far, each one backed by a real Postgres handler and a real end-to-end test against live infrastructure. The iOS, Android, and web SDKs are already part of the codebase; the client apps meant to sit on top of them are still empty scaffolding.

#The manifest

Every state in the manifest needs backing. implemented doesn't get accepted without a citation to the specific end-to-end test that proves it; intentionally_unsupported doesn't get accepted without a stated reason. Without that discipline, a manifest is just a text file that can be told to say anything — flip one line, no evidence required. With it, every line becomes a checkable claim: open the right file and there's either a test or a reason sitting behind it. A companion CLI regenerates the manifest and validates it on every build, failing the build outright if the manifest goes stale or incomplete.

Discipline doesn't protect against one specific failure mode: someone simply forgetting to update a status after the code underneath it changes. A sweep that cross-referenced the manifest against the real request-dispatch code — the part that actually decides which handler serves which call — found 188 methods with working handlers. Every one of them was still marked planned in the manifest. The usual reason for that kind of drift, in any file maintained by hand and separately from the code: a handler gets wired into the dispatcher and the work moves on, and the manifest line meant to track it never gets touched. The lesson that came out of it was simple — check the dispatch code first, the manifest second, because the manifest is the one that tends to lag behind what's actually built.

Opengram lives in its own git repository. At one point, a routine manifest regeneration turned up a higher method count than expected. A separate, parallel process was working on that same manifest file at the same time, landing fixes to unrelated things — an admin login flow, a proxy protocol, a deploy config — and closing out 27 more methods along the way, methods that weren't even part of that session's plan. Nothing got lost: the tool re-reads the manifest fresh off disk on every run and layers new entries on top of what's already there.

#Encryption

1:1 chats get real encryption: X3DH for key agreement, then the Double Ratchet, both backed by real tests. Original MTProto also has an older path, secret chats: a separate Diffie-Hellman-only scheme, around nine RPC methods. All nine are marked intentionally_unsupported in Opengram's manifest, with a reason recorded right there in the file. 1:1 chats already have working modern encryption; keeping a second, weaker scheme running in parallel would mean writing, testing, and guarding a whole separate cryptographic path against regressions, for a capability that already exists somewhere better. It's added attack surface with nothing on the other side of the ledger.

But the Double Ratchet implementation itself — the one meant to settle the question of 1:1 encryption for good — has an open problem. One of its tests is a round trip on a bidirectional ratchet: encrypt a message through a full back-and-forth cycle and check that it still comes out intact and verifiable on the other end. That test fails. Not intermittently. Deterministically, with a message-authentication error, reproduced the same way across multiple runs in a row. The determinism is the one piece of good news here: a flaky failure could be waved off as a timeout or a race condition in the test environment. A deterministic one can't be explained away that easily — somewhere in the protocol logic or its implementation, there's a real mismatch between what one side of the conversation encrypted and what the other side is able to decrypt and authenticate. It's a real bug, it lives in the cryptography layer, and it's flagged as needing focused review on its own — not the kind of failure where rerunning the test and hoping is a real option.

#Channels and groups

The message-send path in Opengram only ever resolves one direction: a one-to-one conversation. Code that would resolve a channel or a group as a message target doesn't exist yet — not a missing branch in a conditional, not a stub that returns an error, there's simply no route there at all.

From the outside, channel membership and actually sending a message inside one look like the same feature; both hide behind the phrase "channel works." From the inside they're two different pieces of work with two different fates. Membership — creating a channel or group, joining, leaving, inviting, banning, adjusting settings — is real and fully working. Sending an actual message inside one is not there yet. In practice: a channel can be created, people can be invited into it, a troublemaker can be thrown out of it, but writing a message that reaches everyone in it has nowhere to go, because the route for that kind of recipient hasn't been built.

Ban enforcement at the membership layer is real: it removes the member from the chat, and checks again, right there in the join-channel path, that a banned user can't walk back in. Skip that second check and a ban would only be a row in a database — accurate, and useless the moment the same person knocks on the channel door again, if nothing cross-references that row. The system also correctly reports a middle state: muted, but still present in the chat, two distinct facts that are easy to collapse into one if they're serialized carelessly.

The first test pass caught a real bug here: one of the membership lookup paths never checked the ban table at all, silently hiding an active restriction. A user reading that response would look like an ordinary, unrestricted member of the chat, even though every rule said they should still be banned. It's a quiet kind of bug — it doesn't crash, doesn't throw, doesn't draw attention to itself, it just answers with the wrong data. Catching it takes a test that asks specifically about a known banned user's status. A generic check that the request merely came back without an error would miss the swap entirely.

#Where things stand

The REST API — the embedded admin panel, public registration, that whole surface — gets its own accounting, and the bar there is even higher than the MTProto manifest's. 41 operations out of 41. Checked by a test that walks the real production router and cross-references it three independent ways: against the OpenAPI spec, against a checked-in inventory file, and by directly parsing every handler's source to catch any placeholder marker. Three methods exist because each one catches a different failure mode: the spec can drift from the code, the inventory file can go stale the same way the MTProto manifest did, and a handler can be honestly wired into the router while still being a stub underneath that does nothing real.

A chunk of the catalog-style methods — default emoji statuses, sponsored-message placeholders, various platform feature lists — answer with a permanent empty response. Telegram's own protocol has no matching write method for any of them: no Telegram client anywhere, including the official one, can send a request that would fill those fields with anything but emptiness. An empty response from Opengram is the complete implementation here, in full — the same answer a client would get back from the real Telegram, asked the same question.

Only the backend and the SDKs are real right now — iOS, Android, and web, already embedded in the codebase. The client apps meant to sit on top of them still need to be written.

// available for hire

Need a website or setup, done right?