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, amimeTypeand asize. It must be the only key in its object, and its path is relative to the JSON file that holds it.mimeTypecomes from the file's extension. The lookup table is generated from the samemime-types3.0.2 / mime-db 1.54.0 that wisp-cli uses (hack/mime-table.shgeneratespkg/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.getBlobreaches it through one literalcid=200 rule per blob.com.atproto.sync.listBlobsis always written. checkfetches every blob throughgetBloband 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'sacceptandmaxSize. - 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
keyrejects. - A hand-written blob ref. Blobs come only from
$file. - A
$filewhose 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.
buildwarns above 300 MB.
Worth knowing #
$filepublishes 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
getBlobandgetRecordshould both serve from wisp.place. Once a site with an avatar is live, fetchhttps://cdn.bsky.app/img/avatar/plain/<did>/<cid>@jpegonce 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).