Sky Yoo

Application engineering · 2024 · shipped

Switchback

A strength-training app — programs authored in git, logged sets synced to SQLite.

Switchback — main view
SWITCHBACK · MAIN VIEW

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

Switchback — Your plan is a document, not a database
PLATE I — YOUR PLAN IS A DOCUMENT, NOT A DATABASE

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.

Switchback — A move library you can actually search
PLATE II — A MOVE LIBRARY YOU CAN ACTUALLY SEARCH

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.

Switchback — Two write doors, two keys
PLATE III — TWO WRITE DOORS, TWO KEYS

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.

resolve.ts
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.

auth.ts
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();
}
Specification
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