A CLI tool for Statically Defined PDSes — ship an atproto account & your lexicons with your wisp.place static site.

Serve `$file` blobs from the site, and validate records against their lexicons #2

Merged byjp.me
targeting main from blobs
Merged opened by and assigned to byjp.me
targeting main from blobs

A record can now point at a local file with {"$file": "avatar.png"}. at-sdp publishes it as a real atproto blob that the site serves itself. The first use is a Bluesky profile photo; images in posts come next. Every record is now also checked against its lexicon before anything is written.

Blobs #

  • {"$file": path} becomes a blob ref with a raw sha2-256 CID, a mimeType and a size. It must be the only key in its object, and its path is relative to the JSON file that holds it.
  • mimeType comes from the file's extension. The lookup table is generated from the same mime-types 3.0.2 / mime-db 1.54.0 that wisp-cli uses (hack/mime-table.sh generates pkg/records/mimetypes.go). So wisp serves each blob with the Content-Type its record names.
  • Each blob is copied to xrpc/_sdp/blobs/<cid>.<ext>. com.atproto.sync.getBlob reaches it through one literal cid= 200 rule per blob. com.atproto.sync.listBlobs is always written.
  • check fetches every blob through getBlob and verifies its hash and Content-Type. A blob's HTML 404 is classified the same way as a record's, so the exit-code contract doesn't change.

Validation #

  • Every record and its record key is validated against its lexicon with indigo's atproto/lexicon. That includes a blob's accept and maxSize.
  • Lexicons scanned from the site win over published ones. Any other lexicon is fetched over the network with a 10 s timeout. One that can't be fetched only gives a warning, so offline builds still succeed.
  • Validation runs before anything is written.
  • The outcome doesn't depend on the order indigo walks a record's fields. If any lexicon a record depends on is missing, that record's errors become a warning, never a false failure.

Builds that used to pass can now fail #

  • A record that doesn't fit its lexicon, or a record key that the lexicon's key rejects.
  • A hand-written blob ref. Blobs come only from $file.
  • A $file whose extension isn't in the table, or that is over wisp's 200 MB per-file limit.
  • The same bytes published under two MIME types.
  • A site over wisp's 700 MB supporter cap. build warns above 300 MB.

Worth knowing #

  • $file publishes a file's bytes unchanged, EXIF and GPS metadata included. It also publishes whatever path it names, from anywhere the build can read. The README warns about both.
  • wisp's SDP-k12n fix is now deployed, so getBlob and getRecord should both serve from wisp.place. Once a site with an avatar is live, fetch https://cdn.bsky.app/img/avatar/plain/<did>/<cid>@jpeg once by hand to confirm the whole path works.
  • Parked: if the first reference to a published lexicon names a fragment that lexicon doesn't have, later records of that NSID are built without validation, with a warning. This can only wrongly pass a record, never wrongly fail one.
  • Video is a follow-on (SDP-vf7n).

Reviewing #

The design is in docs/superpowers/specs/2026-10-30-blobs-design.md, with the plan beside it in docs/superpowers/plans/. The commits follow the plan's tasks in order. 3d429c4 fixes review findings on d10756c, and the two could be squashed before merging.

Completes SDP-7eu5 (blobs) and SDP-68bu (lexicon validation).

Commits 12

fix: reject $file over 200 MB, substitute $file beside $strongRef, bound lexicon fetches
fix: validate deterministically, whatever order indigo walks a record
feat: validate records and their keys against their lexicons
feat: check verifies every blob's bytes and Content-Type
feat: fail sites over wisp's 700 MB cap and warn over 300 MB
feat: serve $file blobs through getBlob and listBlobs
fix: catch MIME conflicts within a record and walk records deterministically
feat: turn $file into a blob ref in records
feat: generate a MIME table from the mime-types wisp-cli uses
docs: plan blobs and lexicon validation
docs: design blobs as site files and lexicon validation
chore: note the fix of SDP-k12n