Building HeatSync Part 3: Making It Work

Jan 26, 2026

So I had the idea and a clear scope: upload a PDF, enter a swimmer name, get calendar events in under 30 seconds. Time to build.

My goal for the first milestone was simple: get the core flow working end-to-end. Upload a PDF, enter a name, see the events. Ugly UI acceptable. Polish comes later.

Tech Stack Decisions

I made these choices fast and didn’t second-guess them:

LayerChoiceWhy
FrontendSvelteKitLightweight, reactive, I know it well
BackendHono + Bun~14KB framework, native TypeScript, fast
AIOpenAI SDKWorks with any OpenAI-compatible endpoint
PDF ProcessingmupdfServer-side rendering, handles any PDF
Calendarics libraryGenerates valid .ics files
StylingTailwindCSS v4Rapid prototyping, responsive out of the box

Nothing exotic here. I picked tools I’m comfortable with so I could focus on the actual problem, not learning new frameworks.

The Model Selection Circus

Here’s where things got interesting.

I knew from my ChatGPT experiment that gpt-5.2 could extract events from heat sheet PDFs. But gpt-5.2 is expensive. For a free tool, cost matters.

So I tested every model I could get my hands on:

  • All AI Builder Space models
  • OpenAI’s gpt-5-mini
  • OpenAI’s gpt-5-nano
  • gpt-5 (the full model)

The results were disappointing.

Most models either:

  • Couldn’t read the PDF properly (hallucinated events that didn’t exist)
  • Returned malformed JSON
  • Missed events entirely
  • Confused swimmers with similar names

The only models that consistently produced correct results were gpt-5 and gpt-5.2.

gpt-5 worked but was painfully slow (sometimes 30+ seconds per extraction). That kills the “under 30 seconds” OKR immediately.

So I ended up right back at gpt-5.2. Went in a circle and landed where I started.

Lesson learned: test your assumptions early. I spent half a day on model experiments before accepting the obvious answer.

The Core Architecture

I landed on a simple architecture:

┌──────────────────┐    ┌──────────────────┐    ┌──────────────────┐
│   SvelteKit UI   │───▶│   Hono Backend   │───▶│   OpenAI API     │
│   (Upload Form)  │    │   (PDF + AI)     │    │   (gpt-5.2)      │
└──────────────────┘    └──────────────────┘    └──────────────────┘

Why a backend at all? Two reasons:

  1. API key security: Can’t expose OpenAI keys to the client
  2. PDF processing: Server-side mupdf is more reliable than client-side PDF.js, especially on mobile

The backend does all the heavy lifting: receives the PDF, optionally renders it to images (for non-GPT models), calls the AI, parses the response, returns structured data.

The Extraction Prompt

Getting the AI prompt right took several iterations. The final version includes:

InstructionPurpose
Name normalizationHandle both “First Last” and “Last, First” formats
Exact name matchingReturn the name exactly as it appears in the PDF
Page thoroughnessScan ALL pages before returning (no early termination)
Session date calculationDerive date from meet date range + weekday indicator
Heat time extractionGet explicit times or estimate from previous heats
temperature: 0Deterministic output for consistency

The prompt also explicitly warns about swimmers with similar names (but more on that in Part 6 when I cover accuracy improvements).

First Working Demo

Two days after starting, I had a working demo:

  • Upload a PDF ✓
  • Enter a swimmer name ✓
  • See extracted events ✓
  • Download .ics file ✓

Was it pretty? No. Was it fast? Not really. Did it work? Yes.

That’s the milestone. Get it working, then make it good.

What I Used to Build It

This project was built almost entirely with Claude Code. I described what I wanted, reviewed the changes, iterated. The commit history tells the story: dozens of small, focused changes over a couple of days.

I’m not going to pretend I typed every line of code myself. That’s not the point anymore. The point is knowing what to build and being able to guide the tool effectively.


This is Part 3 of a series on building HeatSync. ← Part 2: Defining the MVP | Part 4: Removing Friction →

Tags: