DAVAUX
Alpha

@davaux/storage

Zero-dependency S3-compatible object storage client for Davaux. Uses hand-rolled AWS Signature V4 signing over fetch — no AWS SDK dependency. Works against any S3-compatible provider: Linode Object Storage, Cloudflare R2, MinIO, AWS S3.

Installation

npm install @davaux/storage

Basic setup

import { createS3Client } from '@davaux/storage'

const s3 = createS3Client({
  endpoint: process.env.S3_ENDPOINT!,       // hostname only, e.g. 'us-southeast-1.linodeobjects.com'
  region: process.env.S3_REGION!,           // e.g. 'us-southeast-1'
  bucket: process.env.S3_BUCKET!,
  accessKeyId: process.env.S3_ACCESS_KEY_ID!,
  secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
})

Config

OptionTypeDescription
endpointstringHostname only, no protocol — e.g. us-southeast-1.linodeobjects.com.
regionstringSigning region — e.g. us-southeast-1.
bucketstringBucket name.
accessKeyIdstringAccess key.
secretAccessKeystringSecret key.
publicUrlstringOptional CDN/custom domain base URL, used instead of the virtual-hosted-style https://{bucket}.{endpoint} URL when constructing object URLs.

Uploading objects

Buckets are private by default on most S3-compatible providers. Pass acl: 'public-read' for objects that should be directly viewable by browsers, such as avatars or banners:

// src/routes/api/upload.post.ts
import { defineAction } from 'davaux'
import { createS3Client } from '@davaux/storage'

const s3 = createS3Client({ /* ...config */ })

export const action = defineAction(async (ctx) => {
  const form = await ctx.form()
  const file = form.get('avatar') as File
  const buffer = Buffer.from(await file.arrayBuffer())

  const { key, url } = await s3.putObject(
    `avatars/123/${file.name}`,
    buffer,
    file.type,
    { acl: 'public-read' },
  )

  return { key, url }
})

putObject throws if the upload fails (non-2xx response), with the provider's status code and body in the error message.

Deleting objects

await s3.deleteObject('avatars/123/abc.png')

deleteObject treats a 404 response as success — deleting an object that's already gone is not an error.

Resolving public URLs

s3.getPublicUrl('avatars/123/abc.png')
// => 'https://my-bucket.us-southeast-1.linodeobjects.com/avatars/123/abc.png'

If publicUrl is set in the config (for a CDN or custom domain in front of the bucket), getPublicUrl uses that base instead:

const s3 = createS3Client({
  // ...
  publicUrl: 'https://cdn.example.com',
})

s3.getPublicUrl('avatars/123/abc.png')
// => 'https://cdn.example.com/avatars/123/abc.png'

S3Client API

MethodDescription
putObject(key, data, contentType, options?)Uploads a Buffer to key with the given content type. Returns { key, url }.
deleteObject(key)Deletes the object at key. No-op if it doesn't exist.
getPublicUrl(key)Synchronously resolves the public URL for key, without making a request.

PutObjectOptions

OptionTypeDescription
acl'private' | 'public-read'Canned ACL applied to the object. Omit to leave the bucket's default ACL in place.

Why no AWS SDK?

The AWS SDK pulls in a large dependency tree for functionality this package implements directly: SigV4 request signing via Node's built-in node:crypto, and uploads/deletes via fetch. This keeps the package dependency-free and works identically against any S3-compatible endpoint, not just AWS.