Serving a static site straight from an object bucket: what breaks
Bucket website hosting needs an index document, MIME handling, a custom domain and CORS. Here's what an object API gives you, and what Infrai refuses outright.
Sometimes, and not here. A bucket with website hosting — Amazon S3’s website endpoint, Google Cloud Storage, Azure Blob’s static website feature — will serve index.html and a 404 page from a prefix, and paired with a CDN that’s a legitimate way to run a site with no web server. Infrai’s storage API is not one of those buckets: it refuses HTML and JavaScript uploads outright with STORAGE_CONTENT_TYPE_BLOCKED, has no website endpoint, no index-document behaviour and no CORS configuration, so the answer for this API specifically is a flat no.
That’s worth knowing in one call rather than three hours in. If you’re here to host a site, skip to the table below and pick one of the real options; if you’re here because your app also produces files that a static site references, the second half is the part that pays.
The upload the API rejects
Try it and the refusal is immediate and specific:
export INFRAI_API_KEY="your_infrai_api_key"
BODY=$(python3 -c 'import base64, json; print(json.dumps({"data_base64": base64.b64encode(open("index.html","rb").read()).decode(), "content_type": "text/html"}))')
curl -sS -X PUT "https://api.infrai.cc/v1/storage/object/put/kb-site-0726/index.html" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d "$BODY"
{
"ok": false,
"error": {
"code": "STORAGE_CONTENT_TYPE_BLOCKED",
"http_status": 415,
"message": "content-type 'text/html' is not allowed (active/executable)",
"retryable": false
}
}
text/javascript, application/javascript and application/xhtml+xml come back the same way. Passive types are fine — CSS, PNG, SVG, WOFF2, WASM, JSON and plain text all upload without complaint — which tells you the policy exactly: this is an asset store that declines to host executable documents, on purpose, because a bucket serving arbitrary user HTML from a shared hostname is a phishing kit waiting to happen.
What bucket hosting needs that an object API doesn’t have
Even where uploads are allowed, “just dump the files in” quietly assumes six behaviours:
- an index document, so
/docs/serves/docs/index.htmlrather than a directory listing or a 404; - an error document, so a client-side router doesn’t hand users raw XML;
- content types stored per object, or every file downloads instead of rendering;
- a custom domain with TLS, which on most stores means a CDN in front, not the bucket itself;
- CORS rules, the moment a font or an XHR call crosses an origin;
- cache headers, or you pay for every reload and can’t invalidate a bad deploy.
On this API the first two don’t exist — a request to a bucket prefix root returns a 404, not an index — and CORS is the sharpest edge of the lot. Bucket create accepts a cors_rules field and drops it: read the bucket back and the array is always empty, and a preflight OPTIONS against a presigned URL answers 403 with no CORS headers at all. Browser-direct requests to these buckets don’t work today, which rules out the XHR half of a static app even if the HTML lived somewhere else.
Where to actually host it
| Option | Index/error docs | Custom domain + TLS | Best for |
|---|---|---|---|
| Cloudflare R2 + Pages | Yes, via Pages | Included | Sites with zero-egress economics and a Git deploy |
| Amazon S3 website endpoint + CloudFront | Yes | Via CloudFront | Teams already inside AWS |
| Google Cloud Storage website config | Yes | Via load balancer | GCP-native stacks |
| Azure Blob static websites | Yes | Via CDN | Azure-native stacks |
| MinIO on your own box + nginx or a CDN | You configure it | You configure it | On-prem or air-gapped sites |
| Infrai storage | No | No | Not this; use it for the assets a site links to |
For a plain marketing or docs site, Cloudflare Pages is the least work of any of these — a repo, a build command, done. That’s not a grudging admission; it’s the right recommendation, and no object API competes with it on this job.
What the bucket is genuinely good for here
Build artefacts and downloadable assets, which is a real part of a static-site workflow. Publish the tarball your CI produced, keep the last N releases, hand a deploy target a short-lived link:
import { readFile } from "node:fs/promises";
import process from "node:process";
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const BUCKET = "kb-site-0726";
const sha = process.env.GIT_SHA ?? "unknown";
const key = `releases/${new Date().toISOString().slice(0, 10)}/site-dist.tar.gz`;
const bytes = await readFile("dist.tar.gz");
const payload = {
data_base64: bytes.toString("base64"),
content_type: "application/gzip",
metadata: { "git-sha": sha },
};
const put = await fetch(`${API}/v1/storage/object/put/${BUCKET}/${key}`, {
method: "PUT",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!put.ok) throw new Error(`upload failed: ${put.status} ${await put.text()}`);
const stored = (await put.json()).data;
console.log(`stored ${stored.key} (${stored.size_bytes} bytes, etag ${stored.etag})`);
Metadata keys must be hyphenated — git-sha works, git_sha breaks the upstream signature and comes back as a 503. That one costs people an afternoon.
Confirm what landed, without paying for a download:
curl -sS "https://api.infrai.cc/v1/storage/object/head/kb-site-0726/releases/2026-07-26/site-dist.tar.gz" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"found": true,
"status": "found",
"key": "releases/2026-07-26/site-dist.tar.gz",
"size_bytes": 12,
"etag": "623eaa3daf5d08b15c263ab39cbb8e64",
"content_type": "application/gzip",
"metadata": { "git-sha": "a1b2c3d" },
"last_modified": "2026-07-26T00:56:27Z"
}
}
And to see the whole shelf of releases, a free listing:
curl -sS "https://api.infrai.cc/v1/storage/object/list/kb-site-0726?prefix=releases/&limit=50" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
One security caveat before you point a site at these URLs
Objects in a bucket are fetchable by anyone who has the full storage URL — strip the signature off a presigned link and the object still returns 200. So a presigned URL here is an expiring convenience, not an access control: fine for a CSS file or a public download, wrong for anything you’d call private unless the key itself is unguessable and server-derived. If your site needs genuinely private assets behind a login, put your own handler in front and don’t hand out storage URLs at all.
The region field has a matching caveat — a bucket created as eu-central-1 reported that back while its signed URLs pointed at an ap-singapore host, so don’t treat it as a residency guarantee for anything a site serves to EU users.
What this costs
Free, mostly: bucket create, listing, head, presign and lifecycle rules are free and rate-limited. Writes are billable per call — verified 26 July 2026 at $0.0001 per object/put — so a 40-file asset drop costs well under a cent in call charges, plus stored bytes. There’s no per-GB egress line item on the call itself, but bandwidth limits apply and heavy public traffic is exactly what a CDN is for. Read today’s rates rather than trusting a page:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; [print(c['id'], c['billing'].get('price_usd', 0)) for c in json.load(sys.stdin)['capabilities'] if c['id'].startswith('storage.object')]"
New accounts get $2 of trial credit. The honest summary: host the site on a static host built for it, and keep the artefacts, exports and user files it links to on an API where the same credential also runs your cron, queue and outbound mail.