Back to Projects
CodeIn ProgressMarch 2026

Idea to Launchable Product

What is the best way to plan a Scottish golfing trip

Idea to Launchable Product preview

Status

In Progress

AI Engineering

Last Monday I had a blank directory. By Sunday I had a working AI product: React frontend, FastAPI backend, a Claude-powered planning agent with tool use, SSE streaming, Supabase auth, itinerary sharing, and a React Native mobile app. One person. One week. Claude Code as the engine.

This is the technical story of how I built MonarchLinks, what the stack looks like, how I used Claude Code across the entire build, and why the hours I spent writing config files before touching application code were the most productive of the week.

What I built

MonarchLinks is a planning tool for golfers visiting Scotland. A multi-step wizard captures preferences (dates, budget, regions, skill level), then a Claude Sonnet agent calls tools (search courses, get pricing, calculate travel times between venues) and assembles a priced day-by-day itinerary. The user watches it build in real time via SSE streaming, then refines conversationally in a chat interface.

The stack

Frontend. React + TypeScript, built with Vite, served as static files by FastAPI. Tailwind CSS. No component library. The wizard, chat interface, itinerary views, comparison tool, and share pages are all functional components with hooks.

Backend. FastAPI on Python 3.12, deployed on Railway as a single service that serves both the API and the static frontend. Pydantic models everywhere, with camelCase aliases so JSON keys match the TypeScript interfaces without any mapping layer.

AI agent. Raw anthropic Python SDK. No LangChain, no CrewAI, no LangGraph. A simple tool-use loop where Claude Sonnet gets a set of tools (search courses, get course details, get travel times, add/remove courses from the itinerary, check weather) and a PlanningState Pydantic model as the source of truth. The agent reasons in phases: constraint analysis, enrichment, assembly. Max 25 tool calls per session.

If the agent fails or times out, a hardcoded golden fallback itinerary is returned so the user never sees an error when a degraded result is possible.

Streaming. SSE via sse-starlette. The frontend consumes with fetch() + ReadableStream (not EventSource, because POST bodies are needed). Event types: status (semantic progress like "Checking availability at Kingsbarns..."), done, error. No unexplained spinners.

Database. Supabase for Postgres, Auth (Google OAuth), and Row Level Security. JWT validation in FastAPI. Anonymous-first sessions for the MVP, with a "claim your itinerary" flow when the user signs in.

Mobile. Expo + React Native with StyleSheet.create() for styling. Six screens: auth, dashboard, trip detail, chat with character-reveal streaming, alerts, booking confirmation. Supabase auth with SecureStore on native and a web fallback. Lazy-loaded expo-notifications behind a platform guard.

Observability. Langfuse Cloud from day one. Every tool call logged, cost per session tracked, prompt versions pinned.

Caching. In-memory cachetools LRU. No Redis. Course details cached 24 hours, travel times cached 7 days, weather cached 1 hour.

The CLAUDE.md file, this is the part that made everything else work.

Claude Code reads a CLAUDE.md file at the start of every session. I wrote mine on Day 1 before opening a code editor. It ran to 300+ lines and covered architecture decisions, coding conventions, agent behaviour, UX principles, and brand rules.

Some of what went in:

The architecture locks. "FastAPI on Railway. No Next.js. Raw Anthropic SDK, no LangChain. Supabase for auth. In-memory caching, no Redis." These weren't suggestions. They were constraints. Claude never once proposed LangChain across 50+ sessions because the file said not to. One line eliminated a recurring category of debate.

The coding conventions. "Pydantic models use camelCase aliases with alias_generator=to_camel and serialize with by_alias=True." Every API endpoint serialised correctly on the first try. The frontend TypeScript interfaces matched. Zero time debugging JSON key casing.

The agent design. "Tools return curated summaries, 200 tokens max, not raw API responses. Top 5 results, 5-6 fields each." This kept the agent's context window clean and prevented the token bloat that kills multi-turn tool-use sessions.

The error philosophy. "Golden fallback itinerary if the agent fails. Never show the user an error when a degraded result is possible." This was baked into every agent-related implementation without me re-specifying it.

