Amazon S3
Amazon S3 and S3-compatible stores (R2, MinIO, B2) as the planned MainSDK files driver, with bucket setup, direct browser uploads and an example.
Amazon S3 is object storage. The s3 driver for files works with S3 itself and with stores that speak the S3 API, such as Cloudflare R2, MinIO and Backblaze B2. You would pick it when you already have a bucket, need its region, or want your files in your own cloud account.
Interfaces it serves
Pick Amazon S3 in main.yaml for any of these. Your calls stay the same, and you can swap later.
filesBuilding for 0.3Driver s3 Amazon S3 and S3-compatible stores (R2, MinIO, B2)From the files-share spec
files:
driver: s3Swap to another provider
Before you start
- Create a bucket with Block all public access on. MainSDK never serves files from a public bucket.
- Create an access key limited to that bucket (
s3:PutObject,s3:GetObject,s3:DeleteObjectonarn:aws:s3:::acme-uploads/*). - Allow your site to upload from the browser. Add this CORS rule to the bucket:
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["PUT", "POST"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3000
}
]- Put the secret key in your secrets manager as
S3_SECRET_ACCESS_KEY. On AWS you can skip the key and use the runtime's IAM role.
Configure
files:
driver: s3
bucket: acme-uploads
region: eu-west-1 # the bucket's own region
access-key-id: AKIA… # an identifier, not a secret
key: secret:S3_SECRET_ACCESS_KEY
scan: true
max-size: 25MBaccess-key-idis an identifier, so it may sit in the file as a literal.keyis the secret and must be asecret:reference.- On AWS, omit both
access-key-idandkeyto use the runtime's IAM role. With neither set, the driver uses role credentials and reads credentials from nowhere else. regionis the bucket's region. Theeuregion in the rest of these docs is MainSDK's data region for MainSDK's own data, and it does not move your files. Pick a bucket in the region your users' data must stay in.
For an S3-compatible store add its endpoint:
files:
driver: s3
bucket: acme-uploads
endpoint: https://<account>.r2.cloudflarestorage.com # R2; MinIO and B2 use their own URL
access-key-id: <access key id>
key: secret:S3_SECRET_ACCESS_KEYUse it
Browsers upload straight to the bucket on a signed, single-use URL; your server never carries the bytes. Issue the upload on the server:
import { auth, files } from "mainsdk";
export async function POST(req: Request) {
const session = await auth.require(req);
const upload = await files.uploadUrl({
owner: session.user,
maxSize: "5MB",
types: ["image/png", "image/jpeg"],
});
return Response.json(upload);
}import { auth, files } from "mainsdk";
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
const session = await auth.require(req);
const { id } = await params; // upload.ref.id
const ref = await files.complete(id, { owner: session.user }); // only the uploader can finish it
const url = await files.url(ref, { for: session.user, expiresIn: "1h" });
return Response.json({ url });
}The browser sends the file to upload.url with upload.method, then calls the complete route. The browser function, the Hono version and the offline test are in Upload files with S3, Uploadthing or Vercel Blob. A 6 MB file or a text/html file sent to the URL above is refused by the store, and the file is not readable until files.complete() has run the scan.
Local development and testing
Use the local driver in development and createTestApp() from @mainsdk/testing in tests; neither needs a bucket. To test against the real driver offline, run MinIO and point endpoint at it in main.env.dev.yaml.
What MainSDK adds on top
- Owner-scoped reads: a signed URL is never issued for another user's file.
- A scan before a file is readable, and resumable, idempotent uploads.
- Short-lived URLs with a size and type limit on every upload URL.
mainsdk checkfails if route code imports@aws-sdk/client-s3directly;files.raw()gives you the client and is listed as outside the guarantees.
Verify
Run npx mainsdk check --env prod. A pass exits 0 and names no MAIN-FILES or MAIN-CONFIG finding.
If it fails
| Code | Cause | Fix |
|---|---|---|
MAIN-FILES-0002 | files.url() was called for a reader access denies (403). | Read as the owner, or add an access rule for the reader. |
MAIN-FILES-0005 | The scan in files.complete() found an infected file. The upload fails and the file is quarantined. | Ask the user for a clean file. Nothing to fix in code. |
MAIN-FILES-0010 | mainsdk check found @aws-sdk/client-s3 imported in route code. | Use files.uploadUrl() and files.complete(), and keep the store as the files driver. |
MAIN-CONFIG-0004 | key is a literal value, not a secret: reference. | Move the value to your secrets manager and write secret:S3_SECRET_ACCESS_KEY. |
For agents
Goal: use s3 as the files driver and add owner-scoped uploads to this app.
Read: docs/specs/files-share.md, this page, main.yaml.
Touch only: main.yaml, main.env.dev.yaml, app/api/uploads/**, one test file.
Do: set files.driver to s3 with key: secret:S3_SECRET_ACCESS_KEY;
issue uploads with files.uploadUrl({ owner, maxSize, types });
send the file to upload.url with upload.method; call files.complete(upload.ref.id, { owner }) as the same user, then files.url(ref, { for, expiresIn }).
Verify: npx mainsdk check --json --agent
Never weaken, skip or loosen a check to make it pass.
Never import @aws-sdk/client-s3 in route code.Swap it
Change driver to getqr, uploadthing or vercel-blob. Your calls stay the same; existing files stay where they are until you migrate them. See Swap a driver.
Was this page helpful?