omajot

For the nerds

Under the hood

omajot is written in Zig 0.16 and QML. This page explains the parts: the architecture, the CRDT, the hub, the limits, and the one core that runs natively and in the browser.

Architecture

One hub stores and forwards changes. Every device keeps a full copy of all notes. The hub never merges anything: it only moves operations and attachments.

Tailscale connects the devices. tailscale serve terminates HTTPS with a real certificate and adds the caller's identity as a header. The hub listens on loopback only and admits exactly one login.

The CRDT

You can edit the same note on a laptop in a plane and on a phone at the same time. When both come online, omajot merges the edits. It uses a CRDT (conflict-free replicated data type), written from scratch in Zig.

Operations are small JSON objects, for example {"v":1,"k":"ins","r":"…","c":42,"t":…,"n":"…","o":"…","s":"hello"}. The hub stores them without reading them.

Edits and patches

The editor and the engine run in different processes. You can type while a change from another device arrives. Then your edit and the remote patch cross on the pipe.

omajot solves this with two-party operational transformation, in the style of the Jupiter system:

The reference is src/core/ot.zig. The QML plugin and the web app port it line by line and test their ports.

Undo works on your own edits only. The built-in undo of the text field would also undo changes from other devices, so omajot replaces it.

The hub

Everything bounded

The hub runs on baz, a Zig web framework on the bounded/http engine (io_uring on Linux, kqueue on macOS). Both follow one rule: set explicit limits, and reserve memory and threads before the first request. When a limit is reached, the server refuses or waits. It does not grow.

omajot sets its limits as named constants in the source. The build of this site reads them from the code:

LimitValueWhere
Connections24src/hub/hub.zig
Worker threads4src/hub/hub.zig
Waiting event streams64src/hub/hub.zig
Memory budget of the HTTP engine256 MiBsrc/hub/hub.zig
Request body (one batch or one blob chunk)1 MiBsrc/hub/store.zig, blobs.zig
Attachment16 MiB, in chunks of 1 MiBsrc/hub/blobs.zig
Request deadline30 ssrc/hub/hub.zig
Event stream route deadline600 ssrc/hub/hub.zig
Idle keep-alive wait60 ssrc/hub/hub.zig
Event stream ends before the deadline10 ssrc/hub/hub.zig
Heartbeat on idle event streams15 ssrc/hub/hub.zig
Web app files512 files, 128 MiB in totalsrc/hub/static.zig
Log loaded at start4 GiBsrc/hub/store.zig

Why request bodies are 1 MiB

bounded/http reserves the body buffers of all connections when it starts: two buffers of max_body per connection. With 24 connections, 16 MiB bodies would reserve 24 × 2 × 16 MiB = 768 MiB. 1 MiB bodies reserve 48 MiB. So a batch has at most 1 MiB, and a large image goes up in 1 MiB chunks. The hub resumes an interrupted upload at the last good chunk.

Deadlines

In bounded/http, a deadline covers the whole request, also a long event stream. The hub uses three separate limits:

The per-route deadlines and the separate idle timeout came into baz and bounded/http during the work on omajot.

One core, native and WASM

The core in src/core/ holds the CRDT, the note model, the protocol engine, the HTML-to-markdown converter and the QR encoder. It is pure Zig: no std.Io, no clock, no randomness, no global state. The caller passes the allocator and the time. Bytes go in, bytes come out.

This makes one core possible for all devices:

1engine, the same Zig code
199 KiBcore.wasm, ReleaseSmall
85 KiBgzip -9 of core.wasm
3places for one QR encoder
// the whole wasm interface (src/wasm/wasm.zig)
omj_alloc(len) → ptr            omj_free(ptr, len)
omj_engine_new(replica_lo, replica_hi) → handle
omj_call(handle, ptr, len, now_ms) → result     // one request, reply + events
omj_ingest(handle, ptr, len) → result           // operations from the hub
omj_take_new_ops(handle) → result               // operations to send
omj_pending(handle)      omj_engine_free(handle)      omj_free_result(result)

One static binary

omajot is one program: omajot hub, omajot daemon and omajot qr are subcommands of the same binary.

Release targetSize (stripped, ReleaseSafe)
x86_64-linux-musl1.89 MB
aarch64-linux-musl1.58 MB
aarch64-macos1.49 MB
x86_64-macos1.69 MB
x86_64-windows-gnu (experimental)2.18 MB

Measured on 2026-09-25 with Zig 0.16.0, cross-compiled on one x86_64 Linux computer.

Tests

$ zig build test
$ npm test
$ cd web && npm test && npm run e2e