What a storage host must support for this package, and a setup checklist for any of them.
The package talks plain S3 over HTTP, with its own small SigV4 signer. It has no code per provider.
So any host that supports the requests below should work. AWS S3, Cloudflare R2 and MinIO have
their own pages; this one is for everything else.
The bucket is the first path segment of the URL: https://<endpoint>/<bucket>. The package never
puts the bucket in the host name (virtual-hosted style). A host that only serves virtual-hosted URLs
works for a public bucket's reads, since those take any address, but not for publishing or for a
private bucket.
Signed requests use AWS Signature Version 4 with the service s3, the region you set, and an
x-amz-content-sha256 header holding the body's hash. The host name is part of the signature, so a
proxy in front must pass the Host header through unchanged.
The region is PREVIEW_S3_REGION for the CLI and region in the app. Both default to us-east-1.
Use whatever region your host expects; many accept us-east-1 or auto.
The two lists, index.json and index/<platform>/<key>.json, are read, changed and written back.
The write sends If-Match with the ETag it read, or If-None-Match: * when the file is new. If the
host answers 412 or 409, someone else wrote first: the CLI reads again and retries, up to 8 times.
That is what lets two publishes at the same time both end up listed.
If a host ignores those headers, publishing still works, but two publishes that overlap can lose
one of the entries: the later write wins. Files and manifests are not affected. Publishing one at a
time, as one CI job per pull request mostly does, avoids it. If an entry goes missing, publish that
preview again.
A public bucket needs anonymous GET on its objects. The app reads <url>/<key> with no
signature, so url can be any address the files are public under, such as a CDN.
A private bucket needs a read-only key. The app signs each GET with it, so url must be the real
<endpoint>/<bucket> address. Give the key ListBucket as well as GetObject if the host has
that distinction: without it, many hosts answer a missing file with 403 instead of 404, and a build
with nothing published yet shows an error instead of an empty list.
Missing files are expected: an app asks for its own build's list before anything is published for
it. The package treats 404 as "nothing yet", and also 403 on an unsigned read, since a public bucket
often answers that way.
CORS is a browser rule. The app's requests come from React Native's native networking on Android and
iOS, which does not apply it, and the package does not run on the web. The CLI runs in Node, which
does not apply it either. So the bucket needs no CORS settings.
Create a bucket. One bucket per app, or one shared bucket with a folder per app in the URL.
Make the write key. Read, write, delete and list on that bucket only. In S3 policy terms:
s3:GetObject, s3:PutObject, s3:DeleteObject and s3:ListBucket.
Choose public or private.
Public: allow anonymous read on the bucket's objects, and nothing else.
Private: make a read key with read and list only (s3:GetObject, s3:ListBucket).
Set the publish settings in CI secrets or a gitignored .env.local:
It prints the URL of the list it wrote. Open it, or curl it for a public bucket: you should see
storage-test. Then check the app lists it, and clean up with
npx preview unpublish storage-test and npx preview prune.
None of the hosts below has been run against this package. Each entry says what its own docs say
about the needs above, with links. Their docs are the source of truth for creating buckets, keys and
access rules, and for prices.
Settings:PREVIEW_S3_URL=http(s)://<host>:3900/<bucket>. Path-style is always on. Sign with
the s3_region from Garage's config (garage in its sample), not us-east-1
(configuration).
Public reads: Garage serves public buckets as websites on a separate endpoint
(exposing websites).
Anonymous reads on the S3 API are not documented, so a read key is the documented path.
Conditional PUT: not documented. The
S3 compatibility page
marks PutObject as implemented, with no word on If-Match or If-None-Match.
Conditional PUT: documented. Its
conditional operations page
says a read-modify-write cycle "that fails with 412 means another writer changed the object", and
that with If-None-Match: "*" the losing concurrent PUTs "return 412 Precondition Failed".
Settings:PREVIEW_S3_URL=https://<gateway>/<bucket>. Path-style is the form its
bucket operations use. The signing region
is not documented; use the zonegroup's API name if you set one.
Settings:PREVIEW_S3_URL=https://s3.<region>.backblazeb2.com/<bucket>, HTTPS only
(calling the S3 API). The
region is the part of the endpoint between s3. and .backblazeb2.com.
Keys: application keys can be limited to one bucket, read and write or read only
(application keys).
Settings:PREVIEW_S3_URL=https://<region>.digitaloceanspaces.com/<bucket>. Path-style is
supported (limits). Sign with
us-east-1; the endpoint picks the datacenter
(AWS SDKs).
Keys: per-bucket keys, read or read/write/delete
(manage access).
Public reads: files are private unless made public one by one
(file permissions).
The package uploads with no ACL, so plan on a private bucket with a read key.
Settings:PREVIEW_S3_URL=https://storage.googleapis.com/<bucket>
(request endpoints), with
HMAC keys of a service
account. Sign with the bucket's location
(signatures).
Keys: bucket-level IAM roles on the service account
(IAM permissions).
Public reads: Storage Object Viewer for allUsers on the bucket
(making data public).
Conditional PUT: not with the headers this package sends. The
request preconditions page says
If-Match and If-None-Match are "applicable for requests that retrieve data", and uses
x-goog-if-generation-match for writes, which the package does not send. So treat the lists as
unprotected and publish one at a time.