Background
`S3Client` constructor today: `(region?: string, profile?: string)`. The endpoint is implicitly `s3.<region>.amazonaws.com`. Every S3-API-compatible store needs a custom endpoint:
| Provider | Endpoint format |
|---|---|
| Cloudflare R2 | `https://<account-id>.r2.cloudflarestorage.com` |
| MinIO | self-hosted, arbitrary URL |
| DigitalOcean Spaces | `https://<region>.digitaloceanspaces.com` |
| Backblaze B2 | `https://s3.<region>.backblazeb2.com` |
| Wasabi | `https://s3.<region>.wasabisys.com` |
Without a way to override the endpoint, downstream packages can't ship adapters for any of these — they're all S3-API-compatible but the request URL needs to point elsewhere.
This blocks stacksjs/stacks#1896 (R2 driver). The framework's per-tenant scoping wrapper (`ScopedStorageAdapter`) already works with any S3-shaped adapter — only the endpoint config is missing.
Proposed API
new S3Client({ region: 'auto', endpoint: 'https://<account-id>.r2.cloudflarestorage.com', credentials })
// Or as a third positional arg if the constructor must stay positional-compatible:
new S3Client(region, profile, { endpoint })The signing logic stays unchanged — SigV4 against the configured endpoint host. Apps using the default AWS endpoint see no behavior change.
Acceptance
S3Clientaccepts a customendpointfield (constructor or options)- When provided, the endpoint host wins over the default AWS-derived URL for all requests (getObject, putObject, multipart, presigned URLs)
- SigV4 signing uses the host from the configured endpoint, not the derived AWS host
- Path-style addressing supported for MinIO-shaped endpoints (where buckets aren't in the subdomain)
- Tests covering an R2-shaped endpoint round-trip + a path-style MinIO-shaped endpoint round-trip
Once this lands, the Stacks-side R2 driver is ~30 lines extending S3StorageAdapter with the right config defaults.