PLUTUS · ENGINE ROOM // PAPER ONLY · NO LIVE CAPITAL
ENGINEERED BY LEOPARD DATA

Local First, Cloud at Gate 4

Infrastructure is deployed when a gate requires it, not before. Three phases run on a laptop with one storage account; the estate arrives when there is a daemon that has to stay awake.

The rule

The design document says "infrastructure-as-code from day 1." That means the estate is code from day one — which it is, about 900 lines of Pulumi — not that it is running from day one. That is the payoff of IaC: the spend can be deferred without deferring the design, and pulumi up is an afternoon whenever it is wanted. The full estate costs ~$90–100 a month on Azure, $120–200 all-in, and nothing in Phases 1 through 3 is shaped differently by where it runs.

PhaseRuns whereCloud spend
1 — ArchiverLaptop, plus one real storage account~$1–3/mo
1.5 — GMI ingestLaptop, same storage account
2 — BacktesterLaptop; it is a batch job
3 — DB + entitiesLaptop, MySQL in Docker
4 — Daemonpulumi up — this is the momentfull estate

The one exception

The archiver holds the only copy of data that can never be re-fetched — the FMP news window is 365 rolling days, and a day that slides out of it is gone from our reach for good. A laptop disk is not where the only copy of that lives. So one storage account was stood up first, before anything else, and the archiver writes to it through the SDK from day one so the storage code is byte-identical on the VM later.

A storage account is a bucket, not infrastructure. Note also what the archiver does not force: the VM. The window is 365 days back, so a manual weekly run loses nothing.

Phases and gatesThe build plan: Phase 0 decisions, four phases, and the gate each must pass
Phases 1 and 2 never touch MySQL, which is what lets them run ahead of the first migration. Gate 4 — five unattended trading days — cannot be passed by a laptop that sleeps.

MySQL 8 in Docker, not the MariaDB already on the machine

Decision 4c put backtest run rows in a real database from the first Phase 2 run rather than deferring them, so holdout enforcement and the trial registry are active, not promised. The database is a Docker container, plutus-dev-mysql, MySQL 8 on port 3307 with a persistent volume. There was a MariaDB already installed on the development machine, and it was passed over on purpose.

At the time of the decision the schema had 39 JSON columns. MariaDB's JSON type is an alias for LONGTEXT; MySQL 8's is a native binary type with its own functions and validation. The Pomelo provider generates different SQL for the two. Production is Azure MySQL Flexible Server 8.x — 8.0.21 in the Pulumi program — so developing against MariaDB would have meant developing against a database that behaves differently from the one the code will run on, in exactly the columns the JSON review was scrutinising.

Loading the schema into a real MySQL 8 paid for itself the same day. It caught two defects the SQL file could never have: twenty-five foreign-key names over MySQL's 64-character limit, and a seed row with six values where the table had seven columns.

CI now, CD later

azure-pipelines.yml exists. Build-and-test on the free hosted tier costs nothing, and the backtester's frozen-baseline gate — a stored spy-hold tear sheet that every later run is checked against — is a CI concept that started protecting the golden file the day Gate 2 passed. CD has nothing to deploy to until Phase 4, so there is no CD.

The guard against "perfect it locally"

The known risk of this posture is the class of things that work on a laptop and break in the cloud: managed identity versus user secrets, Key Vault resolution, time zones, the behaviour of a process that runs for days rather than minutes. Three mitigations:

  1. AddPlutusKeyVault() is a no-op without KEY_VAULT_NAME and real on the VM — the same code path both places.
  2. Blob goes through the SDK with DefaultAzureCredential, never the filesystem, so a laptop and a VM differ only in which identity the credential resolves to.
  3. A rule: deploy when there is a daemon to run, not when the code feels finished. Cloud surprises should surface at Gate 4, where they are cheap, not after months of local polish built on an assumption that does not hold.
Where things run today
archiver laptop → plutusprodblob backtesterlaptop → blob + docker database plutus-dev-mysql :3307 daemon not yet estate coded, deployEstate=false
Why Gate 4 is the cloud moment

Gate 4 is five consecutive unattended trading days, a morning cycle and hourly windows on each, and a deliberate mid-cycle kill the daemon must survive without duplicating an order. None of that can be demonstrated on a machine that goes to sleep. The gate demands the VM; the VM is not stood up until the gate demands it.