Blobs — binary storage
@civility/blobs is content-addressed storage for images and files — the binary counterpart to @civility/store’s JSON, and useful on its own for local-only apps. Pair it with sync and the bytes replicate across devices, lazily by default so a phone never pulls gigabytes it may never view.
put returns a sha256:<hex> key, so identical bytes are stored once however many times they’re added.
Design and lazy-fetch protocol: blobs spec. Full API:
jsr.io/@civility/blobs/doc.
1. Install #
deno add jsr:@civility/blobs
deno add jsr:@civility/blobs/idb
2. Store and read bytes #
import { BlobStore } from '@civility/blobs'
import { IDBBlobStorage } from '@civility/blobs/idb'
const blobs = new BlobStore(new IDBBlobStorage({ dbName: 'my-app-blobs' }))
const key = await blobs.put(file, file.type) // "sha256:…" — content-addressed, dedupes
await blobs.put('avatar', bytes, 'image/png') // or a named key
const blob = await blobs.get(key) // Blob | null
blobs.put(data, mime?): Promise<string> // content-addressed
blobs.put(key, data, mime?): Promise<string> // named
blobs.get(key): Promise<Blob | null>
blobs.has(key) / blobs.delete(key) / blobs.list()
blobs.getMissing(keys): Promise<string[]> // which keys aren't present
blobs.getMeta(key): Promise<BlobMeta | null>
blobs.compact({ referenced, graceDays?, now? }): Promise<CompactResult>
Backends mirror the store: @civility/blobs/idb (IDBBlobStorage), /opfs, /memory, /deno-fs.
3. The store + blobs pattern #
Keep a lightweight reference in a Collection and the bytes in the BlobStore:
type Photo = { caption: string; hash: string; mime: string; size: number }
async function addPhoto(file: File, caption: string) {
const hash = await blobs.put(file, file.type)
await app.photos.set(crypto.randomUUID(), {
caption,
hash,
mime: file.type,
size: file.size,
})
}
Any object carrying hash (a sha256: key), mime, and size is recognized as a blob reference wherever it appears in a document, so sync knows which bodies to move and compact() knows which are still live — the Photo above qualifies as it stands. If you instead store a typed payload as a JSON string, markBlob(body) gives you the pair: { body, blobRef }, the stringified payload plus a canonical ref to keep discoverable alongside it.
4. Syncing blobs #
Pass the BlobStore to a CloudStore and blob bodies move alongside your change data. With blobPolicy: 'lazy', metadata syncs immediately but bodies download on demand:
const cloud = new CloudStore(backend, {
collections: ['photos'],
versions: [/* … */],
cloud: {
appId: APP_ID,
blobStore: blobs,
blobPolicy: 'lazy', // metadata now, bytes on demand
},
})
const body = await cloud.blob(hash) // fetch a lazy body from the server if missing
await cloud.compactBlobs() // GC blobs no live reference points at
See Sync → Blobs for the full lifecycle.
5. Serving blobs to <img>: withBlobProxy #
The withBlobProxy service-worker plugin serves /<pathPrefix>/<hash> straight from the local blob store, so templates use a plain <img src> with no object-URL bookkeeping:
// www/worker.ts
import {
init,
withBlobProxy,
withFetchStrategy,
withPrecache,
} from '@civility/workers'
init([
withPrecache([/* … */]),
withBlobProxy({ dbName: 'my-app-blobs', pathPrefix: 'blob' }),
withFetchStrategy(),
])
// pair on the client so lazy bodies can be fetched through the proxy
client.configureBlobProxy({ serverUrl, token })
<img src="/blob/${photo.hash}" alt="${photo.caption}">
Without the proxy, resolve to an object URL manually — see the gallery recipe.
Next #
- Gallery recipe — an image gallery end-to-end.
- Sync — replicate blobs (and the rest of your store) across devices.