Crate protocol #
Rust is the source of truth for persisted records and the web API contract.
records.rs derives ATProto Lexicons using Jacquard. files.rs, requests.rs, and
session.rs derive Poem request parsing, response serialization, and OpenAPI
schemas. The server's actual handler response enums define status/body pairs.
Generated Lexicons live under root lexicons/network/crate, following the NSID
directory structure. They are local artifacts; generating them does not publish
records, configure DNS, or change a PDS. The space schema is an explicit alpha
extension and must be supported by the PDS. There is no public-repository fallback.
Public declaration discovery #
OAuth consent resolves the Space declaration even when every collection is named
explicitly in the requested scope. The alpha's
Space schema
requires a consent-screen name and a key rule. Crate generates Crate Drive
and literal:self, declaring this Space type's personal self key.
Live discovery requires separately authorized publication following the Lexicon publication specification:
- Choose a stable schema-publisher DID and publish each generated document in
that DID's public repository, collection
com.atproto.lexicon.schema, with its full NSID as the record key. Add$type: com.atproto.lexicon.schemato the published record without changing the document'sidor definitions. - Point
_lexicon.crate.networkDNS TXT todid=<schema-publisher DID>. - Read back DNS and resolve
at://<schema-publisher DID>/com.atproto.lexicon.schema/network.crate.drive, checking its required metadata and all three declared collection schemas.
These are public schema records, not private file records. Schema publication and personal drive authority are different roles; publishing schemas does not grant access to drives. Do not substitute a publisher DID into the personal Space URI or broaden OAuth scopes to bypass discovery. A local schema file alone is not discoverable by a remote PDS.
Before publication, cargo test --locked -p crate-protocol checks local declaration
metadata; dig TXT _lexicon.crate.network is a read-only DNS preflight. Missing or
invalid declarations produce the provider's invalid_scope error. The alpha
provider
caches resolution for five minutes,
so a later authorized publication may not be visible immediately. No generation
or check command in this repository publishes records or changes DNS.
Identity and ownership #
A personal drive is at://{did}/space/network.crate.drive/self. Its authority and
author are the signed-in DID. The space owner controls access. The space must use
personal membership and permit authorized applications to interoperate.
File identity is a stable TID record key, scoped to that drive. A file URI is
{spaceUri}/{authorDid}/network.crate.file/{id}. A record CID is a revision, and a
blob CID identifies bytes. Neither CID is the stable file ID.
parentId points to a stable directory ID in the same drive. It is omitted for
root entries. Directories have isDirectory: true and no blob; files have a modern
typed blob reference and isDirectory: false. The HTTP DTO exposes kind and
derived byte size without duplicating those fields in persisted records.
Names contain no separators, controls, empty/whitespace-only values, . or ...
They are at most 255 Unicode scalar values and 1024 UTF-8 bytes. Names are
case-sensitive. Clients must not normalize or sanitize a submitted name silently.
Atomic changes and conflict detection #
The current Spaces transaction API does not expose swapRecord. Crate uses two
small shared record conventions inside a single com.atproto.space.applyWrites
transaction, relying on its atomic commit and create-if-absent semantics:
network.crate.fileVersion: immutable full old-head snapshot, keyed{fileId}.{oldRecordCid}. A successful head mutation must first create this snapshot in the same transaction. Another mutation from the same revision cannot create the same snapshot, so the whole transaction fails.network.crate.fileName: filename uniqueness claim, keyed by lowercase SHA-256 of the compact UTF-8 JSON array[parentId-or-null,name], without a final newline. Unicode is encoded directly, not ASCII-escaped.name_claim_keyis the Rust reference implementation. A create, rename, or restore claims the destination name in the same transaction; a rename or trash releases the old claim in that transaction.
All interoperable writers must follow these conventions. A raw PDS writer can violate them; the PDS validates records and transaction preconditions, not the Crate application's cross-record invariants. Do not represent this as protection against a malicious authorized client.
Restore reads an immutable snapshot and performs a new guarded head update. It
does not delete or overwrite the snapshot. Trash sets trashedAt; it is not
permanent deletion. No client should garbage-collect version records or their
blobs without an explicit retention design.
The schema permits blobs up to 1 GiB. The backend's configured upload limit and the PDS's own policy may be stricter. Clients must treat upload-limit errors as errors, never truncate bytes. Private blob reads go through the space API.
HTTP adapter #
The optional server owns browser OAuth sessions and proxies operations to the
authenticated personal drive. Native direct-PDS clients do not need that server.
Renaming uses PUT /api/v1/files/{id} with the new name and expected revision.
Moving existing entries is deliberately unsupported: the alpha transaction API
cannot enforce directory-cycle invariants across concurrent moves. Creation and
duplication can target an existing parent; they create a new stable file ID.
Optional response fields are omitted,
never advertised as nullable when the OpenAPI schema cannot guarantee that.
The same Rust contract generates TypeScript via openapi-typescript and Swift via
Apple's swift-openapi-generator. Generated code is checked in, consumed as
source, and checked for drift. This gives compile-time request/response coverage,
not a proof that middleware or an arbitrary external server honors the contract.
Verification #
cargo test -p crate-protocol verifies serialized records against their derived
Lexicons, API serializer parity, mutation input requirements, and record boundary
rules. Root contract checks regenerate every Lexicon, the real server OpenAPI,
and both clients into temporary locations before comparing with checked-in output.
Swift tests compile the generated client and reject malformed response fixtures.