R2Store — Cloudflare R2 object storage

Read this page in the documentation

R2Store — Cloudflare R2 object storage Overview Cloudflare R2 is Cloudflare's S3-compatible object store. Like Amazon S3 it has no query language, no rows, and no SQL-shaped access pattern — data is opaque objects (byte bodies plus metadata) addressed by a (bucket, key) pair, where the key is a flat string that only looks like a path (foo/bar/baz.json); R2 has no real directories, just an optional key prefix you can filter listings by. None of that fits the SQL-shaped Dialect interface, so R2Store implements the minimal NoSqlStore marker interface (src/nosql/store.ts) — connection lifecycle plus a getClient() escape hatch — and exposes object-level operations (Put/Get/Delete/List/Head) directly. Identity: Property | Value | --------- | ---------------------- | name | 'r2' | library | '@aws-sdk/client-s3' | Because R2 speaks the S3 wire protocol, this store is built on AWS SDK v3 (@aws-sdk/client-s3) exactly like S3Store: an S3Client uses the command pattern — you construct a command object (new PutObjectCommand(input)) and hand it to client.send(command). The only differences from a plain S3 connection are R2-specific client settings: R2 has no regions, so region defaults to the sentinel 'auto'; requests go to an account-specific endpoint; and path-style addressing is forced (forcePathStyle: true). Lazy loading — not a hard dependency @aws-sdk/client-s3 is not a hard dependency of this package. It is loaded lazily via require('@aws-sdk/client-s3') inside connect() (and on demand when a command is first built), rather than a top-level import. Importing this module therefore does not require the SDK to be installed — it is only needed when an R2 store is actually connected and driving real commands. Injected client R2StoreOptions.client accepts a pre-built S3Client (or any object exposing a compatible send(command) method, typed as R2ClientLike). When provided, connect() uses it directly and does not require the SDK to build a client (it still best-effort loads the SDK for the command constructors, falling back to loading them on demand per method). This is how the test suite injects a mock client (no SDK, no network). Connection Build a store from connection options and call connect(): Option | Type | Purpose | ------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | endpoint | string | The account-specific R2 S3 endpoint, e.g. https://<accountid>.r2.cloudflarestorage.com. Required unless a client is supplied. | credentials | { accessKeyId; secretAccessKey; sessionToken? } | R2 access-key credentials (created in the Cloudflare dashboard as an "R2 API Token"). Omit only when supplying a client. | region | string | Region to advertise to the SDK. Defaults to 'auto' (R2 has no regions). | client | R2ClientLike | A pre-built S3Client to use directly (mock or custom config). When set, the SDK is not required to build a client. | Internally, when no client is supplied the SDK builds the client with forcePathStyle: true (R2 requires path-style addressing). Injected-client form Methods Every object operation follows the AWS v3 command pattern: the store resolves a command constructor by name from the (lazily loaded) SDK, builds new XxxCommand(input), and calls client.send(command). If the SDK genuinely can't be resolved when a command is built, a ConnectionError is thrown. SDK failures are wrapped in a DatabaseError (preserving the original error, e.g. R2 PutObject on 'bucket/key' failed (Error): ...); using any method before connect() (or after disconnect()) throws a ConnectionError. Lifecycle Method | Signature | Behavior | ------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | connect | connect(): Promise<void> | Uses an injected client if provided (best-effort loading the SDK for command constructors), otherwise lazy-requires @aws-sdk/client-s3 and builds an S3Client pointed at R2 (region: 'auto', account endpoint, forcePathStyle: true). Wraps failures in ConnectionError. | disconnect | disconnect(): Promise<void>| Calls the client's optional destroy() and clears connection state (client + SDK handle). | isConnected| isConnected(): boolean | true only when connected and a client is present. | getClient | getClient(): R2ClientLike | Returns the underlying S3Client. Throws ConnectionError if not connected. | Object operations Method | Signature | AWS command | Behavior | -------------- | ---------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- | putObject | putObject(bucket: string, key: string, body: unknown, options?: PutObjectOptions): Promise<PutObjectResult> | PutObjectCommand | Store body (and optional metadata) under bucket/key. Returns { etag?, versionId? }. | getObject | getObject(bucket: string, key: string): Promise<GetObjectResult> | GetObjectCommand | Fetch an object's body and metadata. body is returned exactly as the SDK provides it (a stream/blob). | deleteObject | deleteObject(bucket: string, key: string): Promise<void> | DeleteObjectCommand | Remove an object. Succeeds even if the key doesn't exist. | listObjects | listObjects(bucket: string, prefix?: string): Promise<ListObjectsResult> | ListObjectsV2Command | List objects, optionally restricted to keys starting with prefix. Returns at most 1000 per call; page via nextContinuationToken (using the raw client). | headObject | headObject(bucket: string, key: string): Promise<HeadObjectResult> | HeadObjectCommand | Fetch object metadata without the body. Throws (NotFound) if the object doesn't exist. | objectExists | objectExists(bucket: string, key: string): Promise<boolean> | HeadObjectCommand | Returns true/false via headObject. Maps a NotFound / NoSuchKey / 404 (checking the error and its .parent) to false; re-throws any other error (e.g. access denied). | Option and result shapes Example Verification status Unit / mock-verified only. The tests in tests/nosql/r2.test.ts are fully mock-driven: a fake client whose send(command) records the command it was given and returns canned output is injected via R2StoreOptions.client. The real @aws-sdk/client-s3 package is not installed, and there is no live R2 access and no network in the test run — the tests stub the command constructors on the store's loaded SDK handle so command-type assertions are deterministic regardless of SDK presence. What this proves: Each method builds and sends the correct command type (PutObjectCommand, GetObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand) with the expected .input (Bucket, Key, Body, ContentType, Metadata, Prefix, …). Each method shapes the response the way the store's result interfaces promise — including listObjects mapping Contents to { key, size, etag, lastModified } and surfacing IsTruncated/NextContinuationToken, and returning an empty list when Contents is absent. Lifecycle: starts disconnected, connects via an injected client without loading the SDK, disconnect() destroying the client, and ConnectionError before connect() / after disconnect() and from getClient(). Error handling: SDK failures wrapped in DatabaseError, and objectExists mapping a NotFound name and a 404 $metadata status to false while re-throwing other errors (e.g. access denied). What this does not prove: live execution against real Cloudflare R2. The command/response shape is verified against the SDK's documented contract, but end-to-end execution over the wire has not been exercised here. Related reading All data stores — the full catalogue, grouped by purpose Database types — where this sits among the 22 categories