Examples

Static Site on a CDN

Last updated July 2026

The most interesting thing about this architecture is what it does not contain. There is no container and nothing running between requests. Two components hold a website: a bucket with the built files, and a CDN that serves them. This page explains how those two do the job a server usually does, and when that stops being enough.


The Architecture

ComponentWhat it does
siteA private bucket holding your built files
cdnEdge caching and HTTPS
domainA domain you have verified, and the hostname it serves
{
  "version": "2.0",
  "platform": "aws",
  "components": [
    {
      "type": "Bucket",
      "name": "site",
      "source": {
        "git": {
          "url": "github.com/you/site",
          "ref": "main",
          "commands": [
            "npm ci",
            "npm run build"
          ],
          "directory": "dist"
        }
      }
    },
    { "type": "CDN", "name": "cdn" },
    { "type": "ImportedDomain", "name": "domain",
      "zone_name": "example.com", "zone_id": "Z0EXAMPLE123456" }
  ],
  "connections": [
    { "from": "domain", "to": "cdn", "host": "example.com" },
    { "from": "cdn", "to": "site" }
  ]
}

Two components and one connection is the whole architecture. Nothing here runs on a machine you are paying for by the hour.


The Wiring

The bucket carries a build pipeline, which is the part people do not expect. Giving it a git source tells the platform to clone the repository, run commands at the repository root, and then sync one directory of the result into the bucket. A bucket source takes no context: the build always runs at the root, which is where it differs from the zip build in Scheduled Jobs. directory names that output folder: dist for Vite, build for Create React App, out for a Next.js static export. The two halves are independent, so a repository that already contains finished files can drop commands entirely and be synced as it stands. The sync runs on each apply, which means publishing is a change to the repository rather than an upload.

The single connection is what keeps the bucket private. cdn → site makes the bucket the CDN’s origin and grants read access to exactly one reader, the CDN itself. The bucket stays closed to the public and encrypted, and there is no second door into it. That is the opposite of classic bucket website hosting, where the bucket is made public and the CDN is an optional accelerator in front of it. Here the CDN is not an accelerator, it is the only way in, and HTTPS is terminated there.

What runs between requests is nothing at all. A request reaches an edge location, is answered from cache if the file is there, and otherwise fetched once from the bucket and cached for the next visitor. There is no process to keep warm, no instance to scale, and no origin to overload during a traffic spike, because the origin is object storage rather than a server.

domain → cdn is the front door. It writes an A-alias record for example.com into the hosted zone the ImportedDomain names and hands the distribution that zone’s certificate, so the bucket stays private and the CDN is the only thing the hostname resolves to. The zone has to be verified before this applies; drop domain and its connection and the site answers on the distribution’s own address instead.


The Decisions Behind It

The build belongs in the infrastructure file. You could build the site in your own CI and push the artifacts, treating the bucket as a dumb target. Putting commands in the schema instead means the build and the thing it produces are versioned together, and a reader of the file can see how the site is made without opening a separate pipeline definition.

Private bucket, public CDN. Serving straight from a public bucket is fewer moving parts, and it is the wrong trade. It gives every object a second, uncached, unprotected URL, and it makes correctness depend on a policy you have to keep right. Routing everything through the distribution removes the alternative path entirely.

Static until it genuinely cannot be. Reaching for a container early is the most common way this architecture gets abandoned before it has paid for itself. Anything that can be resolved at build time, including content, routing, and most personalisation that can happen in the browser, keeps you here. What forces a move is server-rendered HTML per request, an API of your own, or authentication that has to be enforced somewhere other than a client.

There is no server in this file. Nothing to patch, nothing to restart, and nothing that costs you anything while no one is visiting.


The Limits

The frontier is dynamic behaviour. As soon as a page has to be composed per request, this architecture cannot express it, and the work moves to The Containerized Web App. The common middle ground is to keep both: the marketing site stays here on the CDN while the application runs as a container on its own hostname, in the same project.

There are smaller edges. The CDN serves index.html at the root through default_root_object, and caches in the cheapest set of edge regions unless you change price_class. A single-page app that relies on client-side routing needs its own handling of unknown paths, since the CDN will look for a matching object first. And because the bucket is written by a sync rather than a running process, anything your site needs to write at runtime, such as form submissions, belongs somewhere else entirely: a function with its own hostname is usually the smallest answer.

    Static Site on a CDN: Bucket and Edge Caching | Spawned