Hi all ![]()
I’ve been working on mattermost-rs, an incremental port of the Mattermost server from Go to Rust. It’s a hobby project with no connection to Mattermost, Inc. I’m sharing it because people here might find the approach interesting, and I’d like feedback from anyone who knows the server’s internals.
How it works
A Rust server (mm-api) sits in front of the normal Go server, and both share one Postgres database. Rust answers the routes it has ported and forwards
everything else to Go unchanged. That lets the port move one route at a time, and at every step the stock webapp and mmctl keep working without knowing
which server answered.
browser ──► mm-api (Rust) ──(unported routes, static files)──► Go server ──► Postgres
└──────────── ported routes, the websocket ──────────────────────┘
Rust logs in using session rows that the Go server wrote. The hard requirement is matching Go’s output exactly: same JSON field names, same null-vs-omitted
choices, same number types. It is tested byte for byte against Go.
Where it stands
- 745 of 764 api4 route+method pairs have a Rust handler. That count includes the local-mode (mmctl --local) socket API.
- The websocket hub is ported, including the rules for which events go to which connection.
- There is an opt-in plugin host: existing Go plugins run as child processes and talk to Rust over go-plugin’s net/rpc and encoding/gob, both reimplemented
in Rust. - In a scripted browser session (login, posting, editing, reactions, threads, search, a DM, an upload, profile changes), 55% of requests were answered by
Rust. Most of the rest were static files and HTML pages, which Go still serves. - Enterprise features whose code isn’t in the public repo (SAML, LDAP, clustering, message export) forward to Go permanently. The public logic around them,
such as the licence and settings checks, is ported.
How it’s tested
Reading Go code and reasoning about it produces confident, wrong translations. So every function with branching logic is run through a set of test inputs
in Go, and the Rust tests check against Go’s recorded answers. Every test suite is also mutation-tested: the harness deliberately breaks the code in small
ways to check that the tests notice. Some examples of what this caught:
- Go’s strings.ToLower is not Rust’s to_lowercase. They disagree on İ and on a trailing sigma, which affects emails, usernames and team names.
- filepath.Ext(“.hidden”) and Rust’s Path::extension disagree.
- A few upstream quirks are kept on purpose (for example, SetChannelMuted ignores its argument and toggles instead). Fixing them would make two servers
that write the same rows disagree.
Try it: GitHub - niklabh/mattermost-rs: Rust port of mattermost · GitHub
scripts/demo.sh up # builds, starts Postgres + Go + mm-api, seeds users, prints the URL
Caveats
This isn’t production software. There are no users and no deployments, and it’s pinned to one upstream commit. Some code was written with Claude Code, with the Go test oracle and the mutation tests as the check on correctness.
I’d especially like to hear from anyone who knows about areas where clients depend on behaviour that isn’t documented. Issues and PRs are welcome