X Enterprises

fastify-xstorage

S3-compatible file storage plugin for Fastify — upload, download, delete, copy, move, list, signed URLs, presigned upload URLs, and batch operations for AWS S3, Cloudflare R2, and DigitalOcean Spaces.

fastify-xstorage

S3-compatible file storage for Fastify v5. Register once and get fastify.xStorage — a full-featured decorator covering upload, download, delete, copy, move, list, existence checks, metadata, signed download URLs, and presigned upload URLs. Works with AWS S3, Cloudflare R2, DigitalOcean Spaces, and any S3-compatible provider.

Installation

npm install @xenterprises/fastify-xstorage

For HTTP file uploads, also install:

npm install @fastify/multipart@9

Quick Start

import Fastify from "fastify";
import xStorage from "@xenterprises/fastify-xstorage";

const fastify = Fastify({ logger: true });

await fastify.register(xStorage, {
  accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
  secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
  bucket: process.env.STORAGE_BUCKET,
  publicUrl: process.env.STORAGE_PUBLIC_URL,
  endpoint: process.env.STORAGE_ENDPOINT, // omit for AWS S3
});

// Upload a file
const result = await fastify.xStorage.upload(buffer, "photo.jpg", {
  folder: "avatars",
});
// { key: "avatars/photo-a1b2c3d4.jpg", url: "https://cdn.example.com/...", ... }

// Signed download URL (1 hour)
const url = await fastify.xStorage.getSignedUrl(result.key, 3600);

Options

NameTypeDefaultRequiredDescription
accessKeyIdstringYesStorage access key ID
secretAccessKeystringYesStorage secret access key
bucketstringYesBucket name
publicUrlstringYesBase public URL for the bucket (trailing slash trimmed automatically)
endpointstringNoCustom endpoint URL for R2/DO Spaces/other providers; omit for AWS S3
regionstring"us-east-1"NoAWS region or "auto" for Cloudflare R2
forcePathStylebooleantrueNoUse path-style S3 URLs (required by most non-AWS providers)
aclstring"private"NoDefault ACL applied to uploads

Methods

Upload

Download

Delete

Copy & Move

Metadata & Existence

  • exists(key) — Check whether a file exists without downloading it.
  • getMetadata(key) — Get content type, size, last-modified, ETag, and custom metadata.

List

URLs

Error Reference

Registration errors

Registration fails fast with errors that name the plugin, the option, its type, and a usage example:

ErrorCause
xstorage: missing required option `accessKeyId` (string), e.g. `app.register(xStorage, { accessKeyId: 'AKIA...' })` Missing or non-string accessKeyId
xstorage: missing required option `secretAccessKey` (string), e.g. `app.register(xStorage, { secretAccessKey: '...' })` Missing or non-string secretAccessKey
xstorage: missing required option `bucket` (string), e.g. `app.register(xStorage, { bucket: 'my-bucket' })` Missing or non-string bucket
xstorage: missing required option `publicUrl` (string), e.g. `app.register(xStorage, { publicUrl: 'https://my-bucket.nyc3.digitaloceanspaces.com' })` Missing or non-string publicUrl
xstorage: option `endpoint` must be a string, e.g. `app.register(xStorage, { endpoint: 'https://nyc3.digitaloceanspaces.com' })` Non-string endpoint
xstorage: option `region` must be a string, e.g. `app.register(xStorage, { region: 'us-east-1' })` Non-string region
xstorage: option `forcePathStyle` must be a boolean, e.g. `app.register(xStorage, { forcePathStyle: true })` Non-boolean forcePathStyle
xstorage: option `acl` must be a string, e.g. `app.register(xStorage, { acl: 'private' })` Non-string acl

Method errors

Method calls validate inputs before touching S3 and throw Errors prefixed with [xStorage]:

