# [zat](https://zat.dev)
AT Protocol building blocks for zig.
this readme is an ATProto record
> [view in zat.dev's repository](https://at-me.zzstoatzz.io/view?handle=zat.dev)
zat publishes these docs as [`site.standard.document`](https://standard.site) records, signed by its DID.
## install
requires zig 0.16+.
```bash
zig fetch --save https://tangled.org/zat.dev/zat/archive/main
```
then in `build.zig`:
```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
```zig
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
```zig
// 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
```zig
// 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
```zig
// 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
```zig
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
```zig
// 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.
OAuth client helpers - ATProto OAuth ceremony without app storage policy
```zig
var transport = zat.HttpTransport.init(io, allocator);
defer transport.deinit();
const authserver = try zat.oauth.discoverAuthorizationServer(allocator, &transport, pds_url);
defer allocator.free(authserver);
var metadata = try zat.oauth.fetchAuthorizationServerMetadata(allocator, &transport, authserver);
defer metadata.deinit(allocator);
var secrets = try zat.oauth.prepareAuthRequestSecrets(allocator, io);
defer secrets.deinit(allocator);
var par = try zat.oauth.sendParRequest(allocator, io, &transport, .{
.par_url = metadata.pushed_authorization_request_endpoint,
.authserver_issuer = metadata.issuer,
.client_id = client_id,
.redirect_uri = redirect_uri,
.scope = "atproto repo:example.app.record",
.state = secrets.state,
.pkce_challenge = secrets.pkce_challenge,
.login_hint = handle,
.client_keypair = &client_keypair,
.dpop_keypair = &secrets.dpop_keypair,
});
defer par.deinit(allocator);
```
also includes client metadata JSON generation, authorization URL formatting, code/refresh token exchange, DPoP nonce retry, and DPoP-authenticated resource requests. cookies, sessions, redirects, and persistence stay with your application or web framework.
commit verification - check a repo CAR against the account's signing key
```zig
// given CAR bytes and the account's public key (see identity resolution)
const result = try zat.verifyCommitCar(allocator, car_bytes, public_key, .{
.expected_did = did,
.require_complete_repo = true, // reject CARs with missing MST/record blocks
});
// result.commit_did, result.commit_rev, result.commit_cid, result.record_count
```
atproto repos are self-authenticating, so anything that accepts one from the network verifies it before trusting it: checks the commit signature, confirms the MST root matches the commit's `data` CID, and validates every block hash. `verifyCommitDiff` does the same for the partial CARs on firehose `#commit` events. this is how [zds](https://tangled.org/zat.dev/zds) guards `com.atproto.repo.importRepo` — an uploaded CAR is verified against the account's DID document before the PDS accepts it.
`signCommit` is the produce side: it builds and signs the canonical commit block, and round-trips through `verifyCommitCar` in the test suite.
firehose client - raw CBOR event stream from relay
```zig
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
```zig
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
```zig
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
```zig
// 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](https://tangled.org/zzstoatzz.io/labelz) | AT Protocol labeler — keyword-matched, secp256k1-signed, served over XRPC. exists to pressure-test zat as an SDK ([live](https://labelz.fly.dev/health)) |
| [coral](https://tangled.org/zzstoatzz.io/coral) | named-entity recognition over the firehose ([live](https://coral.waow.tech)) |
| [pollz](https://tangled.org/zzstoatzz.io/pollz) | polls on AT Protocol ([live](https://pollz.waow.tech)) |
| [typeahead](https://tangled.org/zzstoatzz.io/typeahead) | community actor search ([live](https://typeahead.waow.tech)) |
| [find-bufo](https://tangled.org/zzstoatzz.io/find-bufo) | bsky bot that quote-posts matching bufo images for opt-in followers |
### CBOR / CAR / MST users
| project | what it does |
|---|---|
| [zlay](https://tangled.org/zzstoatzz.io/zlay) | AT Protocol relay — direct PDS crawl, signature validation, inline collection index ([live](https://zlay.waow.tech/_health)) |
| [atproto-bench](https://tangled.org/zzstoatzz.io/atproto-bench) | cross-SDK relay benchmarks (zig vs go vs rust vs python) |
| [ken](https://tangled.org/zzstoatzz.io/ken) | semantic search over your atproto repo — vector index of records, search by meaning ([live](https://ken.waow.tech)) |
### identity + XRPC users
| project | what it does |
|---|---|
| [music-atmosphere-feed](https://tangled.org/zzstoatzz.io/music-atmosphere-feed) | bsky feed generator for music links — JWT auth for personalized feed |
| [leaflet-search](https://tangled.org/zzstoatzz.io/leaflet-search) | search across leaflet, pckt, offprint, and other standard.site publishers |
## benchmarks
zat is benchmarked against the Go (indigo, atmos), Rust (rsky, shrike, jacquard), Python, and Elixir ecosystems in [atproto-bench](https://tangled.org/zzstoatzz.io/atproto-bench):
- **firehose decode** (CID-verified): 226k frames/sec (zat) vs 62k (atmos) vs 53k (rsky) vs 20k (indigo)
- **MST insert + root**: 5.7M records/sec, producing the same root bytes as atmos and shrike
- **signature verify**: 21.6k verifies/sec — ahead of Go (12k–19k), behind Rust (~27k); ECDSA dominates every SDK's pipeline
- **end-to-end repo verification** (handle → signature-checked repo): ~300ms compute (zat) vs ~410ms (indigo) vs ~422ms (rsky crates)
## specs
validation follows [atproto.com/specs](https://atproto.com/specs/atp). passes the [atproto interop test suite](https://github.com/bluesky-social/atproto-interop-tests) (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](https://tangled.org/zat.dev/zat/blob/main/LICENSE)
---
[devlog](devlog/) · [changelog](CHANGELOG.md)