Encrypted, cache-first object storage with a persistent write queue for browser applications. Objects are available locally immediately; the application owns the worker that synchronizes queued changes to their HTTP(S) URLs.
npm install @sovereignbase/storage
# or
pnpm add @sovereignbase/storage
# or
yarn add @sovereignbase/storage
# or
bun add @sovereignbase/storage
# or
deno add jsr:@sovereignbase/storage
# or
vlt install jsr:@sovereignbase/storagestoreObject and deleteObject commit locally and append an operation to
WriteQueue. Register a callback that drains the queue and performs your remote
writes. Call finalize() only after the server accepts an operation, so a failed
write remains available for retry.
import { WriteQueue } from '@sovereignbase/storage'
let syncing = false
async function syncWrites() {
if (syncing) return
syncing = true
try {
let queued = await WriteQueue.dequeue()
while (queued) {
const { operation, finalize } = queued
if (operation.kind === 'store') {
const cached = await caches.match(operation.url)
if (!cached) throw new Error(`Missing cached object: ${operation.url}`)
const response = await fetch(operation.url, {
method: 'PUT',
headers: { 'content-type': 'application/octet-stream' },
body: await cached.arrayBuffer(),
})
if (!response.ok) throw new Error(`Store failed: ${response.status}`)
} else {
const response = await fetch(operation.url, { method: 'DELETE' })
if (!response.ok) throw new Error(`Delete failed: ${response.status}`)
}
await finalize()
queued = await WriteQueue.dequeue()
}
} finally {
syncing = false
}
}
WriteQueue.onQueued = () => void syncWrites()
void syncWrites() // resume operations left over from an earlier sessionQueue operations contain URLs, not secret keys or plaintext values. A store consumer obtains the already encrypted bytes from the Cache API.
import { storeObject } from '@sovereignbase/storage'
import { Cryptographic } from '@sovereignbase/cryptosuite'
const url = 'https://objects.example/profile'
const cipherKey = await Cryptographic.cipherMessage.generateKey()
await storeObject(url, { name: 'Ada' }, cipherKey)The returned promise settles after the encrypted cache entry and its persistent queue operation have both been written.
import { loadObject } from '@sovereignbase/storage'
const objectPromise = loadObject(url, cipherKey)
// Build the UI while loading is already in progress.
const output = document.createElement('output')
output.textContent = 'Loading…'
document.body.append(output)
const object = await objectPromise
if (object) output.textContent = object.nameloadObject reads from the cache first and fetches url on a miss. Successful
reads refresh the cache in the background. Start the load as early as useful and
await its promise only when the value is needed. A non-successful HTTP response
resolves to undefined.
Pass true as the third argument to avoid network access. A cache miss then
resolves to undefined:
const cachedObject = await loadObject(url, cipherKey, true)import { deleteObject } from '@sovereignbase/storage'
await deleteObject(url)Deletion removes the local cache entry and persists a delete operation for the sync worker.
storeObject and loadObject accept a cryptosuite CipherKey JWK. Keep the key
secret and persist it separately from the encrypted object. Loading requires the
same key that was used for storage. Generate a random key with
Cryptographic.cipherMessage.generateKey() or derive one with
Cryptographic.cipherMessage.deriveKey(sourceKeyMaterial, salt).
- Browser-only ESM.
- Requires the Cache API, IndexedDB, Fetch, Web Crypto, and
Blob. - Accepts an absolute
http://orhttps://URL as each object's identity and remote location. - Uses MessagePack, gzip compression, 1 KiB length-hiding padding, and
cryptosuite encryption. Objects whose compressed representation exceeds 24
MiB are rejected with error code
MAX_OBJECT_SIZE_EXCEEDED; malformed decrypted padding is rejected withINVALID_PADDING. - Cache hits take precedence over the network. Successful reads refresh
Cache-Control,Date, andExpireswith a 90-day freshness hint. - Cache-only loads never make a network request.
- Remote responses need to allow the browser origin when used cross-origin.
- Write operations are stored in IndexedDB in FIFO order and remain queued until
their
finalize()function succeeds. - IndexedDB failures reject with an operation-specific
StorageError; the browser's original error is available through itscauseproperty. - Dependencies are not bundled.
- Unit and integration tests run in Vitest with TypeScript.
- Browser E2E tests run in Playwright on Chromium, Firefox, WebKit, Pixel 7 mobile Chromium, mobile Firefox emulation, and iPhone 15 mobile WebKit.
- Statement, branch, function, and line coverage are all held at 100%.
Apache-2.0