[READ-ONLY] Mirror of https://github.com/chrisvander/crate. The future of cloud storage.
README.md

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:

  1. 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.schema to the published record without changing the document's id or definitions.
  2. Point _lexicon.crate.network DNS TXT to did=<schema-publisher DID>.
  3. 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. filenameClaimKey in 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.