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.