Cloudflare R2 Driver
The R2 driver provides integration with Cloudflare R2, Cloudflare's S3-compatible object storage with zero egress fees. The driver extends S3Disk, so all S3 features (multipart uploads, presigned URLs, range requests, conditional writes) are available.
Configuration
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
driver | 'r2' | Yes | — | Must be 'r2' |
bucket | string | Yes | — | R2 bucket name |
accountId | string | Yes | — | Cloudflare account ID |
key | string | Yes | — | R2 API token access key ID |
secret | string | Yes | — | R2 API token secret access key |
region | string | No | 'auto' | R2 region hint (usually 'auto') |
url | string | No | — | Custom domain base URL for public access |
visibility | 'public' | 'private' | No | 'private' | Default visibility |
Basic Setup
import { Module } from '@nestjs/common';
import { StorageModule } from '@fozooni/nestjs-storage';
@Module({
imports: [
StorageModule.forRoot({
default: 'r2',
disks: {
r2: {
driver: 'r2',
bucket: 'my-app-assets',
accountId: process.env.CF_ACCOUNT_ID,
key: process.env.R2_ACCESS_KEY_ID,
secret: process.env.R2_SECRET_ACCESS_KEY,
url: 'https://assets.example.com', // Custom domain
},
},
}),
],
})
export class AppModule {}Async Configuration with ConfigService
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';
@Module({
imports: [
ConfigModule.forRoot(),
StorageModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
default: 'r2',
disks: {
r2: {
driver: 'r2',
bucket: config.getOrThrow('R2_BUCKET'),
accountId: config.getOrThrow('CF_ACCOUNT_ID'),
key: config.getOrThrow('R2_ACCESS_KEY_ID'),
secret: config.getOrThrow('R2_SECRET_ACCESS_KEY'),
url: config.get('R2_CUSTOM_DOMAIN'),
},
},
}),
}),
],
})
export class AppModule {}R2 API Token Setup
To generate R2 API credentials:
- Log into the Cloudflare dashboard
- Navigate to R2 in the sidebar
- Click Manage R2 API Tokens
- Click Create API Token
- Select permissions:
- Object Read & Write for full access
- Object Read for read-only access
- Optionally restrict to specific buckets
- Copy the Access Key ID and Secret Access Key
Your accountId is visible in the Cloudflare dashboard URL: https://dash.cloudflare.com/<accountId>/r2
TIP
The R2 endpoint is automatically constructed from your account ID:
https://<accountId>.r2.cloudflarestorage.comYou do not need to set the endpoint field manually.
R2 Extends S3Disk
Under the hood, R2Disk extends S3Disk and configures the S3 client to point at the R2 endpoint:
S3Disk
└── R2Disk (endpoint: https://<accountId>.r2.cloudflarestorage.com)This means every feature documented on the S3 driver page works identically with R2:
- Presigned URLs via
temporaryUrl()andtemporaryUploadUrl() - Presigned POST via
presignedPost() - Multipart uploads via
initMultipartUpload()/putFileMultipart() - Range requests via
getRange() - Conditional writes via
putIfMatch()/putIfNoneMatch() - Streaming via
getStream()/putStream() - Checksums and custom metadata
No Egress Fees
R2's primary advantage over S3 is zero egress fees. Data transfer out is free regardless of volume. This makes R2 particularly cost-effective for:
- Public asset hosting (images, videos, documents)
- Frequently downloaded files
- API responses serving stored content
- CDN origin storage
R2 vs S3 Differences
While R2 is S3-compatible, there are some differences to be aware of:
| Feature | S3 | R2 |
|---|---|---|
| Egress fees | Per-GB charges | Free |
| Per-object ACLs | Supported | Not supported |
| Storage classes | Multiple (Standard, IA, Glacier, etc.) | Single class |
| Regions | Choose specific region | Automatic (globally distributed) |
| Versioning | Supported | Supported |
| Lifecycle rules | Full support | Basic support |
| S3 Select | Supported | Not supported |
| Replication | Cross-region replication | Not supported |
Public Access and Custom Domains
R2 does not support per-object ACLs. Instead, public access is controlled at the bucket level through Cloudflare's dashboard:
- Go to R2 > your bucket > Settings
- Under Public access, click Allow Access
- Optionally connect a Custom Domain
// With a custom domain configured:
const disk = this.storage.disk('r2');
// Returns: https://assets.example.com/images/hero.jpg
const url = await disk.url('images/hero.jpg');No Per-Object ACL Support
Unlike S3, R2 does not support per-object ACLs. The visibility config option controls the default behavior, but you cannot make individual objects public or private. Public access must be enabled or disabled at the bucket level through the Cloudflare dashboard.
// This will NOT make the individual object public on R2
await disk.put('file.txt', content, { visibility: 'public' });
// The file's accessibility depends on the bucket-level public access settingPresigned URLs
Even without public bucket access, you can generate presigned URLs for temporary access:
@Injectable()
export class R2DownloadService {
constructor(private readonly storage: StorageService) {}
async getTemporaryLink(path: string): Promise<string> {
const disk = this.storage.disk('r2');
// Generate a presigned URL valid for 1 hour
const url = await disk.temporaryUrl(path, 3600);
return url;
}
}Multipart Uploads
@Injectable()
export class R2UploadService {
constructor(private readonly storage: StorageService) {}
async uploadLargeFile(path: string, stream: Readable) {
const disk = this.storage.disk('r2');
// Convenience method handles chunking automatically
await disk.putFileMultipart(path, stream, {
partSize: 10 * 1024 * 1024, // 10 MB parts
concurrency: 3,
});
}
async uploadWithPresignedUrl(path: string) {
const disk = this.storage.disk('r2');
// Generate a presigned URL for client-side upload
const url = await disk.temporaryUploadUrl(path, 3600, {
contentType: 'application/pdf',
});
return { uploadUrl: url };
}
}Range Requests and Streaming
@Controller('assets')
export class AssetController {
constructor(private readonly storage: StorageService) {}
@Get(':path(*)')
@RangeServe()
async serve(@Param('path') path: string) {
return { disk: 'r2', path };
}
}Using R2 with Cloudflare Workers
If you deploy your NestJS application alongside Cloudflare Workers, R2 can be accessed with lower latency from Workers. For server-side NestJS applications, the driver communicates with R2 via the S3-compatible HTTP API.
// Standard NestJS usage — communicates via HTTPS
const disk = this.storage.disk('r2');
await disk.put('data/report.json', JSON.stringify(report));Complete Example: Asset Service
import { Injectable, NotFoundException } from '@nestjs/common';
import { StorageService } from '@fozooni/nestjs-storage';
import { v4 as uuid } from 'uuid';
@Injectable()
export class AssetService {
constructor(private readonly storage: StorageService) {}
private get disk() {
return this.storage.disk('r2');
}
async upload(file: Express.Multer.File): Promise<{ key: string; url: string }> {
const ext = file.originalname.split('.').pop();
const key = `assets/${uuid()}.${ext}`;
await this.disk.put(key, file.buffer, {
metadata: {
originalName: file.originalname,
mimeType: file.mimetype,
},
});
return {
key,
url: await this.disk.url(key),
};
}
async getTemporaryUrl(key: string, expiresIn = 3600): Promise<string> {
if (!(await this.disk.exists(key))) {
throw new NotFoundException('Asset not found');
}
return this.disk.temporaryUrl(key, expiresIn);
}
async delete(key: string): Promise<void> {
await this.disk.delete(key);
}
async listAssets(prefix = 'assets/') {
return this.disk.listContents(prefix);
}
}Environment Variables Example
# .env
CF_ACCOUNT_ID=your-cloudflare-account-id
R2_BUCKET=my-app-assets
R2_ACCESS_KEY_ID=your-r2-access-key-id
R2_SECRET_ACCESS_KEY=your-r2-secret-access-key
R2_CUSTOM_DOMAIN=https://assets.example.com