Store and resume files in R2
Use Cloudflare's R2 S3-compatible API to upload an invoice, read its bytes and list the files in a bucket. Stop the World, resume it and read the same file without uploading it again. The app keeps the real AWS S3 SDK; only Cloudflare is selected for substitution.
Use Node 22.6 or newer, npm and an empty app directory. Dependency installation needs the normal package registry. Local use needs no platform account or real Cloudflare credentials. This example selects admitted @volter/twin-cloudflare@3.0.11 and the exact World cohort shown below; keep the manifest, lockfile and generated service source together.
Prepare the app
{
"name": "world-storage-user",
"private": true,
"type": "module",
"dependencies": {
"@aws-sdk/client-s3": "3.1147.0"
},
"devDependencies": {
"@volter/world": "3.0.158",
"@volter/world-core": "3.0.148",
"@volter/world-runtime": "3.0.156",
"@volter/world-console": "3.0.149",
"@volter/twin-cloudflare": "3.0.11"
}
}Declare the credential names and R2 endpoint setting below. The World issues throwaway values when it starts:
CLOUDFLARE_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_ENDPOINT=Save the application:
import { S3Client, CreateBucketCommand, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
const client = new S3Client({
region: 'auto',
endpoint: `https://${process.env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY },
forcePathStyle: true,
maxAttempts: 1,
});
const Bucket = 'user-uploads';
const Key = 'invoices/invoice-1001.txt';
const write = process.argv[2] !== 'read';
try {
if (write) {
await client.send(new CreateBucketCommand({ Bucket }));
await client.send(new PutObjectCommand({ Bucket, Key, Body: 'Invoice 1001: paid\n', ContentType: 'text/plain' }));
}
const file = await client.send(new GetObjectCommand({ Bucket, Key }));
const text = await file.Body.transformToString();
const listing = await client.send(new ListObjectsV2Command({ Bucket, Prefix: 'invoices/' }));
console.log(JSON.stringify({ bucket: Bucket, key: Key, text, contentType: file.ContentType, size: file.ContentLength,
files: listing.Contents?.map(({ Key, Size }) => ({ key: Key, size: Size })) }));
} finally { client.destroy(); }R2 uses the S3 SDK with region auto, the account's r2.cloudflarestorage.com endpoint and its R2 access key and secret, as Cloudflare's SDK guide describes. These values come from this World; never copy production keys to make a local request pass. The Node injector routes the account hostname to the selected twin. AWS is the client library's publisher here, not another vendor the app calls.
Select Cloudflare and store the file
npm install
./node_modules/.bin/volter world init --name storage-user --twins cloudflare --source cloudflare=@volter/twin-cloudflareReview the report before starting. Keep the Cloudflare service at 3.0.11 and its generated bindings. The static scan also recognizes @aws-sdk/client-s3 as an AWS signal; AWS is excluded by this explicit selection because the application targets Cloudflare R2. No AWS access is granted.
./node_modules/.bin/volter world up
./node_modules/.bin/volter world clock set 2026-01-15T12:00:00Z
./node_modules/.bin/volter world run -- node storage.mjs{"bucket":"user-uploads","key":"invoices/invoice-1001.txt","text":"Invoice 1001: paid\n","contentType":"text/plain","size":19,"files":[{"key":"invoices/invoice-1001.txt","size":19}]}The SDK creates the bucket and stores the object through the vendor API. The response reads the file's bytes, content type and length and lists the stored object. No handler fakes a successful mutation.
./node_modules/.bin/volter world log
./node_modules/.bin/volter world downThe log contains bucket creation and PutObject. Reads do not add stored mutations. down stops compute and retains the data.
Resume without another upload
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node storage.mjs read{"bucket":"user-uploads","key":"invoices/invoice-1001.txt","text":"Invoice 1001: paid\n","contentType":"text/plain","size":19,"files":[{"key":"invoices/invoice-1001.txt","size":19}]}./node_modules/.bin/volter world downThe read argument skips both writes. Resume preserves the bucket and file; it is not a fresh boot or reset. Start with a fresh branch for another copy of this example. Deliberately reset only when you want to discard the current branch's data and load defaults. Running the write mode again on this retained bucket is not an idempotent bucket-creation operation.
This example covers bucket creation, a small object upload, object retrieval, a prefix listing and retained reads after resume with the named release and SDK. It does not cover multipart uploads, public or presigned URLs, custom domains or Worker execution. The selected release owns those capabilities and limits. If a request fails, use recovery to distinguish transport, credentials and an unsupported operation.