Storage (R2)
S3-compatible object storage, one bucket per project, auto-provisioned and wired into your app's environment. Connect with any S3 SDK — no keys to copy.
What you get
Every Loor project gets one object-storage bucket, provisioned automatically the moment the project is created — you'll see it in Project settings → R2 Storage next to MongoDB and Redis. Under the hood it's Cloudflare R2, which speaks the S3 API, so anything that talks to S3 talks to your bucket. Buckets are private by default.
Auto-injected env vars
Just like MONGODB_URI and REDIS_URL, the bucket's connection is injected
into your app's environment — in the dev workspace and in production deploys — so there's nothing to
copy or paste:
R2_BUCKET # the bucket name, e.g. studio-myapp-ab12cd34
R2_ENDPOINT # https://<account>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID # app-scoped token, object read/write
R2_SECRET_ACCESS_KEY # app-scoped token, object read/write
R2_REGION=auto
R2_PUBLIC_URL # public buckets only — https://<public-host>
The access keys are a dedicated app-scoped token (object read/write, limited to
your bucket) — not your account's root credentials. The same bucket is used in dev and production
because R2 is global. If you'd rather name the variables yourself, the same values are available as
template placeholders — {{RESOURCE:r2:bucket}},
{{RESOURCE:r2:endpoint}}, etc. — see
.env.example.loor.
Using it from code
Point any S3 client at the injected variables. With the AWS SDK for JavaScript:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: process.env.R2_REGION, // "auto"
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});
await s3.send(new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: "avatars/user-123.png",
Body: buffer,
ContentType: "image/png",
}));
The same shape works from boto3 (Python), aws-sdk-go-v2, or any other
S3-compatible library — set the endpoint, region auto, and the access/secret keys.
Public vs private
Private buckets (the default) are reachable only with credentials or signed URLs. Flip a bucket to
public and Loor attaches a public host; objects then serve directly over HTTPS and
the URL shows up as R2_PUBLIC_URL in your environment. You can change visibility per
bucket from the Studio sidebar, or ask Vibe Chat ("make the uploads bucket public").
Browser uploads (temp credentials)
For uploads that happen in the browser, you don't want the app-scoped secret on the client. Instead, create a scoped API key for the project (Studio → API keys) and have your backend mint short-lived, prefix-scoped credentials on demand:
# Mint short-lived, scoped S3 credentials with a project API key
curl -X POST "$LOOR_API/m2m/r2/buckets/$BUCKET_ID/credentials" \
-H "Authorization: Bearer $LOOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "permission": "object-read-write", "ttlSeconds": 900, "prefixes": ["uploads/"] }'
# → { accessKeyId, secretAccessKey, sessionToken, expiresAt, bucketName, publicHost } The returned credentials expire (60s–1h), can be locked to specific key prefixes or objects, and carry only the permission you ask for — hand them to the browser to upload directly to R2 without proxying bytes through your server.
Managing buckets
From Project settings → R2 Storage you can see usage and quota, view the connection values, toggle visibility, and edit CORS rules (allowed origins, methods, max-age) per bucket. Vibe Chat can do the same on request. Deleting a project tears down its buckets and revokes their tokens.