● Atlas Commons · Infrastructure Decision

How to run this, forever

A persistent VPS? A container with block storage? Modal? The honest answer: there are three different problems wearing one coat — where state lives, where compute runs, where the control plane lives. Solve each with the cheapest thing that fits.

The actual problem

Atlas Commons is a stateful batch pipeline running on stateless ephemeral compute (GitHub Actions runners that are born, run once, and die). That single mismatch causes everything:

1 · Input state has nowhere to live. The singulariki atlas was built on a curated 4.6 GB local corpus (data/staging/ — O*NET dumps, BLS workbooks, crosswalks, Lightcast, academic datasets). It's git-ignored (correctly — big + partly licensed). Ephemeral runners don't have it → FileNotFoundError. This is a state-location problem, not a compute problem.

2 · Re-fetch cost & fragility. Even URL-fetched assets re-download every run (nothing persists). Today we landed O*NET — 46 tables — but it re-pulls the whole O*NET DB each time.

3 · No always-on control plane. No live Dagster UI, schedules, or asset-freshness memory. A cron prompt pokes a workflow. Fine for a sprint; thin for a forever-system.

Decouple (A) where state lives · (B) where compute runs · (C) where the control plane lives. Mix vendors freely across the three. The trap is buying a server to fix (1) when (1) is about where the bytes live, not where the CPU is.

The options, honestly

0 · GitHub Actions keeptoday

