zat

zat May 25, 2026
Source

zat

AT Protocol building blocks for zig.

this readme is an ATProto record

view in zat.dev's repository

zat publishes these docs as site.standard.document records, signed by its DID.

install

requires zig 0.16+.

zig fetch --save https://tangled.org/zat.dev/zat/archive/main

then in build.zig:

const zat = b.dependency("zat", .{}).module("zat");
exe.root_module.addImport("zat", zat);

what's here

string primitives - parsing and validation for atproto identifiers
  • Tid - timestamp identifiers (base32-sortable)
  • Did - decentralized identifiers
  • Handle - domain-based handles
  • Nsid - namespaced identifiers (lexicon types)
  • Rkey - record keys
  • AtUri - at:// URIs
const zat = @import("zat");

if (zat.AtUri.parse(uri_string)) |uri| {
    const authority = uri.authority();
    const collection = uri.collection();
    const rkey = uri.rkey();
}
identity resolution - resolve handles and DIDs to documents
// handle → DID
var handle_resolver = zat.HandleResolver.init(io, allocator);
defer handle_resolver.deinit();
const did = try handle_resolver.resolve(zat.Handle.parse("bsky.app").?);
defer allocator.free(did);

// DID → document
var did_resolver = zat.DidResolver.init(io, allocator);
defer did_resolver.deinit();
var doc = try did_resolver.resolve(zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?);
defer doc.deinit();

const pds = doc.pdsEndpoint();       // "https://..."
const key = doc.signingKey();         // verification method

supports did:plc (via plc.directory) and did:web. handle resolution via HTTP well-known and DNS TXT.

CBOR codec - DAG-CBOR encoding and decoding
// decode
const decoded = try zat.cbor.decode(allocator, bytes);
defer decoded.deinit();

// navigate values
const text = decoded.value.getStr("text");
const cid = decoded.value.getCid("data");

// encode (deterministic key ordering)
const encoded = try zat.cbor.encodeAlloc(allocator, value);
defer allocator.free(encoded);

full DAG-CBOR support: maps, arrays, byte strings, text strings, integers, floats, booleans, null, CID tags (tag 42). deterministic encoding with sorted keys for signature verification.

CAR codec - Content Addressable aRchive parsing with CID verification
// parse with SHA-256 CID verification (default)
const parsed = try zat.car.read(allocator, car_bytes);
defer parsed.deinit();

const root_cid = parsed.roots[0];
for (parsed.blocks.items) |block| {
    // block.cid_raw, block.data
}

// skip verification for trusted local data
const fast = try zat.car.readWithOptions(allocator, car_bytes, .{
    .verify_block_hashes = false,
});

enforces size limits (configurable max_size, max_blocks) matching indigo's production defaults.

MST - Merkle Search Tree
var tree = zat.mst.Mst.init(allocator);
defer tree.deinit();

try tree.put(allocator, "app.bsky.feed.post/abc123", value_cid);
const found = tree.get("app.bsky.feed.post/abc123");
try tree.delete(allocator, "app.bsky.feed.post/abc123");

// compute root CID (serialize → hash → CID)
const root = try tree.rootCid(allocator);

the core data structure of an atproto repo. key layer derived from leading zero bits of SHA-256(key), nodes serialized with prefix compression.

crypto - signing, verification, key encoding
// JWT verification
var token = try zat.Jwt.parse(allocator, token_string);
defer token.deinit();
try token.verify(public_key_multibase);

// ECDSA signature verification (P-256 and secp256k1)
try zat.jwt.verifySecp256k1(hash, signature, public_key);
try zat.jwt.verifyP256(hash, signature, public_key);

// multibase/multicodec key parsing
const key_bytes = try zat.multibase.decode(allocator, "zQ3sh...");
defer allocator.free(key_bytes);
const parsed = try zat.multicodec.parsePublicKey(key_bytes);
// parsed.key_type: .secp256k1 or .p256
// parsed.raw: 33-byte compressed public key

ES256 (P-256) and ES256K (secp256k1) with low-S normalization. RFC 6979 deterministic signing. did:key construction and multibase encoding.

