# [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.
STAR v1 - streaming archives and public repository verification ```zig var reader = try zat.star.PublicRepoReader.init(allocator, input, .{}); defer reader.deinit(); try reader.verifyCommit(public_key, expected_did); while (try reader.next()) |record| { // accumulate provisional results; slices borrow the reader } // EOF confirms the reconstructed MST root; publish results now ``` implements the [STAR v1 draft and public-repo profiles](https://tangled.org/microcosm.blue/star/tree/512d0bee6bf49c7c4302d5917a1bb49c8da6fc0e). `star.Reader` validates canonical metadata and reads arbitrary byte pairs; `next()` supplies a key and value length, and `readValue(buffer)` consumes the value in caller-sized chunks. Limits can be lowered for an application. `PublicRepoReader` adds path, order, commit, and root validation, buffering at most one 1 MiB record. Its unsigned-subtree profile verifies integrity without claiming account authenticity. Neither profile establishes freshness. `zig build example-verify-star -- repository.star` checks repository integrity.
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.initWithUserAgent(io, allocator, "myapp/1.0 (+https://example.com)"); 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 Use the getters when you need a few fields or want to decide what to do with each missing value. They do not allocate: strings, arrays, and objects borrow the parsed JSON's storage, so keep it alive while using the results. ```zig const uri = zat.json.getString(value, "embed.external.uri"); const count = zat.json.getInt(value, "meta.count"); const first_alt = zat.json.getString(value, "embed.images.0.alt"); ``` Paths can be string literals or runtime strings. Dots separate object keys, and numeric segments index arrays. A missing path or wrong type returns `null`; use `orelse` to skip, supply a default, or return an application error. When a present value of the wrong type must be rejected separately, use `getPath` and inspect its tag. For a literal key containing dots, use the object's `get` method instead. For a coherent group of fields, `extractAt` decodes into a local struct. Its tuple path selects an object subtree; `.{}` selects the root. JSON is still validated at runtime, and unknown fields are ignored. ```zig const FeedPost = struct { uri: []const u8, cid: []const u8, record: struct { text: []const u8 = "", }, }; var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const post = try zat.json.extractAt(FeedPost, arena.allocator(), value, .{"post"}); // use post before arena.deinit() ``` Unlike the getters, this uses `std.json.parseFromValueLeaky`: decoding ordinary string and slice fields allocates, and allocations belong to the supplied allocator, including allocations made before a parse failure. An arena makes cleanup straightforward. Struct defaults apply to absent fields, not wrong-type values; a missing optional field still needs a default such as `= null`. `extractAtOptional` catches every decoding error and returns `null`, including allocation failures, so use `extractAt` when you need to distinguish errors. If you already know the shape before parsing the response body, decode directly with `std.json.parseFromSlice(T, allocator, body, .{ .ignore_unknown_fields = true })` and `defer parsed.deinit()` rather than building an intermediate `std.json.Value`.
## used by
downstream projects — what's building on zat ### protocol infrastructure | project | what it does | |---|---| | [zds](https://tangled.org/zat.dev/zds) | AT Protocol PDS — repos, blobs, OAuth, passkeys, firehose, permissioned spaces ([live](https://pds.zat.dev/xrpc/_health)) | | [zlay](https://tangled.org/zat.dev/zlay) | AT Protocol relay — crawls every PDS directly, verifies commit signatures, serves the merged event stream ([live](https://zlay.waow.tech/_health)) | | [stream](https://tangled.org/zat.dev/stream) | Jetstream V2 service with the whole network archived behind it — 16.3M repos, sealed segments back to seq 1 ([live](https://stream.waow.tech)) | | [jetstream](https://tangled.org/zat.dev/jetstream) | the zig SDK for Jetstream v2 services — subscribe, archive backfill, failover | ### applications | project | what it does | |---|---| | [weir](https://tangled.org/zzstoatzz.io/weir) | orchestration server whose database *is* an atproto repo — every run and state transition a record under signed commits, exportable as one CAR | | [coral](https://tangled.org/zzstoatzz.io/coral) | named-entity recognition over the firehose ([live](https://coral.waow.tech)) | | [pensieve](https://tangled.org/zzstoatzz.io/pensieve) | public memory search over a repo — semantic artifacts extracted from arbitrary record shapes | | [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)) |
## 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)