Overview
Object storage substrate for AbsoluteJS. One BlobStore interface, multiple adapters, and bounded streaming for large artifacts.
@absolutejs/blobv0.5.1betaData & SyncOne BlobStore interface over local disk and every S3-compatible service, without a hard AWS SDK dependency.
Object storage substrate for the AbsoluteJS PaaS: one BlobStore interface (put, get, getStream, head, delete, list, presign) with adapters for local disk and any S3-compatible service. The S3 adapter takes a narrow S3ClientLike shape instead of depending on the AWS SDK directly, so the SDK stays out of your dependency tree until you need it. Other AbsoluteJS packages ride this substrate for uploads, replay artifacts, and media storage.
bun add @absolutejs/blobObject storage substrate for AbsoluteJS. One BlobStore interface, multiple adapters, and bounded streaming for large artifacts.
Subpath — Backs
@absolutejs/blob/local — Filesystem (dev / single-host prod / tests)
@absolutejs/blob/s3 — AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, Tigris — any S3-compatible HTTP API
@absolutejs/blob/aws-s3 — Official AWS SDK wiring, including multipart streaming uploads
@absolutejs/blob/uploadthing — UploadThing server SDK with stable application-owned custom ids and signed reads
@absolutejs/blob/inspection — Provider-neutral inspection contract, bounded stored-object inspection, and ClamAV clamd adapter
Both implement the same BlobStore interface — swap providers with one constructor change.
Uploads that can contain customer-controlled bytes should remain under a private quarantine key until an inspector returns clean. The inspection subpath preserves the same workflow across local, UploadThing, S3, R2, and Spaces storage:
The clamd adapter uses the bounded INSTREAM protocol and returns clean, infected, or unavailable. Scanner errors never become a clean verdict. The host owns durable quarantine, retry, retention, and audit policy; Blob only owns byte transport and normalized inspection.
Files at /. Metadata (contentType, user metadata, cache headers) at /.meta.json. Atomic writes via temp file + rename. presign() throws BlobError('UNSUPPORTED') — use the S3 adapter against a local MinIO if you need presign in dev.
awsS3BlobStore uses the official SDK command clients and @aws-sdk/lib-storage multipart uploads. Streams are never materialized as one control-plane buffer. The lower-level s3BlobStore and S3ClientLike remain available for custom clients.
Cloudflare R2
R2 is fully S3-compatible — the only thing that changes is the endpoint. Same wiring + s3BlobStore adapter.
Backblaze B2, MinIO, Wasabi, Tigris
All the same pattern. Point endpoint at the provider's URL, provide credentials, hand the client into s3BlobStore.
put returns the stored object's metadata (size, contentType,
etag, user metadata).
get returns null for missing keys (not throw).
getStream for large blobs — avoids loading the body into memory.
delete is idempotent: deleting a missing key is success.
list paginates via cursor — pass back into the next call as
options.cursor.
presign builds a time-limited URL for direct browser upload/
download. operation: 'put' for uploads, 'get' (default) for downloads. Throws BlobError('UNSUPPORTED') on local.
Adapters call validateKey() on every operation. Leading slashes, NUL bytes, and . / .. path segments throw BlobError('INVALID_KEY') — closes the path-traversal class of bugs at the substrate level.
put, get, getStream, head, delete, list, and presign behave identically across adapters, so provider choice is a deploy-time detail.
AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi, and Tigris all work through the same s3BlobStore adapter — only the endpoint changes.
The adapter accepts a narrow S3ClientLike object, keeping @aws-sdk/client-s3 out of your dependency tree as a hard dependency.
presign builds time-limited URLs for direct browser upload (operation: put) or download (operation: get) without proxying bytes through your server.
Every operation runs validateKey, rejecting leading slashes, NUL bytes, and . or .. path segments with BlobError(INVALID_KEY) — path traversal is closed at the substrate level.
getStream serves large blobs without loading them into memory, and list paginates with a cursor you pass back into the next call.
Keep customer-controlled uploads private until bounded inspection returns clean. The ClamAV adapter distinguishes clean, infected, and unavailable; scanner errors never become a clean verdict.
Choose an adapter by deployment and upload requirements without changing the BlobStore contract used by application code.
| Option | Local | Generic S3 | AWS SDK | UploadThing |
|---|---|---|---|---|
| Streaming reads | Yes | Yes | Yes | Yes |
| Presigned transfer | No | Yes | Yes | Signed reads |
| Multipart upload | No | Manual | Built in | Provider managed |
| Best fit | Dev/single host | S3-compatible clouds | AWS production | Managed uploads |
Treat uploaded bytes as untrusted until bounded inspection succeeds.
Outcomes
Object storage substrate for AbsoluteJS. One BlobStore interface, multiple adapters, and bounded streaming for large artifacts.
Subpath — Backs
Uploads that can contain customer-controlled bytes should remain under a private quarantine key until an inspector returns clean. The inspection subpath preserves the same workflow across local, UploadThing, S3, R2, and Spaces storage:
Hardening checklist
Follow in order
# @absolutejs/blob
const store: BlobStore = /* localBlobStore(...) | s3BlobStore(...) */;
await store.put('users/42/avatar.png', body, { contentType: 'image/png' });
const bytes = await store.get('users/42/avatar.png');
const url = await store.presign('users/42/avatar.png', { ttlSeconds: 900 });Uploads that can contain customer-controlled bytes should remain under a private quarantine key until an inspector returns clean. The inspection subpath preserves the same workflow across local, UploadThing, S3, R2, and Spaces storage:
import {
createClamdBlobInspector,
inspectStoredBlob,
} from "@absolutejs/blob/inspection";
const inspector = createClamdBlobInspector({ host: "clamav.internal" });
const result = await inspectStoredBlob(blobs, inspector, {
filename: "evidence.pdf",
key: "quarantine/case/evidence.pdf",
maxBytes: 25 * 1024 * 1024,
});Working example for Local.
import { localBlobStore } from "@absolutejs/blob/local";
const blobs = localBlobStore({ root: "./var/blobs" });
await blobs.put("uploads/file.pdf", body);The local adapter stores files on disk — ideal for development, tests, and single-host deployments. get returns null for missing keys instead of throwing, and delete is idempotent.
import { localBlobStore } from '@absolutejs/blob/local';
const blobs = localBlobStore({ root: './var/blobs' });
await blobs.put('users/42/avatar.png', body, {
contentType: 'image/png'
});
const bytes = await blobs.get('users/42/avatar.png'); // null if missing
const meta = await blobs.head('users/42/avatar.png');
await blobs.delete('users/42/avatar.png'); // idempotentThe S3 adapter works with any S3-compatible service. You hand it an S3ClientLike object wired onto the real AWS SDK client, so the SDK is your dependency, not the substrate’s.
import {
S3Client,
PutObjectCommand,
GetObjectCommand,
HeadObjectCommand,
DeleteObjectCommand,
ListObjectsV2Command
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { s3BlobStore } from '@absolutejs/blob/s3';
// Cloudflare R2 — swap the endpoint for B2, MinIO, Wasabi, Tigris
const aws = new S3Client({
credentials: { accessKeyId, secretAccessKey },
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
region: 'auto'
});
// Wire the narrow S3ClientLike shape onto the real SDK client
const blobs = s3BlobStore({
bucket: 'my-bucket',
client: {
deleteObject: (input) => aws.send(new DeleteObjectCommand(input)),
getObject: (input) => aws.send(new GetObjectCommand(input)),
headObject: (input) => aws.send(new HeadObjectCommand(input)),
listObjectsV2: (input) => aws.send(new ListObjectsV2Command(input)),
presignGetObject: (input, options) =>
getSignedUrl(aws, new GetObjectCommand(input), options),
presignPutObject: (input, options) =>
getSignedUrl(aws, new PutObjectCommand(input), options),
putObject: (input) => aws.send(new PutObjectCommand(input))
}
});
// Time-limited URL for direct browser upload
const url = await blobs.presign('uploads/file.pdf', {
operation: 'put',
ttlSeconds: 900
});Inspect a quarantined object with strict byte bounds before promoting it into application-visible storage.
import {
createClamdBlobInspector,
inspectStoredBlob
} from '@absolutejs/blob/inspection';
const inspector = createClamdBlobInspector({ host: 'clamav.internal' });
const result = await inspectStoredBlob(blobs, inspector, {
filename: 'evidence.pdf',
key: 'quarantine/case/evidence.pdf',
maxBytes: 25 * 1024 * 1024
});
if (result.verdict === 'clean') await promoteUpload(result);Both adapters implement the same BlobStore interface, so swapping providers is a one-constructor change.
Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
@absolutejs/blob — object storage substrate. One BlobStore interface, multiple adapters: - @absolutejs/blob/local — filesystem (dev / single-host prod) - @absolutejs/blob/s3 — S3-compatible (AWS S3, Cloudflare R2, Backblaze B2, MinIO, Wasabi) Adapters share the same shape so swapping providers is one constructor change. Out-of-scope (deliberate): - CDN integration. The presign URLs from S3-compat stores work for direct browser download; CDN cache invalidation is the CDN's job. - Encryption-at-re
type BlobMetadata = {
/** Size in bytes. */
size: number;
/** MIME type the blob was stored with (or `undefined` if unset). */
contentType?: string;
/** Server-side ETag (typically the MD5 hash, but provider-specific). */
etag?: string;
/** Last-modified time, ms since epoch. */
lastModified?: number;
/** Caller-supplied user metadata. */
metadata?: Record<string, string>;
};@absolutejs/blobCurrent package surface
Import surface · click to copy