← Blog · Pattern Library · Play the Sim

Static Files ARE the API: How to Build a Backend With Zero Servers

kody-w · April 2025 · Architecture Infrastructure Pattern

Every project starts the same way. You have data. You need to serve it. So you spin up an Express server, connect it to a database, add auth middleware, deploy to a cloud provider, configure SSL, set up monitoring, and now you have ops.

You didn't want ops. You wanted to serve JSON.

What if you didn't need a server at all?

The Problem

APIs need servers. Servers need ops. Ops need money. The standard architecture for serving data is:

Client → Load Balancer → Server Process → Database ↓ Auth / Rate Limit / Logging ↓ Response (200 OK, JSON body) Requirements: ✗ Server running 24/7 ($5-500/month) ✗ Database with backups ($5-100/month) ✗ SSL certificate management ✗ Monitoring and alerting ✗ Scaling under load ✗ Security patches ✗ Someone on-call when it breaks

This is insane for data that changes once every few minutes. You're paying for a 24/7 server to respond to requests for data that was last updated an hour ago. It's like hiring a librarian to stand next to a book in case someone wants to read it.

The Solution: Static Files on Git

Here's the insight: if your data changes less than once per minute, you don't need a server. You need a file host.

Engine generates data → writes JSON → git commit → git push ↓ Client → CDN → Static JSON file ← raw.githubusercontent.com ↑ (or GitHub Pages) └── polls every 30s ────────────────────┘ Requirements: ✓ Git repository (free) ✓ GitHub Pages or raw CDN (free) ✓ Nothing else

The git repo IS the database. The CDN IS the server. The commit log IS the audit trail. Pushing a JSON file IS deploying the API.

The Three Endpoints

A Static API needs exactly three files:

1. frames.json — The Data Bundle

All your data in a single JSON file. For the Mars sim, this is every frame — environmental readings, colony state, scores, events. One GET request, all data.

// GET https://kody-w.github.io/mars-barn-opus/data/frames.json
[
  {"sol": 1, "temp": -60.2, "dust": 0.31, "power": 800, "alive": true, ...},
  {"sol": 2, "temp": -58.7, "dust": 0.35, "power": 785, "alive": true, ...},
  // ... 500 frames, ~200KB total
]

2. latest.json — The Heartbeat

A tiny file (< 1KB) that tells clients whether anything changed. Poll this. If it hasn't changed, don't fetch the big bundle.

// GET https://kody-w.github.io/mars-barn-opus/data/latest.json
{
  "sol": 147,
  "timestamp": "2025-04-01T12:00:00Z",
  "frameCount": 147,
  "hash": "a1b2c3d4e5f6...",
  "engine": "mars-barn-engine-v2"
}

3. manifest.json — The Index

Lists all files, their sizes, and their hashes. Enables tooling, discovery, and federation between repos.

// GET https://kody-w.github.io/mars-barn-opus/data/manifest.json
{
  "spec": "static-api",
  "files": [
    {"path": "data/frames.json", "size": 245000, "hash": "a1b2c3d4..."},
    {"path": "data/latest.json", "size": 180, "hash": "e5f6a7b8..."}
  ],
  "federation": [
    {"repo": "other-user/mars-colony-2", "url": "https://..."}
  ]
}

How the Mars Sim Does It

The Mars Barn Opus uses exactly this pattern. Here's the flow:

  1. Engine runs: The simulation engine generates a new frame for each sol. Environmental data computed from NASA climate models. Colony state computed from player/agent decisions.
  2. Bundle rebuilt: The engine rebuilds frames.json with the new frame appended, computes the SHA-256 hash, and updates latest.json and manifest.json.
  3. Commit and push: All three files committed atomically. One commit = one API update.
  4. CDN serves: GitHub Pages (or raw.githubusercontent.com) serves the new files. Cache TTL expires within minutes.
  5. Clients poll: The viewer, SimHub, leaderboard, and AI agents all poll latest.json. When the hash changes, they fetch the new bundle and diff for new frames.

The result: a full API serving structured sim data to multiple consumers, with zero servers, zero cost, zero ops.

How Clients Consume

The client pattern is simple: poll, diff, process.

const LATEST = 'https://kody-w.github.io/mars-barn-opus/data/latest.json';
const FRAMES = 'https://kody-w.github.io/mars-barn-opus/data/frames.json';
let cache = JSON.parse(localStorage.getItem('mars-cache') || '{}');

