#2872·instant

S3_PUBLIC_ENDPOINT forces path-style presigned URLs, breaking Cloudflare R2 custom domains

Author: rzkyydsCreated Aug 22, 2026Updated Aug 22, 2026

Summary

When self-hosting InstantDB with Cloudflare R2 as the S3 backend, presigned URLs for Storage ($files) are always generated in path-style (https:////). Cloudflare R2 custom domains do not support presigned URLs at all (Cloudflare docs: "Presigned URLs work with the S3 API domain and cannot be used with custom domains"), and R2 custom domains serve the bucket at the root (/), so any URL that includes the bucket name in the path returns 404 NoSuchKey.

Current State

  • config/s3-public-endpoint + presign path (server/src/instant/storage/s3.clj:156-158) force :path-style? true whenever S3_PUBLIC_ENDPOINT is set.
  • server/src/instant/util/aws_signature.clj (presign-s3-url, lines 288-337) builds url-path as "/" + bucket + path when path-style? is true.
  • Result for R2 custom domain (files.example.com): https://files.example.com////?X-Amz-... → R2 looks up object //... → 404 (bucket is implicit at root on R2 custom domains).
  • For R2 public dev URL (*.r2.dev), the same path-style URL also 404s, and additionally the host serves a certificate mismatch (SEC_E_WRONG_PRINCIPAL/ERR_CERT_COMMON_NAME_INVALID) on the current pub-.r2.dev hostname.

Workaround

  • MinIO (self-hosted) works because MinIO supports path-style + presigned URLs (the official VPS guide). Set S3_ENDPOINT=http://minio:9000, S3_PUBLIC_ENDPOINT=https://files.example.com.
  • Backblaze B2 also works (path-style + presigned supported), no custom-domain requirement for the API.
  • R2 users can only use the S3 API domain (https://<ACCOUNT_ID>.r2.cloudflarestorage.com) as S3_ENDPOINT, but cannot get clean custom-domain URLs without a Cloudflare Worker (which must also handle presigning, since R2 custom domains reject SigV4).

Where They Break

  • Any deployment that sets S3_PUBLIC_ENDPOINT to a host that does not accept path-style URLs (R2 custom domain, R2 .r2.dev dev URL, and any S3-compatible storage that only supports virtual-hosted style).
  • There is no flag to opt out of path-style; it's hardcoded when S3_PUBLIC_ENDPOINT is present (s3.clj:156-158).
  • Also affects PUT presigned uploads (browser uploads to $files), not just GET.

The Ask

  1. Add S3_VIRTUAL_HOST_STYLE opt-out env (path-style → virtual-hosted) di presign-creds + S3 client config.
  2. Document R2 caveat: custom domain + presigned URLs not supported (Cloudflare); supported path = S3 API domain.