A Bucket is a logical store for files. Encore provisions the underlying object storage (S3 on AWS, GCS on GCP, in-memory locally). Declare buckets at package level.
import { Bucket } from "encore.dev/storage/objects";
// Private bucket (default)
export const uploads = new Bucket("user-uploads", {
versioned: false, // Set to true to keep multiple versions
});
// Public bucket — files accessible via public URL
export const publicAssets = new Bucket("public-assets", {
public: true,
versioned: false,
});
// Upload
const attrs = await uploads.upload("path/to/file.jpg", buffer, {
contentType: "image/jpeg",
});
// Download
const data = await uploads.download("path/to/file.jpg");
// Existence check
const exists = await uploads.exists("path/to/file.jpg");
// Attributes (size, content type, ETag)
const meta = await uploads.attrs("path/to/file.jpg");
// Delete
await uploads.remove("path/to/file.jpg");
// List
for await (const entry of uploads.list({})) {
console.log(entry.key, entry.size);
}
// Public URL (only for public buckets)
const url = publicAssets.publicUrl("image.jpg");
The keys shown are placeholders. In real handlers, build keys on the server per user and confirm ownership before downloading, reading attributes, or removing an object. Scope listing to the caller with a per user prefix and a result limit, and keep object keys out of logs.
Generate temporary URLs so clients can upload/download directly without going through your service:
const uploadUrl = await uploads.signedUploadUrl("user-uploads/avatar.jpg", { ttl: 7200 });
const downloadUrl = await uploads.signedDownloadUrl("documents/report.pdf", { ttl: 7200 });
The ttl value is in seconds. Choose the shortest expiry the client flow needs, usually a few minutes rather than hours. Issue signed URLs only from endpoints that require authentication, after checking that the caller may access that specific key.
Pass bucket access to other code with a specific permission set:
import { Uploader, Downloader } from "encore.dev/storage/objects";
const uploaderRef = uploads.ref<Uploader>();
const downloaderRef = uploads.ref<Downloader>();
// Permission types: Downloader, Uploader, Lister, Attrser, Remover,
// SignedDownloader, SignedUploader, ReadWriter
ObjectNotFound — object doesn't exist
PreconditionFailed — upload preconditions not met (e.g. setIfNotExists)
ObjectsError — base error type
- Declare buckets at package level.
- Default to private buckets; opt into
public: true only for assets meant for unauthenticated download. Every object in a public bucket is readable by anyone, so keep user uploads and private documents in private buckets and share them with short lived signed download URLs.
- Use signed URLs for browser uploads/downloads instead of streaming through your service. Treat signed URLs as bearer secrets: keep the expiry short, keep them out of logs, and limit the allowed content type and size of uploads where the platform supports it.
- Use bucket references when passing access to helpers — they encode the permission contract in the type system. Pass the narrowest permission type a helper needs, and use ReadWriter only when the helper needs read, write and delete.
- Require authentication on endpoints that wrap download, remove, list, attributes or signed URL generation, and verify that the caller owns or may access the object key.
- Build object keys on the server from the authenticated user ID plus a generated ID. Reject client supplied keys or prefixes, and reject keys that contain two dots or start with a slash.
- Before storing an upload, enforce a maximum size and check the file type from its leading bytes against an allowlist. Set the content type on the server rather than trusting the value a client sends. Serve only safe image types from public buckets, and keep web pages and scripts out of them.
- With versioning off, deletes and overwrites are permanent. Enable versioning for user data that must be recoverable, and use the setIfNotExists precondition to guard against accidental overwrites.