npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beignet/provider-storage-s3

v0.0.53

Published

S3-compatible object storage provider for Beignet

Readme

@beignet/provider-storage-s3

Runtime: Beignet requires Node.js 22.12 or newer. Bun is optional.

[!CAUTION] Beignet is experimental alpha software. The 0.0.x package line is for early evaluation, and APIs may change between releases while the framework settles.

S3-compatible object storage provider for Beignet.

The provider installs the app-facing ctx.ports.storage port. Use it for production object storage on AWS S3, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces, or another S3-compatible backend.

createS3StorageProvider(...) returns the stable S3StorageProvider type. S3StorageConfig describes its validated config; the Zod schema remains internal.

Install

bun add @beignet/provider-storage-s3 @beignet/core @aws-sdk/client-s3 @aws-sdk/[email protected] @aws-sdk/s3-request-presigner

Provider setup

import { createS3StorageProvider } from "@beignet/provider-storage-s3";
import { createServer } from "@beignet/core/server";

const server = await createServer({
  ports: basePorts,
  providers: [createS3StorageProvider()],
  context: ({ ports }) => ({ ports }),
  routes,
});

Environment variables:

| Variable | Description | | --- | --- | | STORAGE_S3_BUCKET | Bucket name. | | STORAGE_S3_REGION | Region. Defaults to us-east-1. Use auto for Cloudflare R2. | | STORAGE_S3_ENDPOINT | Optional S3-compatible endpoint. Required for R2, MinIO, Spaces, B2, and similar services. | | STORAGE_S3_ACCESS_KEY_ID | Optional static access key. | | STORAGE_S3_SECRET_ACCESS_KEY | Optional static secret key. | | STORAGE_S3_SESSION_TOKEN | Optional static session token. | | STORAGE_S3_PUBLIC_BASE_URL | Optional base URL returned by publicUrl(...) for public objects. | | STORAGE_S3_FORCE_PATH_STYLE | Optional true or false path-style addressing toggle. | | STORAGE_S3_KEY_PREFIX | Optional prefix for every object key written by this app. | | STORAGE_S3_MAX_ATTEMPTS | Optional maximum request attempts, including the first. Defaults to the AWS SDK's 3. | | STORAGE_S3_RETRY_MODE | Optional AWS SDK retry mode: standard (default) or adaptive. |

STORAGE_S3_BUCKET is required unless you pass bucket directly. beignet doctor --strict checks that installed S3 storage providers are registered in server/providers.ts and that the bucket requirement is present in app env examples or config when the env-backed provider is used.

AWS S3

STORAGE_S3_BUCKET=my-app-assets
STORAGE_S3_REGION=us-east-1
STORAGE_S3_PUBLIC_BASE_URL=https://cdn.example.com

When credentials are omitted, the AWS SDK uses its normal credential provider chain.

Cloudflare R2

STORAGE_S3_BUCKET=my-app-assets
STORAGE_S3_REGION=auto
STORAGE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
STORAGE_S3_ACCESS_KEY_ID=...
STORAGE_S3_SECRET_ACCESS_KEY=...
STORAGE_S3_PUBLIC_BASE_URL=https://assets.example.com

R2 is S3-compatible, but not every S3 feature exists on every compatible service. This provider only relies on object put, get, head, and delete.

Client lifecycle

When the provider creates the AWS SDK S3Client itself, its stop hook destroys that client on server shutdown to release keep-alive sockets.

Clients you inject — through createClient on createS3StorageProvider(...) or client on createS3Storage(...) and createS3UploadSigner(...) — are caller-owned. Beignet never destroys them, so close them yourself when your app shuts down.

Request cost

Some port operations need more than one S3 request to honor the StoragePort contract:

| Operation | S3 requests | | --- | --- | | put(...) with a known-size body | PutObject + HeadObject | | put(...) with a generic stream up to 5 MiB | managed PutObject + HeadObject | | put(...) with a generic stream over 5 MiB | CreateMultipartUpload + one UploadPart per 5 MiB part + CompleteMultipartUpload + HeadObject | | get(...) | GetObject | | stat(...) | HeadObject | | exists(...) | HeadObject | | delete(...) | HeadObject + DeleteObject | | publicUrl(...) | HeadObject, or none when publicBaseUrl is unset |

  • put(...) stats after writing so the returned StorageObject reflects what S3 actually stored instead of echoing the inputs.
  • delete(...) stats before deleting because S3 DeleteObject does not report whether the object existed, and the port returns that boolean.
  • publicUrl(...) stats to check the stored visibility, and skips S3 entirely when no public base URL is configured.

IAM permissions and missing objects

Grant the app the object actions its selected operations use (s3:GetObject, s3:PutObject, and s3:DeleteObject) plus s3:ListBucket on the bucket. Apps that upload generic streams also need s3:AbortMultipartUpload so a failed or cancelled managed upload can clean up uploaded parts. Amazon S3 can return 403 AccessDenied instead of 404 NotFound for a missing key when the caller lacks s3:ListBucket. Beignet intentionally does not map a generic 403 to "missing," because doing so would hide real credential or bucket policy failures. Without s3:ListBucket, get(...), stat(...), exists(...), and the preflight lookup in delete(...) may therefore throw for absent keys instead of returning their normal null or false result.

For AWS, scope s3:ListBucket to the bucket ARN and object actions to the object ARN, including the configured STORAGE_S3_KEY_PREFIX where practical. S3-compatible services may use different permission names but need equivalent head/get missing-object behavior.

Retries

