@antelopejs/file-storage-s3
The AntelopeJS file storage module for S3-compatible object storage, such as AWS S3 and Cloudflare R2. It uses @aws-sdk/client-s3, and clients upload and read files directly from the bucket through presigned URLs.
Interfaces
| Interface | Documentation |
|---|---|
@antelopejs/interface-file-storage | File storage |
The interface pages document uploads, reads, visibility, metadata, staging, and promotion.
Install
ajs project modules add @antelopejs/file-storage-s3
The module ships no default configuration, so add a config block with a default storage before you start the project.
Configuration
import { defineConfig } from "@antelopejs/interface-core/config";
export default defineConfig({
name: "my-project",
modules: {
"file-storage-s3": {
source: {
type: "package",
package: "@antelopejs/file-storage-s3",
version: "^0.1.5",
},
config: {
default: {
endpoint: "https://<account>.r2.cloudflarestorage.com",
region: "auto",
accessKeyId: "<access-key-id>",
secretAccessKey: "<secret-access-key>",
bucket: "app-files",
attachmentPrivateBucket: "app-private-files",
publicUrl: "https://cdn.example.com",
defaultVisibility: "private",
defaultUploadExpiration: 3600,
defaultReadExpiration: 300,
stagingExpirationDays: 1,
assumePrivateBuckets: false,
},
storages: {
media: {
endpoint: "https://<account>.r2.cloudflarestorage.com",
region: "auto",
accessKeyId: "<access-key-id>",
secretAccessKey: "<secret-access-key>",
bucket: "app-media",
publicUrl: "https://media.example.com",
defaultVisibility: "public",
defaultUploadExpiration: 3600,
defaultReadExpiration: 300,
},
},
},
},
},
envOverrides: {
S3_SECRET_ACCESS_KEY:
"modules.file-storage-s3.config.default.secretAccessKey",
},
});
| Key | Description |
|---|---|
endpoint, region | S3 endpoint and region. R2 uses region auto. |
accessKeyId, secretAccessKey | Credentials of the S3 client. |
bucket | Bucket that holds the files. |
attachmentPrivateBucket | Optional bucket for uploads that request visibility: "private", kept apart from a public bucket. Without it, such an upload throws. |
publicUrl | Optional public origin of bucket. A public file is read from <publicUrl>/<resourceKey>. Without publicUrl, it is read through a presigned URL. |
defaultVisibility | Visibility of a file uploaded without one, "public" or "private". |
defaultUploadExpiration | Lifetime of an upload URL, in seconds. |
defaultReadExpiration | Lifetime of a private read URL, in seconds, when the caller sets none. |
stagingExpirationDays | Optional. At startup, the module puts a lifecycle rule on bucket and attachmentPrivateBucket that expires staged files after this many days. Omit it when your infrastructure manages lifecycle rules. |
assumePrivateBuckets | Optional, false by default. Relaxes the private bucket verification for providers that cannot pass it. |
Every key except the optional ones is required. default holds the main storage. storages adds named storages with the same keys, and the storage argument of the interface functions selects one. The name default is reserved in storages, and the module refuses to start with it.
Quick start
In the module that handles uploads, add the interface:
pnpm add @antelopejs/interface-file-storage
import {
CreateReadUrl,
CreateUploadUrl,
} from "@antelopejs/interface-file-storage";
const upload = await CreateUploadUrl(
{ filename: "avatar.png", size: 52_000, mimetype: "image/png" },
{ maxSize: 1_000_000, allowedMimetypes: ["image/png", "image/jpeg"] },
);
const read = await CreateReadUrl(upload.resourceKey);
The client sends the file with PUT upload.uploadUrl and every header of upload.headers, unchanged, then fetches it from read.url.
Before deploying
- Upload URLs sign an
If-None-Match: *condition, so an upload never replaces an existing object. The provider must enforce conditionalPUT, and the bucket CORS rules must allow theif-none-matchheader. - The module verifies that
attachmentPrivateBucketblocks all public access before using it. MinIO and Hetzner Object Storage needassumePrivateBuckets: true. - S3 stores metadata keys in lowercase, so
GetFileMetadatareturnstenantidfor an upload sent withtenantId. Two keys that differ only by case make the upload throw.
UPLOADS.md details these guarantees. It covers create-only uploads, promotion, privacy and expiry, private bucket verification, metadata keys, and CORS.
Documentation
- File storage documents the
@antelopejs/interface-file-storagecontract that this module implements. - Configuration covers module sources,
configblocks, and config variables inantelope.config.ts.
Compatibility
| Package | Supported | Tested with |
|---|---|---|
@antelopejs/interface-core | >=0.2.4 <1.0.0 | 0.2.4 |
@antelopejs/interface-file-storage | >=0.1.6 <0.2.0 | 0.1.6 |
.nvmrc names the Node.js release for development. The tests run against the Moto image that .github/release-services.yml pins.
Contributing
The tests need a Moto S3 server on 127.0.0.1:5005. Start the one that CI uses:
docker compose -f .github/release-services.yml up -d --wait
Then run the checks:
pnpm install
pnpm lint
pnpm format:check
pnpm knip
pnpm test # build, then ajs module test
The tests also run the shared conformance suite of @antelopejs/interface-file-storage. Read the contribution guidelines before opening a pull request.