ErrorCause
[xStorage] file is required for uploadupload() called without a file
[xStorage] filename is required and must be a stringMissing or non-string filename
[xStorage] files must be a non-empty arrayuploadMultiple() given empty or non-array
[xStorage] key is required and must be a stringAny method called without a valid key
[xStorage] keys must be a non-empty array of stringsdeleteMultiple() given empty or non-array
[xStorage] sourceKey is required and must be a stringcopy()/move() missing source key
[xStorage] destinationKey is required and must be a stringcopy()/move() missing destination key
[xStorage] expiresIn must be a positive number (seconds)Non-positive or non-number expiresIn in getSignedUrl()
[xStorage] Failed to upload file "<key>": <reason>S3 API error during upload
[xStorage] Failed to download file "<key>": <reason>S3 API error during download
[xStorage] Failed to delete file "<key>": <reason>S3 API error on single delete
[xStorage] Failed to delete multiple files: <reason>S3 API error on batch delete
[xStorage] Failed to copy "<src>" to "<dst>": <reason>S3 API error on copy (also surfaces from move())
[xStorage] Failed to check existence of "<key>": <reason>HeadObject error other than 404 in exists()
[xStorage] Failed to get metadata for "<key>": <reason>HeadObject error in getMetadata()
[xStorage] Failed to list files with prefix "<prefix>": <reason>S3 API error on list()
[xStorage] Failed to list all files with prefix "<prefix>": <reason>S3 API error on listAll()
[xStorage] Failed to generate signed URL for "<key>": <reason>Pre-signer error on download URL
[xStorage] Failed to generate signed upload URL for "<key>": <reason>Pre-signer error on upload URL

exists() is the only method that swallows an S3 error — a 404/NotFound returns false; any other error is rethrown.

Environment Variables

The plugin never reads process.env — all configuration arrives via the options object passed to app.register(). The variables below are a consumer convention: your application owns env access, reads these variables, and passes the values in as options:

await fastify.register(xStorage, {
  accessKeyId: process.env.STORAGE_ACCESS_KEY_ID,
  secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY,
  bucket: process.env.STORAGE_BUCKET,
  publicUrl: process.env.STORAGE_PUBLIC_URL,
  endpoint: process.env.STORAGE_ENDPOINT, // omit for AWS S3
  region: process.env.STORAGE_REGION, // optional, defaults to "us-east-1"
});
VariableMaps to optionRequired by the pluginDescription
STORAGE_ACCESS_KEY_IDaccessKeyIdYesStorage access key ID
STORAGE_SECRET_ACCESS_KEYsecretAccessKeyYesStorage secret access key
STORAGE_BUCKETbucketYesBucket name
STORAGE_PUBLIC_URLpublicUrlYesBase public URL for the bucket
STORAGE_ENDPOINTendpointNoCustom endpoint for R2/DO Spaces/other providers; omit for AWS S3
STORAGE_REGIONregionNoAWS region (option defaults to "us-east-1" when not passed)

Nothing is auto-detected: if you don't read an env var and pass it in, the plugin never sees it. Variable names are yours to choose — the plugin only validates the options object.

How It Works

On registration the plugin validates all required options, creates an S3Client with the provided credentials and optional custom endpoint, then decorates fastify.xStorage with all storage methods. The publicUrl trailing slash is normalised once at startup. Uploaded files get a random 8-byte hex suffix by default to avoid collisions (useRandomName: false disables this). MIME types are auto-detected from the filename extension via mime-types. list() returns a single page up to maxKeys; listAll() follows ContinuationToken until all results are fetched. move() is copy-then-delete. exists() uses HeadObject and returns false on 404 without throwing. getSignedUploadUrl() generates a PutObject presigned URL so clients can upload directly to storage without routing file payloads through your Fastify server.

AI Context

package: "@xenterprises/fastify-xstorage"
type: fastify-plugin
use-when: File storage with AWS S3, Cloudflare R2, or DigitalOcean Spaces — upload, download, delete, copy/move, list, signed URLs
decorator: fastify.xStorage
env: STORAGE_ACCESS_KEY_ID, STORAGE_SECRET_ACCESS_KEY, STORAGE_BUCKET, STORAGE_PUBLIC_URL, STORAGE_ENDPOINT (optional), STORAGE_REGION (optional) — consumer convention; the app reads these and passes values into register options (the plugin never reads process.env)
requires: accessKeyId, secretAccessKey, bucket, publicUrl at registration
Copyright © 2026