S3 operations are idempotent, so bounded retry is appropriate. The provider does not add its own retry loop; when it creates an AWS SDK client, the AWS SDK retries transient failures itself. The SDK default is standard retry mode with 3 attempts, including the first attempt.

Tune the SDK behavior through provider options or env:

createS3StorageProvider({
  maxAttempts: 5,
  retryMode: "adaptive",
});
STORAGE_S3_MAX_ATTEMPTS=5
STORAGE_S3_RETRY_MODE=adaptive

standard retries throttling and transient errors with exponential backoff. adaptive adds client-side rate limiting on top of standard retries; pick it when the app regularly hits S3 throttling and prefers slower requests over throttle errors.

createS3Storage(...) and createS3UploadSigner(...) accept the same maxAttempts and retryMode options when they create the default client. Injected clients keep their own retry configuration and are called once by Beignet. Configure retry behavior on the injected client when the app owns the client lifecycle.

put(...) supplies Content-Length for strings, byte arrays, and Blobs. A generic ReadableStream stays streaming: the AWS SDK holds a bounded set of 5 MiB parts in memory and switches to multipart upload when the stream exceeds one part. Failed multipart uploads are aborted automatically. Prefer signed direct uploads for large browser files so bytes do not pass through the application server at all.

Direct port factory

import { createS3Storage } from "@beignet/provider-storage-s3";

const storage = createS3Storage({
  bucket: "my-app-assets",
  region: "auto",
  endpoint: "https://<account-id>.r2.cloudflarestorage.com",
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
  publicBaseUrl: "https://assets.example.com",
});

The same StoragePort works with local files, memory tests, and S3-compatible object stores:

await ctx.ports.storage.put("avatars/user_123.png", avatarBytes, {
  contentType: "image/png",
  visibility: "public",
});

const object = await ctx.ports.storage.get("avatars/user_123.png");
const url = await ctx.ports.storage.publicUrl("avatars/user_123.png");

get(...) returns a one-shot body backed by the provider response. Consume one body method, or call await object.cancel() when only inspecting metadata so the unread response can release its resources. Prefer stat(...) for metadata-only reads.

Object keys and keyPrefix use the shared validation and prefix helpers from @beignet/core/ports, keeping S3 behavior aligned with memory, local, and Vercel Blob storage.

Direct upload signer

Use createS3UploadSigner(...) with @beignet/core/uploads when browsers should upload directly to S3 or an S3-compatible service:

import { createUploadRouter } from "@beignet/core/uploads";
import { createUploadRoute } from "@beignet/next";
import { createS3UploadSigner } from "@beignet/provider-storage-s3";
import { postUploads } from "@/features/posts/uploads";
import { getServer } from "@/server";

export const { POST } = createUploadRoute(async () => {
  const server = await getServer();

  return createUploadRouter({
    uploads: postUploads,
    ctx: () => server.createContextFromNext(),
    storage: server.ports.storage,
    signer: createS3UploadSigner({
      bucket: "my-app-assets",
      region: "auto",
      endpoint: "https://<account-id>.r2.cloudflarestorage.com",
      credentials: {
        accessKeyId: process.env.R2_ACCESS_KEY_ID!,
        secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
      },
      keyPrefix: "production",
    }),
  });
});

The signer returns presigned PUT URLs with the content type, cache control, and Beignet storage metadata headers the browser must send.

Visibility

visibility is stored as reserved object metadata so publicUrl(...) can return URLs only for objects written with visibility: "public". The provider does not set S3 ACLs. Configure bucket policies, R2 public buckets, or a CDN outside the provider when objects should be publicly reachable.

The reserved metadata key is beignet-visibility. It is hidden from StorageObject.metadata.

Escape hatch

The provider also installs ctx.ports.s3Storage for S3-specific operations that do not belong in StoragePort:

import { ListObjectsV2Command } from "@aws-sdk/client-s3";

const s3Key = ctx.ports.s3Storage.objectKey("exports/report.csv");
const s3Prefix = ctx.ports.s3Storage.objectPrefix("exports");

await ctx.ports.s3Storage.client.send(
  new ListObjectsV2Command({
    Bucket: ctx.ports.s3Storage.bucket,
    Prefix: s3Prefix,
  }),
);

const health = await ctx.ports.s3Storage.checkHealth();

Use objectKey(...) when direct S3 calls need to address objects written through ctx.ports.storage. Use objectPrefix(...) for list operations. Both helpers apply the configured STORAGE_S3_KEY_PREFIX. Use checkHealth() from app-owned readiness endpoints when the bucket policy allows HeadBucket.

Devtools

When ctx.ports.devtools is installed, the provider records storage operations under the storage watcher and direct upload signing under the uploads watcher. Events include operation name, key, bucket, duration, object size, visibility, and whether a lookup hit. Object bodies and metadata values are never recorded.

Failure behavior

The env-backed provider throws during startup when STORAGE_S3_BUCKET is missing. Storage operations surface AWS SDK or compatible-service errors after the SDK's built-in retries are exhausted. A missing object maps to null/false where the stable StoragePort expects that shape; unexpected service failures throw. checkHealth() returns a structured unhealthy result instead of throwing when the bucket cannot be reached or the credentials cannot perform HeadBucket.

Local and tests

Use @beignet/provider-storage-local, a memory/fake StoragePort, or a local S3-compatible service such as MinIO for tests. Use the direct factory with an injected client for provider adapter tests.

Deployment notes

Configure credentials through the runtime's normal secret mechanism or the AWS SDK credential chain. Set STORAGE_S3_KEY_PREFIX per app/environment when one bucket is shared, and configure bucket policy/CDN behavior outside Beignet for public objects.

License

MIT