Cloudflare R2

The @sobj/r2 adapter connects Sobj to Cloudflare R2 using the S3-compatible API.

It signs requests with AWS Signature Version 4 using the Web Crypto API — no AWS SDK and no external dependencies.

📥 Installation

bash
pnpm add sobj @sobj/r2

⚙️ Configuration

Create an R2 client by passing your credentials to r2():

ts
import { sobj } from 'sobj'
import { r2 } from '@sobj/r2'

const storage = sobj(
  r2({
    accountId: 'YOUR_ACCOUNT_ID',
    accessKeyId: 'YOUR_ACCESS_KEY_ID',
    secretAccessKey: 'YOUR_SECRET_ACCESS_KEY',
    bucket: 'YOUR_BUCKET',
  })
)

🔧 Config options

OptionTypeRequiredDescription
accountIdstringCloudflare account ID
accessKeyIdstringR2 S3 API access key ID
secretAccessKeystringR2 S3 API secret access key
bucketstringR2 bucket name
sessionTokenstringTemporary credential session token
endpointstringOverride the S3 endpoint (e.g. for jurisdictional buckets)
fetchtypeof fetchCustom fetch implementation

🌐 Jurisdictional buckets

For jurisdictional R2 buckets, override the endpoint:

ts
r2({
  // ...
  endpoint: 'https://YOUR_ACCOUNT_ID.eu.r2.cloudflarestorage.com',
})

📊 Capabilities

The R2 adapter supports all Sobj operations:

OperationSupported
put
get
head
delete
list
copy
presign
multipart
versioning

⚡ Operations

📤 put

Upload an object:

ts
await storage.put('images/avatar.png', body, {
  contentType: 'image/png',
  metadata: { userId: '123' },
})

The body can be a string, Uint8Array, ArrayBuffer, Blob, or ReadableStream<Uint8Array>.

📥 get

Retrieve an object. Returns null if not found.

ts
const object = await storage.get('images/avatar.png')

if (object) {
  const text = await new Response(object.body).text()
}

With a byte range:

ts
const object = await storage.get('video.mp4', {
  range: { start: 0, end: 1_048_575 }, // first 1 MB
})

🔍 head

Retrieve metadata without downloading the object body:

ts
const metadata = await storage.head('images/avatar.png')

if (metadata) {
  console.log(metadata.size, metadata.contentType, metadata.etag)
}

🗑️ delete

Delete an object:

ts
await storage.delete('images/avatar.png')

📋 list

List objects in the bucket:

ts
const result = await storage.list()

for (const object of result.objects) {
  console.log(object.key)
}

With options:

ts
const result = await storage.list({
  prefix: 'images/',
  delimiter: '/',
  limit: 100,
  cursor: result.cursor, // for pagination
})

The prefixes array contains common prefixes (virtual directories) when a delimiter is used.

📂 copy

Copy an object within the same bucket:

ts
await storage.copy('source/file.txt', 'destination/file.txt')

Prevent overwriting an existing destination:

ts
await storage.copy('source.txt', 'dest.txt', { overwrite: false })

🔑 presign

Generate a pre-signed URL for GET or PUT:

ts
const url = await storage.presign.get('images/avatar.png', {
  expiresIn: 3600, // seconds (default: 3600)
})

Pre-signed upload URL:

ts
const url = await storage.presign.put('uploads/file.bin', {
  expiresIn: 900,
  contentType: 'application/octet-stream',
})

📦 multipart

Upload large objects using the S3 multipart upload API:

ts
const upload = await storage.multipart.create('large-file.bin')

const part1 = await storage.multipart.uploadPart(upload, 1, chunk1)
const part2 = await storage.multipart.uploadPart(upload, 2, chunk2)

await storage.multipart.complete(upload, [part1, part2])

Abort an incomplete upload:

ts
await storage.multipart.abort(upload)

⚠️ Error handling

The adapter throws SobjError subclasses:

ts
import { isSobjError, SobjNotFoundError } from 'sobj/errors'

try {
  const object = await storage.get('missing.txt')
} catch (error) {
  if (error instanceof SobjNotFoundError) {
    console.log('Object not found')
  } else if (isSobjError(error)) {
    console.log(error.code, error.status)
  }
}

See the Errors reference for all available error classes.