Serving static sites from R2 and a database
The fast path behind app.nz static deploys: upload changed files, prune removed paths, index them in the database, and serve bytes directly from R2 or local storage.
Listen to this article
On-device voiceUses the voice built into your browser; no article text leaves this page.
Audio narration is not supported by this browser.
The fastest app.nz deploy path is static hosting. There is no builder, no scheduler, and no container boot. The CLI uploads a directory, the control plane records the file index, and the request path serves bytes through the Go server.
That sounds plain, but it is where a lot of "deploy speed" comes from. A deploy that does not need compute should not allocate compute.
The data model
A hosted site is a row plus a set of files. The row owns the slug, title, user, and timestamps. The files table owns path, content type, size, hash, and storage key. When R2 is configured, the bytes live in the appnz-sites bucket. In local development and tests, the database path still works without a cloud object store.
That split gives the deploy path two jobs:
- write changed bytes to storage,
- make the database index match the directory the user wanted live.
The file index is the important part. Serving a request should not list a bucket, guess content types, or scan a directory. It should resolve one row and fetch one object.
Diff, upload, prune
Static deploys are stateful. If the local directory no longer contains old.js, old.js should stop being public. That is why the deploy command sends the desired file set and the server prunes files that are no longer present unless the user explicitly asks for a partial deploy.
The happy path is:
walk local directory skip hidden files and ignored substrings upload changed files upsert hosted_site_files rows delete rows for removed paths serve the new index immediately
This is why static deploys can feel instant. The platform is not rebuilding the bundle. The bundle already exists.
Two URL shapes
app.nz serves a site in two ways:
- path form: /sites/<slug>/
- origin form: <slug>.app.nz
The origin form is the production shape because it gives the site its own browser origin. The path form is useful for previews and quick sharing, but it lives under the app.nz origin, so it is treated more carefully.
For path-hosted sites the server applies sandbox headers. The goal is to let a generated frontend run without letting it act like the main app.nz application. A static-site host that shares origin with the dashboard has to assume the page content is not fully trusted.
SPA fallback
Most generated frontends are single-page apps. If a request asks for /settings and there is no settings file, the server falls back to index.html when the site has one. Asset requests still behave like asset requests: missing .js, .css, .png, and similar paths return 404 instead of index.html.
That distinction matters for debugging. A missing JavaScript chunk should not render the app shell and hide the real error.
Caching
Static hosting has two caching goals that pull in opposite directions:
- HTML should update quickly after deploy.
- fingerprinted assets should be cacheable.
The server can infer content type and set conservative headers by path. The long-term direction is to let the deploy index understand fingerprints and cache policies directly, but the core contract remains the same: request path to indexed file to bytes.
Why this is app infrastructure, not just file upload
Hosted sites are the end of many app.nz workflows. Coding agents produce frontends. Build jobs produce assets. Demos, docs, and generated tools need public URLs. Static hosting is the low-friction landing zone for all of them.
The engineering principle is to keep the path short. Static output should not be forced through the container runtime just because containers exist. If the artifact is a directory, deploy the directory.