s3nd

Configuration

Every option createBucket accepts, and the environment variable behind it.

createBucket({
  bucket: 'my-bucket', // required (or S3ND_BUCKET / S3_BUCKET)
  region: 'eu-west-3', // or S3ND_REGION / AWS_REGION / AWS_DEFAULT_REGION
  credentials: {
    // omit to use the AWS default provider chain
    accessKeyId: '…',
    secretAccessKey: '…',
    sessionToken: '…', // optional
  },
  endpoint: 'https://…', // S3-compatible storage — or S3ND_ENDPOINT / S3_ENDPOINT
  forcePathStyle: true, // defaults to true when `endpoint` is set
  publicUrl: 'https://cdn…', // public bucket or CDN — or S3ND_PUBLIC_URL / S3_PUBLIC_URL
  prefix: 'snapshots', // internal namespace, applied on the way in and out
  maxSize: 5 * 1024 * 1024, // reject bigger writes before any network call
  syncCode: { length: 8 }, // shape of the codes store.codes makes and reads
  client: myS3Client, // bring your own S3Client
})

Options

OptionEnvironment fallbackNotes
bucketS3ND_BUCKET, S3_BUCKETThe only required option.
regionS3ND_REGION, AWS_REGION, AWS_DEFAULT_REGIONDefaults to "auto" when an endpoint is set.
credentialsOmit for the AWS default provider chain.
endpointS3ND_ENDPOINT, S3_ENDPOINTFor R2, MinIO, Scaleway, Wasabi…
forcePathStyleDefaults to true with a custom endpoint, false otherwise.
publicUrlS3ND_PUBLIC_URL, S3_PUBLIC_URLMust be an absolute URL. Trailing slash optional.
prefixApplied by every method.
maxSizeIn bytes. Enforced before any network call.
syncCode{ length, alphabet }. Defaults to eight Crockford base32 characters — see Configuring codes.
clientAn S3Client you built yourself.

The prefix is a namespace, not part of the key

The prefix is applied on the way in and stripped on the way out, so the identifier you store in your database is the one you hand back to every method:

const store = createBucket({ bucket: 'my-bucket', prefix: 'snapshots' })

const result = await store.putSnapshot('K7QP2M4X', state)

result.key // "K7QP2M4X" — the code the user carries
result.path // "snapshots/K7QP2M4X" — the real object key, for the AWS console

await store.getSnapshot('K7QP2M4X') // reads snapshots/K7QP2M4X
await store.delete('K7QP2M4X') // deletes snapshots/K7QP2M4X

Every method takes a per-call prefix that overrides the bucket-level one, which is how you keep transfer codes, per-account backups and attachments apart:

await store.putSnapshot(`user-${userId}`, state, { prefix: 'backups' })
await store.getSnapshot(`user-${userId}`, { prefix: 'backups' })

Separating them early is what lets you give each one its own S3 lifecycle rule: a transfer code should expire in a day, a backup should not expire at all.

Public URLs

Set publicUrl when the bucket is served publicly or sits behind a CDN. Two things change:

  • upload() returns a ready-to-use url in its result.
  • getUrl() returns the public URL instead of a presigned one, unless you pass signed: true.
const store = createBucket({ bucket: 'my-bucket', publicUrl: 'https://cdn.example.com' })

await store.getUrl('avatars/42.png')
// → "https://cdn.example.com/avatars/42.png"

await store.getUrl('avatars/42.png', { signed: true, expiresIn: 60 })
// → a presigned URL, even though publicUrl is set

Without publicUrl, upload() leaves url undefined on purpose. A URL that answers 403 is worse than no URL at all.

Size caps

maxSize is checked against the body size before anything is sent, so an oversized upload costs you nothing:

const store = createBucket({ bucket: 'my-bucket', maxSize: 4 * 1024 * 1024 })

await store.upload(tooBig) // throws S3ndError { code: 'FILE_TOO_LARGE' }

Set it below your runtime's own request limit and you get a clean 413 instead of a truncated request. See How big can a snapshot be.

Bringing your own client

client takes over completely — region, credentials, endpoint and retry strategy all come from it:

import { S3Client } from '@aws-sdk/client-s3'

const store = createBucket({
  bucket: 'my-bucket',
  client: new S3Client({ region: 'eu-west-3', maxAttempts: 5 }),
})

A client you passed in is yours to close: store.destroy() leaves it alone.

On this page