The brand voice. "Error messages should be helpful and warm, not robotic." The mobile app ended up with "Jock lost the thread, try again?" instead of "An error occurred. Please retry." I didn't ask for that in any specific prompt. It came from the CLAUDE.md, absorbed into every generation.

The file isn't documentation. It's a context multiplier. Every hour I spent on it bought back 5-10 hours downstream, because every new Claude Code session inherited the full decision history without me re-explaining anything.

How I worked with Claude Code

The workflow was a four-phase loop: Research, Plan, Implement, Test. Human review between each phase.

Research meant Claude explores the codebase and reports back: "here's how auth works, here are the files involved, here's what would need to change." Plan meant Claude proposes a step-by-step approach and I review it before any code is written. Bad ideas die here for 5 minutes instead of costing an hour in implementation. Implement meant Claude writes code while I verify each step compiles, passes types, and actually works at runtime.

The thing I didn't expect: research mistakes cascade the worst. If Claude misunderstands the codebase in the research phase and that gets into the plan, the implementation is doomed. Catching it early is almost free. Catching it late means rewriting.

I also built six custom skills (reusable prompt packages). /brand evaluated any UI output against the style guide. /craft scanned for code quality issues like unused props, empty catch blocks, magic strings. /ai-writing-check flagged copy that sounded machine-generated. /persona-feedback tested UI decisions against 12 defined user personas. Quality wasn't a one-off review at the end. It was a repeatable gate I could run at any point.

What went wrong

On Day 6 I scaffolded the entire React Native mobile app: six screens, tab navigator, chat with streaming, push notifications, booking flows. TypeScript passed. I opened it in the browser: blank white screen.

Three things had failed simultaneously. NativeWind (a Tailwind-to-React-Native bridge) was silently broken on web, so every component rendered with zero height, invisible. The Supabase client was initialised with empty strings because the mobile app didn't have its own .env file, so the library threw at import time and crashed the module tree. And expo-notifications was imported at the top level of a file in the render path, throwing on web where no native notification module exists.

This was the cost of speed without verification. Claude generated working TypeScript that passed every static check. But "compiles" and "renders" are different things, and I hadn't opened the app until everything was wired together.

The fix: rip out NativeWind entirely, replace all className usage with StyleSheet.create(), guard the Supabase init with a fallback URL and a dev warning, lazy-import expo-notifications inside a useEffect behind a Platform.OS !== "web" check.

The lesson: my CLAUDE.md was thorough for the backend stack. I locked "FastAPI, no Next.js, raw Anthropic SDK" and it held. But I didn't apply the same rigour to the mobile stack. "No NativeWind, use StyleSheet" would have been one line and would have prevented the entire problem.

What I'd do differently

Run the app after every screen, not after all six. Static analysis is not a substitute for opening the browser.

Document environment setup in CLAUDE.md, not just architecture. The Supabase credentials existed in the web app's .env but the mobile app didn't have its own copy. One line in the config file would have prevented half the blank-screen bug.

Rule things out more aggressively on the frontend. My backend architecture locks were specific ("no LangChain, no Redis, no LangGraph"). My mobile decisions were vague. Vagueness drifts. Claude picked NativeWind because it seemed reasonable and nothing in CLAUDE.md said otherwise.

Key numbers

  • ~50 Claude Code sessions across the week
  • 300+ line CLAUDE.md configuration file
  • 25 max tool calls per agent planning session
  • Agent cost: ~£0.50 for a 4-day itinerary, ~£1.60 for a 7-day tour
  • 14 seed courses in the database, expandable to 50+
  • SSE streaming with semantic status indicators (not spinners)
  • Zero LangChain. Zero component libraries. Zero Redis.

The takeaway

The leverage from Claude Code isn't typing speed. It's context inheritance. Every session starts with the CLAUDE.md loaded, which means every session starts with the architecture decisions, coding conventions, agent design, error philosophy, and brand voice already in place.

The skill that mattered this week wasn't writing code. It was writing specifications clearly enough that a language model could execute them without drift across dozens of sessions. In a nutshell, the early specs cast a large shadow.

Interested in working together?

I'm always open to discussing new projects and opportunities.