Publish a permanent link
chronozarr publish copies a store to cloud storage in your own account, checks that a browser can read it, and prints a viewer link. The link keeps working when your computer is off.
You need four things:
- A bucket, or an Azure container. The command does not create one.
- Credentials that can write to it.
- A public address for it: the bucket itself, readable by anyone, or a CDN or custom domain in front of it.
- A CORS rule that lets the viewer page read it.
--apply-corsadds the rule.
Anyone who has the link can read the store. Your provider charges you for the storage and for each download, and every view downloads part of the store. chronozarr.org serves the viewer page and stores no data. For data that must stay private, see Private stores. For a link that lasts only while your computer is on, see Share a temporary link.
Choose a provider
| Provider | Destination | Extra | Public address | Set up the bucket |
|---|---|---|---|---|
| Amazon S3 | s3://BUCKET/PREFIX | publish | https://BUCKET.s3.REGION.amazonaws.com/PREFIX, or a CloudFront address | Public bucket or with CloudFront |
| Cloudflare R2 | s3://BUCKET/PREFIX and --endpoint-url | publish | A custom domain, or the bucket's r2.dev address. Always given as --public-url | Cloudflare R2 |
| Google Cloud Storage | gs://BUCKET/PREFIX | publish-gcs | https://storage.googleapis.com/BUCKET/PREFIX | Google Cloud Storage |
| Azure Blob Storage | az://ACCOUNT/CONTAINER/PREFIX | publish-azure | https://ACCOUNT.blob.core.windows.net/CONTAINER/PREFIX | Azure Blob Storage |
For another static host, such as Source Cooperative or a university web server, see Hosting recipes.
Publish
-
Install the extra for your provider. With
uv tool install, list every extra that you use:uv tool install --python 3.13 'chronozarr[geo,publish]==0.4.0' -
Sign in to the provider. The command uses the provider's usual credentials:
Provider Credentials Amazon S3 aws configure,aws sso login, theAWS_*variables or an instance role.--profilepicks a profileCloudflare R2 An R2 API token as AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY, or a profile. A token with admin permission can also set CORSGoogle Cloud Storage gcloud auth application-default login, orGOOGLE_APPLICATION_CREDENTIALSAzure Blob Storage az login, a service principal inAZURE_CLIENT_ID,AZURE_TENANT_IDandAZURE_CLIENT_SECRET, orAZURE_STORAGE_CONNECTION_STRING -
Make the bucket readable. Follow the recipe for your provider in the table above.
publishnever changes who can read a bucket. -
Check the plan. A dry run prints what would be uploaded and what the prefix holds, and uploads nothing:
chronozarr publish my_store --destination s3://my-bucket/my_store-v1 --apply-cors --dry-run -
Publish:
chronozarr publish my_store --destination s3://my-bucket/my_store-v1 --apply-cors
Use a new prefix, such as my_store-v2, for every new version of a store. A prefix that holds other files is refused.
What you see
The command prints the plan, uploads, checks the public address as a browser would, and prints the link. A shortened run:
store my_store (conforms to chronozarr 0.3.0)
destination s3://my-bucket/my_store-v1 via AWS S3, region us-east-1
public URL https://my-bucket.s3.us-east-1.amazonaws.com/my_store-v1 (derived from the AWS S3 endpoint; pass --public-url for a CDN or custom domain; checked after the upload)
objects 51 objects, 46,149,689 bytes
...
chronozarr doctor https://my-bucket.s3.us-east-1.amazonaws.com/my_store-v1
[ ok ] root zarr.json 200, 17,214 bytes in 5 ms, chronozarr 0.3.0
[ ok ] CORS on zarr.json Access-Control-Allow-Origin allows https://chronozarr.org
...
[ ok ] decode level 0 cell (1,1) t=0 and t=5 in 27 ms; plain-Zarr pixel at t=0 matches
11 ok, 3 info, 0 warning(s), 0 failure(s)
added the viewer CORS rule after the bucket's 0 existing rule(s)
verified. Open the store in the viewer:
https://chronozarr.org/demo/?store=https%3A%2F%2Fmy-bucket.s3.us-east-1.amazonaws.com%2Fmy_store-v1Open the link, choose the view that you want to show, and click Copy link. The copied link also holds the date, the product and the map view. chronozarr link URL prints a link with a chosen product and date from the command line.
Provider examples
# Amazon S3 behind CloudFront: the bucket stays private, so give the distribution's address.
chronozarr publish my_store \
--destination s3://my-bucket/aoi/store-v1 \
--public-url https://d111111abcdef8.cloudfront.net/aoi/store-v1 \
--profile research
# Cloudflare R2. The endpoint is for uploads only, so --public-url is required.
chronozarr publish my_store \
--destination s3://my-bucket/aoi/store-v1 \
--endpoint-url https://<account id>.r2.cloudflarestorage.com \
--public-url https://data.example.com/aoi/store-v1
# Google Cloud Storage. The public address is https://storage.googleapis.com/my-bucket/aoi/store-v1.
chronozarr publish my_store --destination gs://my-bucket/aoi/store-v1 --apply-cors
# Azure Blob Storage: account, container, prefix.
chronozarr publish my_store --destination az://myaccount/stores/aoi/store-v1 --apply-cors--profile, --endpoint-url and --region are S3 options. The command rejects them with a gs:// or az:// destination.
The destination and the public URL
A published store has two addresses:
| Destination | Public URL | |
|---|---|---|
| Form | s3://BUCKET/PREFIX, gs://BUCKET/PREFIX or az://ACCOUNT/CONTAINER/PREFIX | https://HOST/PREFIX |
| Used by | the upload, with your credentials | browsers and the viewer link |
| Source | --destination | --public-url, or the bucket's own HTTPS address |
The bucket's own address serves objects only when the bucket allows anonymous reads. For a private bucket behind a CDN, pass the CDN's address as --public-url. R2 has no default: its storage endpoint needs credentials, so give the custom domain or the r2.dev address. The command never uses storage.cloud.google.com, because that host needs a Google sign-in and does not answer CORS.
A public URL that is an s3:// address, or that holds a login, a signed query or a fragment, is refused. It ends up in a link that other people open.
When a check fails
The objects stay in the bucket, no link is printed, and the exit status is 1. Fix the cause and run the same command again: it skips the objects already stored.
| What the checks say | Cause and fix |
|---|---|
root zarr.json ... HTTP 403 | The bucket is not readable by anyone. Follow the recipe for your provider, or pass a CDN address as --public-url |
root zarr.json ... HTTP 404 | The public URL does not point at the store. It must be the folder that holds the root zarr.json |
CORS on zarr.json and two more CORS lines fail | The bucket has no CORS rule for the viewer. Add --apply-cors. Behind a CDN, set CORS on the CDN |
validate fails with HTTP 403 | The host answers 403 for a file that does not exist. On S3, grant s3:ListBucket as in the recipes, so it answers 404 |
no credentials found, Unable to locate credentials, or access denied | The credentials are missing or cannot write to the bucket. See step 2 |
bucket ... was not found | Create the bucket first, or check its name and --endpoint-url |
--public-url is required | The destination is an R2 or other S3-compatible endpoint. Give the custom domain or r2.dev address |
chronozarr doctor URL runs the same checks at any time. Requirements and checks explains each one.
What the command does
- Validates the store, as
chronozarr validatedoes. A store that fails is not uploaded. - Prints the plan: destination, public URL, object count, bytes, the four upload phases and the cache headers.
- Lists the prefix. A prefix that holds only objects identical to the store (same size and MD5) is an interrupted upload, and the command resumes it. A prefix that holds any other object is refused. A store that you published and then appended to is the exception: use
--update(Publishing an append).--overwritereplaces objects whose content differs. It never deletes, and it leaves a one-year cached copy stale for readers that already loaded it. - Uploads in four phases: data chunks, then the time and volatility chunks, then the group and array
zarr.jsonfiles, then the rootzarr.json. A phase starts after the previous one is fully stored. If an upload fails, the command stops with no rootzarr.jsonin the bucket and names the failing key. boto3 and the Azure client retry each request up to five times. The Google client retries transient errors with its default policy. - Runs the doctor checks against the public URL, with
Origin: https://chronozarr.org. - Prints
https://chronozarr.org/demo/?store=<URL-encoded public URL>only when no check failed. Warnings are printed and do not block the link.
Cache headers follow the cache classes. Every zarr.json, each level's time/c/0 and the volatility chunks get public, max-age=300. Every other object gets public, max-age=31536000, immutable. The command does not mark the last shard of a sharded store as short-lived, so --update refuses a sharded append that rewrites a trailing shard.
CORS and public access
- The command reads the bucket's CORS rules only when the doctor reports a CORS failure. Without
--apply-corsit prints the rule the viewer needs and the number of rules that exist, and changes nothing. - With
--apply-corsit writes the existing rules unchanged, in their order, followed by the viewer rule, and runs the checks again. Putting the new rule last means that no request an existing rule answers today changes its answer. If an earlier rule already matches the viewer's requests without the headers it needs, the checks still fail, and you merge the rules by hand. - Google Cloud Storage keeps CORS on the bucket as a list of rules. The viewer rule is the one in Google Cloud Storage.
- Azure keeps CORS on the Blob service of the storage account. One rule list applies to every container in the account. Azure allows five rules, and the command refuses to write a sixth. Reading and writing the list needs the Storage Account Contributor role, or an account key in
AZURE_STORAGE_CONNECTION_STRING. - S3 CORS needs
s3:GetBucketCORSands3:PutBucketCORS. An R2 token with Object Read and Write cannot manage CORS. Use a token with admin permission, or set the rule with wrangler (Cloudflare R2). - Behind CloudFront, CORS comes from the response headers policy (Amazon S3 with CloudFront). The bucket's CORS rules are not what the checks see.
- The command never changes a bucket's public access settings, bucket policy, IAM bindings, custom domains or cache rules. Setting CORS does not make private objects readable.
Next
- Append timesteps to a published store, then
publish --update. - Embed the viewer in your own page.
- Advanced hosting: manual uploads, other hosts and the requirements behind each check.