repo verification - full AT Protocol trust chain
const result = try zat.verifyRepo(io, allocator, "pfrazee.com");
defer result.deinit();

// result.did, result.signing_key, result.pds_endpoint
// result.record_count, result.block_count
// result.commit_verified (signature check passed)
// result.root_cid_match (MST rebuild matches commit)

given a handle or DID, resolves identity, fetches the repo, parses every CAR block with SHA-256 verification, verifies the commit signature, walks the MST, and rebuilds the tree to verify the root CID.

firehose client - raw CBOR event stream from relay
var client = zat.FirehoseClient.init(io, allocator, .{});
defer client.deinit();

const Handler = struct {
    pub fn onEvent(_: *@This(), event: zat.FirehoseClient.Event) void {
        switch (event.header.type) {
            .commit => {
                // event.body.blocks, event.body.ops, ...
            },
            else => {},
        }
    }
};
var handler: Handler = .{};
try client.subscribe(&handler);

connects to com.atproto.sync.subscribeRepos via WebSocket. decodes binary CBOR frames into typed events. round-robin host rotation with backoff.

jetstream client - typed JSON event stream
var client = zat.JetstreamClient.init(io, allocator, .{
    .wanted_collections = &.{"app.bsky.feed.post"},
});
defer client.deinit();

const Handler = struct {
    pub fn onEvent(_: *@This(), event: zat.JetstreamClient.Event) void {
        if (event.commit) |commit| {
            const record = commit.record;
            // process...
            _ = record;
        }
    }
};
var handler: Handler = .{};
try client.subscribe(&handler);

connects to jetstream (bluesky's JSON event stream). typed events, automatic reconnection with cursor tracking, round-robin across community relays.

xrpc client - call AT Protocol endpoints
var client = zat.XrpcClient.init(io, allocator, "https://bsky.social");
defer client.deinit();

const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?;
var response = try client.query(nsid, params);
defer response.deinit();

if (response.ok()) {
    var json = try response.json();
    defer json.deinit();
    // use json.value
}
json helpers - navigate nested json without verbose if-chains
// runtime paths for one-offs:
const uri = zat.json.getString(value, "embed.external.uri");
const count = zat.json.getInt(value, "meta.count");

// comptime extraction for complex structures:
const FeedPost = struct {
    uri: []const u8,
    cid: []const u8,
    record: struct {
        text: []const u8 = "",
    },
};
const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"});

used by

downstream projects — what's building on zat

firehose consumers (Jetstream)

project what it does
labelz AT Protocol labeler — keyword-matched, secp256k1-signed, served over XRPC. exists to pressure-test zat as an SDK (live)
coral named-entity recognition over the firehose (live)
pollz polls on AT Protocol (live)
typeahead community actor search (live)
find-bufo bsky bot that quote-posts matching bufo images for opt-in followers

CBOR / CAR / MST users

project what it does
zlay AT Protocol relay — direct PDS crawl, signature validation, inline collection index (live)
atproto-bench cross-SDK relay benchmarks (zig vs go vs rust vs python)
ken semantic search over your atproto repo — vector index of records, search by meaning (live)

identity + XRPC users

project what it does
music-atmosphere-feed bsky feed generator for music links — JWT auth for personalized feed
leaflet-search search across leaflet, pckt, offprint, and other standard.site publishers

benchmarks

zat is benchmarked against Go (indigo), Rust (rsky), and Python (atproto) in atproto-bench:

  • decode: 202k frames/sec (zig) vs 39k (rust) vs 15k (go) — with CID hash verification and full CBOR validation
  • sig-verify: 15k–19k verifies/sec across all three — ECDSA is table stakes
  • trust chain: full repo verification in ~300ms compute (zig) vs ~410ms (go) vs ~422ms (rust)

specs

validation follows atproto.com/specs. passes the atproto interop test suite (syntax, crypto, MST vectors).

versioning

pre-1.0 semver:

  • 0.x.0 - new features (backwards compatible)
  • 0.x.y - bug fixes

breaking changes bump the minor version and are documented in commit messages.

license

MIT


devlog · changelog

Discussion in the ATmosphere

Loading comments...