strengths$0 on your credits · zero infra · truly isolated (one atlas can't OOM another) · parallel · already proven across 6 atlases
limitsephemeral disk · ~6h/job · ~7GB RAM standard (16/32/64GB larger = paid) · ~90s cold sync · no UI

An excellent executor. Not a state store, not a control plane. Keep it — just stop asking it to hold state.

1 · R2 as the staging layer do this first

Upload the 4.6 GB corpus to a private atlas-staging R2 bucket; re-point bronze readers from Path("data/staging/…") to an R2 read.

strengthsdecouples state from disk → any runner works, GitHub stays $0 · egress-free to CF compute · one source of truth · storage ≈ $0.07/month
costsre-point ~8 source modules (mechanical) · a few files licensed → a private bucket for the operator's own processing is the defensible posture

Not a compute option — the keystone fix. The one change that unblocks singulariki on the free infra you already have. Do it regardless of everything else.

2 · Persistent VPS + block storage your instinct

Always-on Linux box (Hetzner/DO/Linode) + attached volume; Dagster daemon + UI resident; corpus on the volume.

strengthspersistent disk (no re-fetch) · always-on UI + schedules + sensors · big RAM cheap (Hetzner ~€15–40/mo for 16–32GB) · full control
risksit's a pet — you patch/secure/monitor/back-up it · falls over at 3am · single point of failure · you become a part-time sysadmin

Best only if you want an always-on control plane + a heavy box to dev on. For a cron loop it's more than needed — but it's the most tangible "runs itself" upgrade. If you go here, do it as a self-hosted runner (Option 6), not a parallel install.

3 · Cloudflare Containers watch

strengthsegress-free + lowest latency to R2 · stays in your one ecosystem · scales
risksruntime/duration ceilings vs multi-hour batch · block storage is R2/DO not POSIX · Dagster-in-CF-container for long batch unproven

Compelling for serving + light/medium jobs next to the data. Don't make it the heavy-batch executor yet — re-verify limits.

4 · Modal — serverless + persistent Volumes elegant path

Serverless Python with persistent Volumes mounted into ephemeral runs, scale-to-zero billing, huge on-demand memory.

strengthsa Volume holds the 4.6 GB corpus without R2 re-architecture and without a server (cattle, not pet) · pay-per-second, scale to zero · 100s of GB RAM on demand for FJC/NFIP/GLEIF/HMDA · Python-native · free monthly credits
risksnew vendor + learning curve · it's executor+state, not a UI (still need a control plane) · some lock-in

If the priority is persistent state + serverless + no ops, arguably more optimal than a VPS — the persistent volume without the pet. Strongest alternative to "GitHub + R2 staging."

5 · Dagster+ (managed control plane) when mature

strengthsreal UI · run history · schedules/sensors · asset lineage + freshness · alerting · Hybrid keeps compute/data in your infra
costs~$10/mo Solo+ · still only the plane (bring your executor)

The control-plane answer once you want to see the estate breathe instead of reading it from a spec. Pairs with any executor.

6 · Self-hosted GitHub runner on a box hybrid sweet spot

strengthskeep the exact workflow/secrets you have, but run on a box with a persistent volume (corpus stays) + big RAM (no cap) · GitHub as control, your box as muscle
risksstill own/secure the box · self-hosted runners + public repos = footgun (fine for solo/private, but lock down)

The most pragmatic way to get persistence + power without rewriting anything. If you want a server, make it a runner.

7 · AWS/GCP batch avoid

The data's home is R2. Compute in AWS/GCP writing to R2 means cross-cloud egress on every materialization — the one architecture that turns a $0.07/mo bill into a real one. Stay in Cloudflare + GitHub + (Modal, which peers cheaply).

The recommendation

Tier 1 — now · ~$0 · unblocks singulariki this week

  1. Stage to R2. Create atlas-staging, upload the corpus once (wrangler/rclone), re-point the ~8 staging bronze modules.
  2. Keep GitHub Actions as the executor (works, isolated, free).
  3. Larger runners for the heavy assets (FJC, NFIP-full, GLEIF-full, HMDA loan-level) — you have the credits; the workflow already takes a runner input.

Tier 2 — when you want it to watch itself · ~$10–40/mo

  1. Dagster+ Solo (Hybrid) for a real control plane, or a small persistent VPS as a self-hosted runner if you'd rather own the muscle + host the corpus. Pick by temperament: managed-and-observable vs owned-and-powerful.

Tier 3 — elegant alternative to Tier 1's re-pointing

  1. Modal — mount a persistent Volume instead of re-architecting to R2, with serverless heavy-memory and zero servers to babysit.

The singulariki question, answered

Move the source location; do not redo the parse logic. The bronze parse code is good; silver/gold are unaffected (they read bronze from the catalog, not disk). Only ~8 modules read data/staging/….

O*NET — already URL-fetched; 46 tables landed today in singulariki_bronze on a plain runner in 4m19s. Proves the path.
Staging-bound modules — add one helper, staging_open(relpath), that resolves to a local path if present else an R2 GET from atlas-staging. Re-point ~25 path constants through it. Runs both locally and remotely; no parse changes; no silver/gold changes.
Then materialize in layers (bronze light→heavy, silver, gold) — the pattern that just worked for O*NET. Same indirection serves money-pipelines and any future staging atlas.

The 4.6 GB upload is a one-time push (files are local, wrangler is authed — I can run it). The one thing I'll confirm with you first: whether any specific licensed file is okay in even a private bucket.

What it unlocks

Once staging lives in R2, "the atlases I can build remotely" and "the atlas that only built on Ryan's laptop" become the same thing. All eight atlases — plus singulariki's 172 tables and money-pipelines' 74 — become rebuildable by any runner, anywhere, into one catalog, queryable by one SQL surface, for about seven cents a month of input storage. That's the difference between a pipeline that ran once on a particular machine and a public utility that regenerates itself on cheap, disposable compute — the whole thesis. The compute can be GitHub today, Modal tomorrow, a VPS if you want a pet; the architecture stops caring, because the state finally lives in the one place that outlives every executor: R2.

Written 2026-06-19 · re-verify vendor limits/prices against current docs before spend · the recommendation holds regardless of exact numbers
Full doc: .specs/atlas-commons/RUNTIME-ARCHITECTURE.md · companion: the status dashboard →