stable

cloudflare-r2

Cloudflare R2 adapter for noy-db — S3-compatible object storage with zero egress fees; a thin factory over to-aws-s3 pointed at the R2 endpoint.

@noy-db/to-cloudflare-r2 · pnpm add @noy-db/to-cloudflare-r2 Zero egress fees, S3-API-compatible.

Shaperecord
Atomic CAS✓
Localityedge
Speedslow
Persistencedurable

What it does

A thin factory around @noy-db/to-aws-s3, pre-configured for the R2 endpoint (r2EndpointFor(accountId)). Same object-per-record shape and API surface as S3 — just point it at your Cloudflare account.

Notes

  • prefix must not end in / — forwarded to toAwsS3(), so it inherits that package's refusal (since 0.7.0-pre.0); the error names @noy-db/to-aws-s3 even when you called toCloudflareR2() (noy-db-to#109).
  • casAtomic: true — inherits to-aws-s3's conditional-write CAS (IfMatch / IfNoneMatch), plus serverWriteTime: true.
  • Record shape, one object per record, same loadAll() O(N)-requests caveat as S3.
  • Factory: toCloudflareR2(options: R2Options).

BREAKING: plaintext keys removed

accessKeyId / secretAccessKey are no longer accepted on R2Options. Authentication now has exactly two paths — a credentials source or a pre-built client — because the descriptor path cannot offer a plaintext key pair without breaking the credentialless-by-construction rule, and leaving it on the direct path would split auth into two diverging stories. Migrate by wrapping the static keys in a credential source:

// before
toCloudflareR2({ bucket, accountId, accessKeyId, secretAccessKey })

// after
toCloudflareR2({ bucket, accountId, credentials: async () => ({
  kind: 'aws', accessKeyId, secretAccessKey,
}) })

Store-locator descriptor

See store-locator descriptors for the general model. kind: 'cloudflare-r2', class: 'cloud'. address is { bucket, accountId?, prefix?, endpoint? } — endpoint is location, not tuning, so it lives here rather than in options, and supplements accountId rather than replacing it. options is { clockUncertaintyMs? }. binding.client? carries a pre-built S3Client (a shared R2-pointed client, a Workers binding, or a test fake), which always wins over accountId + credentials. R2 keys are S3-compatible, so credentials arrive via a kind: 'aws'StoreCredentialSource at resolve() time.

import { r2StoreDescriptor, registerR2Store } from '@noy-db/to-cloudflare-r2'

const descriptor = r2StoreDescriptor({ bucket: 'my-vault', accountId: '...' })

See also