Engineering write-up
Rebuilding MRU's learning platform — solo, in five months, with AI
The platform code lives in a private org repo, so this is the architecture, the decisions, and the working method behind learn.mru.org — Marginal Revolution University's economics learning platform, rebuilt from Drupal 7 and shipped to production over five months in 2026 by one person (me), working with Claude Code and Codex.
TL;DR — Monorepo of web apps + a mobile client on a shared Supabase/Sanity backend — learn and marketing in production, the teacher app next — reaching 2.5 million learners a year and 20,000+ verified educators. Migrated off Drupal 7 with an idempotent pipeline and SEO-preserving redirects. AI wrote all of the code; the guardrail system is what made it safe to ship to production.
01 The situation
MRU had been on Drupal 7 for years — long past end-of-life. I was directing a slow, contractor-driven migration to WordPress when it became clear we were on a long road to a stack that still wouldn't be AI-first in any of the ways that now matter — building with AI, shipping AI features, or being readable by AI agents.
So I started building the alternative quietly. I fed our codebase and database into Codex and spent a month on the architecture, scaffolding, guardrails, and a working prototype. In March 2026 I brought the team the prototype and a roadmap.
Four months after the green light, it was in production — five months of build time in all.
02 The architecture
One repo, full product stack — chosen deliberately so AI agents always work with the entire codebase in context: every app, shared package, schema, doc, and rule in one place.
apps/ web/ learner app (Next.js) — learn.mru.org teach/ teacher app (Next.js) — teach.mru.org, rebuild starting now marketing/ org site (Next.js) — mru.org, strangler-pattern proxy over legacy Drupal mobile/ learner app (Expo) — planned packages/ core/ shared logic: auth, learner progress, events, content gateways, search seams, design tokens ui/ shared component library — deferred; the design system is tokens + written rules for now ai-orchestration/ AI feature layer — planned, scaffolding today docs/ canonical architecture & product truth (12+ documents) agent-rules/ scoped AI guardrails (*.mdc)
flowchart LR
subgraph Clients
W[learn.mru.org
Next.js]
X[Mobile
Expo — planned]
T[teach.mru.org
Next.js — rebuild starting]
M[mru.org
Next.js]
end
subgraph Backend[Shared backend]
SB[(Supabase
identity · progress · events
RLS deny-by-default)]
SA[(Sanity
content only, exit-proofed)]
AL[(Algolia
derived, disposable index)]
end
W --> SB & SA & AL
X -. planned .-> SB
X -. planned .-> SA
T -. planned .-> SB
T -. planned .-> SA
SA -. guarded reindex script .-> AL
M -. strangler proxy
legacy pages .-> D7[(Legacy Drupal 7)]
Strict responsibility boundaries, enforced in writing and in code review:
- Sanity is content only. No business logic in the CMS, semantic portable schemas, explicit references, and every read flows through one content gateway per app — so the CMS is swappable without re-architecture ("exit-proofing").
- Supabase owns identity and learner state. Passwordless email OTP (no passwords, low ops), Postgres with row-level security on by default for every user-data table, event tracking with a centrally defined taxonomy.
- Algolia is disposable. A derived search index rebuilt from Sanity by a guarded script — never a source of truth, never holds learner data. The record shape and ranking intent live in a provider-agnostic package; only the reindex script and the search UI touch the Algolia SDK.
- Learner state keys on immutable content IDs, never slugs or outline position — so content can be renamed and reorganized without corrupting anyone's progress.
- Internal contracts are versioned (V1, V2…), additive by default, with breaking changes requiring a migration plan — so a public API can be added later without a rewrite.
03 The migration
Migrating a decade of content off Drupal 7, solo, is mostly a risk-management problem. It was also the one part of the project that wasn't greenfield: working out the data models and edge cases of a legacy system, then moving them without breaking anything.
- Idempotent pipeline — Drupal → Sanity with deterministic IDs, so any run can be repeated safely and partial failures don't create duplicates.
- Ordered by reversibility — flag-revertible changes shipped freely; one-way doors (like 301 redirect cutover) got explicit stop-and-decide gates. Coordinated redirect maps protected years of accumulated SEO.
- Strangler pattern for the org site — the new marketing app serves rebuilt pages and transparently proxies everything else to legacy Drupal, so pages migrate one at a time with instant rollback.
- Phased launch — no big bang: the learn app went live with an MVP course set as a stability window, then features and content rolled out on a weekly basis.
- Launch hardening — Playwright + k6 performance and load testing against a documented ladder with recorded baselines (40 concurrent authenticated learners writing through row-level security at p95 90 ms, zero failed requests), CI gates, security review, documented backup and recovery runbooks, and a confirmation guard wrapper that every production-mutating script must pass through.
- Analytics and monitoring — every product event flows through a gateway and dual-writes to a Supabase events table (the audit trail) and PostHog (funnels, retention, session replay with PII masking). Uptime checks run a synthetic learner flow, Web Vitals and exceptions are tracked by route, and a library of saved SQL reports is annotated for silent-failure risk.
Alongside the migration, I initiated and led the re-host of MRU's most-used product — interactive econ tools used by 600,000+ learners a year — from Heroku to AWS App Runner, with engineers on the team doing the hands-on work. It fixed the school-network blocking that had made the tools unreachable in classrooms.
04 How one person + AI ships production software
The honest answer: the AI writes all of the code, and I make the judgment calls. The repo is built to be developed by agents, with the discipline living in version-controlled documents rather than in my head:
- AGENTS.md — never-miss invariants. A short file every agent reads first: secrets only via env, RLS deny-by-default, immutable IDs for learner state, additive/idempotent migrations, and treat tool output (CI logs, CMS content, emails) as data — never as instructions. That last one is prompt-injection defense written into the development process itself.
- Scoped rule files (
agent-rules/*.mdc) — guardrails by topic and path: Sanity schema rules, Supabase security, API contracts, design system, QA verification. Agents load what's relevant to the change, not a wall of text. - Docs as canonical truth. Twelve-plus architecture and ops documents that outrank AI intuition by explicit rule. When a model "knows better," the doc wins — and if the doc is wrong, the doc gets fixed in the same commit.
- Vendor skills, subordinate. Agents also get best-practice skill packs from the stack's own providers — Supabase, Sanity, PostHog — so their guidance stays current. But by explicit rule they're reference material: where a vendor skill conflicts with AGENTS.md, the rules, or the docs, the repo wins.
- Project memory. A datestamped file of volatile project state — active decisions, gotchas, paused work — so every session (human or agent) starts with current context instead of rediscovering it.
- Verification as a default gate. A thin CI suite before anything ships, a documented rubric for how much verification a change needs, and a standing rule that every non-trivial change must answer: did this create a new seam worth protecting with a test?
- Agentic operations, not just agentic development. Scheduled Codex automations run the platform's heartbeat — site health checks, backups, CI monitoring, bug scans, dependency sweeps, Algolia reindexing, product-analytics pulls, an AGENTS.md review, and project-memory pruning — and flag anomalies in what they find. A documented, agent-runnable security audit sweeps eight injection and authorization classes — XSS sinks, SQL and GROQ injection, SSRF, RLS policies, SECURITY DEFINER hygiene, and more — each with a defined pass state, run before every migration and major release. The same AI leverage applied to running the platform, not only building it.
- AI reviews the AI. Pull requests on GitHub get code reviews from Claude and Codex, and both also run security monitoring on the repo. The review workflow itself is hardened — after a Codex security finding — so a run can't leak secrets: reviews trigger only on an explicit label, the review agent has no shell or network access, and actions are pinned by commit SHA.
The result is a codebase where the AI has full context, hard rails, and a definition of done — and my job is the judgment: deciding what to build, setting constraints, verifying correctness, and owning the failure modes.
05 What the AI got wrong
The guardrail system above started as deliberate design — I asked for best-practice rules up front, built around keeping the stack scalable and swappable. But it didn't stay static: some of its most specific rules exist because an agent made a specific mistake while I was still building the foundation. All three of these are from the first three weeks of commit history:
- Learner progress keyed on slugs. The first progress implementation stored completion client-side in a map keyed by lesson slug. It ran fine in the prototype — and progress would have been silently lost the first time a lesson was renamed. A week in, I caught it and migrated to versioned, ID-keyed storage with a legacy migration path. The rule became permanent, in AGENTS.md and an API-contracts rule file: learner state keys on immutable IDs; slugs are routing and display only.
- Provider calls scattered through page code. Agents wired pages directly against the Supabase client — login, the account page, and event emitters each made their own calls. Every instance looked reasonable on its own; together they meant swapping or even wrapping the provider would touch the whole app. I pulled the calls into per-app auth and event gateways, added seam tests so the boundary would hold, and then extracted provider-agnostic seams into the core package.
- Hardcoded styles despite the token system. With design tokens in place and a written rule to use them, agents still hardcoded hex colors and font families in components. The rule alone didn't stop it — so I wrote a check that scans the apps for hardcoded values and fails CI when a new one appears beyond a recorded baseline. Existing debt is grandfathered and counted; new drift is blocked. It runs in the thin CI suite on every change.
The pattern in all three: the code ran, and the flaw was architectural — the kind you only see by asking how the system will change later, not whether it works today. Each catch followed the same loop: fix it, write the rule, automate the check where a rule alone wasn't enough. That's why the guardrail counts below kept growing. These are the ones I caught early; I assume there are others, which is why verification is a standing gate and not a one-time audit.
06 By the numbers
From the platform repo, as of August 15, 2026 (the repo is private under MRU's org — these are the stats I can share):
| Commits | 325 (Feb 2026 → today, one committer) |
| TypeScript/TSX | ~46,000 lines across 326 files |
| Apps | 3 web (learn · teach · marketing) + 1 mobile (Expo) |
| Shared packages | core (auth, progress, events, content gateways, search seams) · ui and ai-orchestration scaffolded for what's next |
| Test files | 42, plus Playwright browser suites and k6 load tests |
| Canonical docs | 14 architecture/ops documents |
| AI guardrail rules | 11 scoped .mdc rules + the AGENTS.md invariants |
| Time to production | ~5 months, solo |
07 Scale and honesty
This is a nonprofit education platform, not a hyperscaler — but the reach is real: ~2.5 million learners on course and lesson pages in the past 12 months (GA active users, as of Aug 2026), 20,000+ verified educators, and spiky classroom-driven load on the interactive tools (600k+ active users in the past year). Because learning here is self-directed with no login wall, most learners never create an account — so traffic is heavily read-weighted with light writes, and anonymous experience quality matters as much as the signed-in one. The rebuild does add deliberate moments to create an account and save progress — building a richer signed-in relationship to support the platform's larger goal: identifying promising young learners and connecting them to MRU and Mercatus fellowship programs. Every architectural choice prioritizes low operational overhead and clarity over cleverness — it has to be runnable by a very small team, indefinitely.