async function poll() {
  const latest = await fetch(LATEST).then(r => r.json());
  if (latest.hash !== cache.hash) {
    const frames = await fetch(FRAMES).then(r => r.json());
    const newFrames = frames.filter(f => f.sol > (cache.lastSol || 0));
    processNewFrames(newFrames);
    cache = { hash: latest.hash, lastSol: latest.sol };
    localStorage.setItem('mars-cache', JSON.stringify(cache));
  }
}

setInterval(poll, 30000); // every 30 seconds

Key details:

How Engines Produce

The engine side is a pipeline: generate → bundle → commit → push.

# Engine pipeline (runs on any machine, CI/CD, or cron)
python3 engine/generate_frame.py --sol 148
python3 engine/rebuild_bundle.py   # rebuilds frames.json, latest.json, manifest.json
git add data/frames.json data/latest.json data/manifest.json
git commit -m "sol 148: dust storm peaks, power at 45%"
git push origin main

This can run anywhere: your laptop, a GitHub Action, a Raspberry Pi, a cron job on a $5 VPS. The engine doesn't serve traffic. It just pushes JSON. The CDN does the serving.

Performance: Better Than You Think

MetricTraditional APIStatic API
Latency (cache hit)50-200ms5-20ms (CDN edge)
Latency (cache miss)100-500ms50-100ms (CDN origin)
Concurrent readersLimited by serverUnlimited (CDN)
Cost per million reads$0.50-5.00$0.00 (GitHub Pages)
Uptime99.9% if you're good99.99% (GitHub's SLA)
Time to deployMinutes (CI/CD)Seconds (git push)
Time to set upHours-daysMinutes

The CDN edge-caches your files globally. A reader in Tokyo gets the same latency as a reader in San Francisco. Try getting that from your Express server without a CDN layer you have to configure yourself.

The Critical Anti-Pattern: Don't Use the GitHub API

⚠ WARNING: RATE LIMITS

The GitHub REST API has strict rate limits:

If you're polling every 30 seconds, you burn 120 requests/hour per client. Two unauthenticated clients and you're rate-limited.

Solution: Use raw URLs or GitHub Pages. These serve static files directly from the CDN with no rate limit on GET requests.

// ✗ BAD: Uses GitHub API (rate limited)
https://api.github.com/repos/kody-w/mars-barn-opus/contents/data/frames.json

// ✓ GOOD: Raw URL (no rate limit)
https://raw.githubusercontent.com/kody-w/mars-barn-opus/main/data/frames.json

// ✓ BEST: GitHub Pages (no rate limit, custom domain, custom cache headers)
https://kody-w.github.io/mars-barn-opus/data/frames.json

Applying It to Any Domain

The pattern isn't Mars-specific. Any system where data changes infrequently and reads outnumber writes can use Static APIs:

Federation: Repos as API Instances

Here's where it gets interesting. Each git repo is an independent API instance. Federation is just discovery:

Repo A (mars-barn-opus) Repo B (mars-colony-2) Repo C (mars-lab) └── manifest.json ──────┐ └── manifest.json ──────┐ └── manifest.json ↓ ↓ ↓ ┌─────────────────────────────────────────────┐ │ Federation Crawler │ │ Reads manifests → builds directory │ │ Publishes: federation-index.json │ └─────────────────────────────────────────────┘ ↓ Clients discover all API instances from one URL

This is RSS for structured data. Each repo is a feed. Crawlers discover feeds. No central server. The whole network is static files discovering other static files.

Security Without a Server

No server means no server-side security. Instead, security is cryptographic and verifiable:

You don't need a server to have integrity. You need math.

When NOT to Use This

Static APIs are not a universal replacement. Don't use them when:

The question isn't "can I use a Static API?" It's "does my data change slowly enough that I can?" For a shocking number of projects, the answer is yes.

The Key Insight

The frame IS the API call.
The repo IS the server.
Git IS the database.
Push IS deploy.

We've been so conditioned to "build a backend" that we forget what a backend does: it stores data and serves it when asked. If you can store data in git and serve it via CDN, you have a backend. It's just not a server. It's a file.

The Mars Barn Opus serves its entire simulation state — 500+ frames of environmental data, colony state, scores, and chain blocks — as three static JSON files in a git repo. Zero servers. Zero cost. Multiple consumers polling simultaneously. It's been running for months.

If your data changes less than once per minute, you don't need a server. You need a push.


The frame is the API call. The repo is the server. Git is the database. Static files are the API. Everything else is ops you don't need.