Recipe: Gallery
Images are blobs, not JSON. Store the bytes in a BlobStore, keep a lightweight reference in a Collection, and let CloudStore move the bytes lazily so a phone doesn’t pull every full-res image at once. Assume the singleton app store around this.
// www/store/app.ts
import { BlobStore } from '@civility/blobs'
import { IDBBlobStorage } from '@civility/blobs/idb'
import { IDBStorage } from '@civility/store/idb'
import { CloudStore } from '@civility/cloud'
const APP_ID = 'me.example.gallery'
type Photo = { caption: string; hash: string; mime: string; size: number }
const blobs = new BlobStore(new IDBBlobStorage({ dbName: `${APP_ID}.blobs` }))
const cloud = new CloudStore(new IDBStorage({ dbName: APP_ID }), {
collections: ['photos'],
versions: [/* … */],
cloud: {
appId: APP_ID,
blobStore: blobs, // blob bodies sync alongside change data
blobPolicy: 'lazy', // metadata now, bytes on demand
},
})
export class App {
photos = cloud.collection<Photo>('photos')
blobs = blobs
cloud = cloud
async addPhoto(file: File, caption: string) {
const hash = await blobs.put(file, file.type) // "sha256:…", dedupes identical bytes
await this.photos.set(crypto.randomUUID(), {
caption,
hash,
mime: file.type,
size: file.size,
})
}
}
Render a thumbnail by resolving the blob to an object URL. cloud.blob(hash) fetches the body from the server on demand if this device doesn’t have it yet:
async function thumbUrl(hash: string): Promise<string> {
const local = await app.blobs.get(hash)
const blob = local ?? await app.cloud.blob(hash) // lazy fetch if missing
return URL.createObjectURL(blob)
}
For <img src> at scale, prefer the withBlobProxy service-worker plugin — plain <img src="/blob/${hash}">, no object-URL bookkeeping. Reclaim space for deleted photos with cloud.compactBlobs().
Next: AI-powered turns free text into structured records with @civility/services.