architectural substrate

The services model.

The portfolio is being re-grounded on reusable services — single, provider-pluggable capabilities that surfaces compose instead of re-rolling. One messaging gate, one reporting pattern, one off-Cloudflare twin — wired once, called everywhere, swappable underneath.

This page is the substrate map: what a service is here, how a surface wires into one, the three that exist today, and why the model lowers upkeep rather than adding it.

What a “service” means here

A service is one capability behind a stable interface, with the how hidden from every caller. Three properties define it, and a thing isn't a service until it has all three:

Bolt-on per-surface = friction, drift, and surfaces that never get the treatment. Service-backed = the floor rises every time, and the upkeep is one fix instead of N.

The substrate, in one diagram

SURFACES — they compose, never re-roll SERVICES — stable interface, hidden mechanism PROVIDERS — pluggable & replaceable happiness.gf.cx sandbox.gf.cx home.gf.cx / ebay burst-vm rig …future Messaging notify.py · notify_verdict() Reporting status JSON → status.gf.cx Durability the off-Cloudflare twin Pushover Telegram* ntfy* email* status.gf.cx data layer + registry Wasabi SG Linode SG rsync.net HK OneDrive SG DNS twin · deSEC + Porkbun

* providers marked with an asterisk are interface-ready, not yet implemented — each is “a class + a registry line” away, and no surface changes when one lands. The Durability twin is highlighted because it exists to survive the one risk the rest of the stack shares: a single Cloudflare account. The DNS twin carries that same crimson for a reason — DNS is the worst place to be frozen, and a warm deSEC standby with the registrar held off Cloudflare (Porkbun) is what makes the failover actually fireable in a suspension.

The three services today

Kicked off by today's work — the R2→Wasabi mirror and the Linode cost monitor needed a push channel and a status home, and rather than hand-roll both, we extracted the services they revealed.

messaging

notify.py

One provider-agnostic gate. Pushover ships; Telegram / ntfy / Slack / email are each a Provider class + one registry line. Severity vocabulary (ok→emerg) maps onto each transport's own priority, so callers never think in Pushover ints.

notify_verdict("R2→Wasabi", summary, "green", surface="…")
reporting

status.gf.cx pattern

Every job emits a normalised status JSON to the data layer, registers in registry.json, and the hub renders + deploys it. Then the contextual one-liner goes through the messaging gate. Two layers, cleanly split — the value is the sentence, not the numbers.

data/<slug>-latest.json → status.gf.cx → notify()
durability

the off-Cloudflare twin

Everything Cloudflare holds — R2 buckets, DNS, the surfaces — duplicated somewhere CF can't touch. Wasabi mirrors R2 (egress-free restore), Linode bursts compute off-platform, rsync.net holds the parachute, OneDrive relays it, and a deSEC warm-standby — registrar at Porkbun, both off Cloudflare — keeps DNS resolving if CF goes dark. One blast-radius answer, five providers.

r2_wasabi_mirror · linode_status · restic_rsyncnet · gfcx_dns

On naming: “backup-as-a-service” undersells it — a backup is a file you hope never to open. This is a live duplicate of the Cloudflare-resident portfolio: storage, DNS standby, and burst compute, each off-platform, each restorable cheaply. The service's job is to make the single-CF-account risk survivable.

How a surface wires in

A surface that needs to report does not hand-roll a curl. It writes its status JSON + calls the gate. That's the whole contract:

# 1 · emit the data-layer status (status.gf.cx renders + deploys it) write("~/Code/status.gf.cx/data/linode-cost-latest.json", payload) # 2 · hand the contextual sentence to the messaging gate from notify import notify_verdict notify_verdict("Linode burst-VM", "Spent $0.48 for 2.5h, 6 days ago — nothing running now.", "green", surface="linode-burst-cost") # Pushover today. Add Telegram? One class in notify.py — this line never changes.

Scheduling is just launchd pointing at the orchestrator — the R2→Wasabi mirror runs twice daily (10:00 & 18:00 ET), the Linode monitor once (09:00 ET). Both publish a card and push a sentence. Swap the transport, change the cadence, add a fourth provider — no surface notices.

Why the model pays — the ROI

The argument for services is operational, not aesthetic. Managing capabilities gets cheaper as the portfolio grows, where bolt-ons get more expensive:

Upkeep is one fix, not N

The Pushover curl block lived in 4 places and was drifting. Now it's one service. A fix to retry logic or rate-limit handling lands once and every surface inherits it.

Providers swap for free

Replace Pushover with Telegram, or add it alongside: one class + one registry line. Zero caller edits, because the interface is the contract — not the transport.

Advances plug in behind the interface

A better push channel, a cheaper storage region, a faster burst image — each lands behind the same call. The surface that wrote one line in 2026 gets the upgrade without being touched.

Observability is built in

Every service reports to status.gf.cx in the same shape. One place to answer “is it working?” across every surface — no per-surface dashboard to maintain.

Risk is centralised, then answered

The whole portfolio shares one Cloudflare account. The Durability twin makes that survivable for all 14 zones at once — a per-surface backup never could.

New surfaces start finished

Scaffold a subdomain and it inherits messaging, reporting, and the resilience floor by default. “Under-finished” shrinks every time the floor rises — the compounding-kb kernel, applied to ops.

Where to look

srvc.gf.cx · architectural substrate · authored 2026-06-13 · three services live: messaging (notify.py) · reporting (status.gf.cx) · durability (Wasabi · Linode · rsync.net · OneDrive · DNS: deSEC/Porkbun) · inherits assets.gf.cx/cards + media · three-layer baseline + release stamp · updated