Application engineering · 2024 · shipped
Switchback
A strength-training app — programs authored in git, logged sets synced to SQLite.

Too steep to climb head-on, so you take it leg by leg. Switchback is a training system built on a plain-text substrate — the exercise library, the reusable blocks, and every program are authored documents in git, while the sets you log land in SQLite through the logging client. The name fits the shape of a plan, too: structured legs, each one setting up the next, rather than one climb straight up. Deliberately the opposite of the site’s own brass-and-ink register, it looks like the things already in its domain — race bibs, chairlift signage, trail markers. Sharp, fast, glanceable at arm’s length mid-set.
The instrument

Your plan is a document, not a database
The active program is plain text and Markdown, parsed at build time — no in-app editor, no drifting `_v3` filenames, every change carries its "why" in git history. Progress renders as a route elevation profile: deload weeks dip, build phases climb, test day is the summit.

A move library you can actually search
Five hundred–plus exercises — the whole substrate — tagged by category and equipment, name-searched instantly. Programs and blocks reference moves by name; the library resolves name → id → page, so a prescription is always one tap from how to perform it.

Two write doors, two keys
Documents change only through the repo — authored in git, never edited in the app; that door is open today. The sets you log are the other kind of data: runtime events that sync to SQLite (Cloudflare D1), plan and actual kept separate and queryable. You log offline at the gym and it syncs when the phone is back on signal.
The movement
The engineering underneath
The interesting decision in Switchback isn't a framework — it's giving each kind of data the substrate that matches how it changes.
Two substrates, split by how the data mutates
Authored content — the exercise library, the reusable blocks, whole programs — is plain-text YAML and Markdown in git, parsed at build time, and that half runs today. The sets you log are the other kind: runtime events in SQLite on Cloudflare D1 — logged offline on-device and synced when the phone is back on signal. Data that changes by editing lives in the repo, versioned by git history; data that changes by happening lives in a queryable store. Nothing is forced into a shape that doesn't fit it.
Programs are compiled, not stored
There is no in-app editor and no drifting `program_v3_final` rows. The active plan is authored as documents and resolved at build time — live today; exercises are referenced by name and resolved name → id → page, so a prescription is always one tap from how to perform it — and a broken reference fails the build instead of surfacing mid-set at the gym.
export function buildResolver(
exercises: Exercise[],
blocks: Block[]
): (name: string) => ResolvedRef {
const byExercise = new Map<string, Exercise>();
for (const ex of exercises) {
const key = normalizeName(ex.name);
if (!byExercise.has(key)) byExercise.set(key, ex);
}
return (name: string): ResolvedRef => {
const key = normalizeName(name);
if (!key) return null;
const ex = byExercise.get(key);
if (ex) return { kind: 'exercise', id: ex.id, name: ex.name, url: ex.urls?.[0] };
return null;
};
}Read-heavy, single-tenant, phone-first
Public read needs no login; owner-only writes are gated by a WebAuthn passkey, and the installable PWA is built to be glanceable at arm's length between sets. The write surface is deliberately narrow — one door through the repo, one through logged sets — which keeps the runtime data small, the queries simple, and the failure modes few.
export async function validateSession(db: D1Database, token: string | undefined): Promise<boolean> {
if (!token) return false;
const row = await db
.prepare('SELECT expires_at FROM auth_sessions WHERE token_hash = ?1')
.bind(await sha256b64url(token))
.first<{ expires_at: string }>();
return !!row && new Date(row.expires_at) > new Date();
}- Domain
- Strength & conditioning programming
- Substrate
- Plain text · YAML + Markdown in git
- Runtime data
- Cloudflare D1 (SQLite) — offline log, syncs on reconnect
- App
- SvelteKit · phone-first, installable PWA
- Access
- Single-tenant · public read; passkey (WebAuthn) owner writes