Crate protocol #
The JSON files under root lexicons/network/crate are authoritative. Edit them,
then run just generate. The official @atproto/lex generator produces the
TypeScript models and validators exported by @crate/lexicons. Swift models
live in packages/swift-models. Do not edit generated files.
The Space declaration is an explicit alpha extension unsupported by the record generators. The generation script checks its metadata and collection inventory, then passes its three record schemas unchanged to both generators. There is no public-repository fallback. Generation never publishes records or changes DNS.
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 declares 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 authored 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, just validate-lexicons 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 action output 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.filenameClaimKeyin the web drive module implements this convention; shared golden values test its UTF-8 encoding. 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 web host's request limits 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.
Web adapter #
Astro actions use the authenticated at-astro client to access the personal drive.
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.
Native direct-PDS clients follow the same record and atomic-write conventions.
Verification #
just test checks generated TypeScript validation, application rules, and Swift
JSON round trips using the same directory, blob, version, and filename fixtures.
just check-contracts regenerates TypeScript and Swift into a temporary directory
and compares the file lists and bytes against the checked-in output.
The pinned Swift generator omits record $type encoding and checking. A small
asserted generation fix in scripts/models/swift.ts adds both; nested-version
round trips and invalid-type tests cover it. Remove the fix when upgrading to a
release that handles record discriminators itself. Swift models check size and
length constraints during strict decoding. Their FormatString values preserve
wire strings; check .typed at the application boundary to validate dates, TIDs,
and revision CIDs. Use the library's XRPC serialization for writes: a bare
JSONEncoder does not encode blob CIDs as $link objects.
Crate-specific filename, hierarchy, and transaction rules still belong at each client's application boundary.