Portfolio

Alissa Hsueh

Software Engineer | Product-Minded Builder

IntroRésuméExperienceChef's selectionField notesTechnicalSoft skillsFinishing

Use arrow keys to turn the page.

✦

Field notes

From the pass

What three months of shipping with Claude Code actually taught me.

Alissa Hsueh✦A vess field report✦8 min read

A friend and former colleague asked me to help build the travel app she'd started. What she handed me was a base44 export — generated React from an AI app builder, with a proprietary SDK threaded through every data call.

By June we had vess: an iOS app where you log the places you've been along with your honest reviews, and see what people you trust thought — recommendations from friends and family instead of sponsored posts and inflated ratings. It's also where you plan the trip itself — pulling in the restaurants and sights you've saved from friends' reviews.

Here's what I learned about shipping production-quality work when the model is doing most of the typing — including the two months we spent discovering that our inherited foundation had a security model that only looked like one.

The vess home feed of places recommended by friends
The vess atlas — a map of places you've visited
vess — a feed of places your friends actually vouch for, and an atlas of everywhere you've been.
✦

Why Claude Code, and how I set it up

I picked Claude Code partly out of curiosity — I hadn't had much room to explore it at work and wanted to see what everyone was raving about — and partly because the work in front of me was archaeology, not authorship. I had to understand a generated codebase I hadn't written before I could safely change it, and I needed something that could read across a repo I couldn't yet hold in my head.

Three months of nights and weekends. Two people. An Expo app on a Supabase backend, being used by its two builders while we get it ready for a wider release. Claude Code was my primary coding interface from day one — even though I didn't yet know what it was capable of.

The structure that made this work — some deliberate, some lucky:

  • One data layer. Every screen goes through src/lib/db.ts. It's 590 lines and it's the single chokepoint for reads and writes. When a task was data-shaped, there was exactly one file to point at.
  • One numbered migration per feature. Twenty-four of them. Schema changes stayed legible in isolation instead of accumulating into a blob.
  • Provenance in every commit. Each AI-assisted commit carries a Co-Authored-By trailer and a link back to the Claude Code session that produced it. That habit cost nothing and is the only reason I can write this post with receipts instead of recollection.

My loop was boring on purpose: describe the change and the constraint, let it propose a plan, read the plan before reading the code, then review the diff file by file. I wrote code myself when the problem was a decision rather than an implementation — the shape of a data model, an auth boundary, anything where the wrong choice would be expensive to unwind.

✦

Where it was genuinely a 10x

Logging a place in vess with a personal review
A vess trip built from saved places
Two features that came together fast: logging a place with an honest review, and planning a trip from the ones you've saved.

Untangling base44, overnight. The proprietary SDK was threaded through every data call, and we wanted it out. Tracing an SDK through generated code you didn't write is exactly the task where reading is the bottleneck, not typing — a weekend of manual tracing, minimum, and I'd have been guessing about the parts I hadn't read. Overnight, we had our own data layer.

Web React to React Native in about 48 hours. Vess started as a web app; once we decided it should be iOS-native, we pointed Claude Code at the port. Neither of us had shipped an Expo app before. Screen by screen, the port went faster than learning the framework would have.

Tests we otherwise would not have written. 68 tests in a single commit, including a 371-line suite against the data layer, retrofitted onto code that had none. Let me be honest about the counterfactual: the alternative wasn't slower tests. On a two-person side project at 1am, the alternative was no tests.

The share importer. Pulling a place out of a TikTok or Reddit link — oEmbed extraction, then Google Places text search, then ranked candidates — plus native share-sheet integration through expo-share-intent. Four unfamiliar APIs stitched together in an evening.

✦

Where it wasn't, and where I got burned

USING (true). The generated schema had row-level security enabled on every table. That reads as “we did the security thing.” But the SELECT policies were USING (true) — every private profile, every logged place, every trip comment readable by any authenticated user. Our is_public flag existed in the UI and meant nothing to the database. It took a manual audit — logging in as one user, trying to read another's private data — to catch it. Migration 015 is what finally replaced those policies with real ones.

The rule

For anything authorization-shaped, I write the adversarial check first — log in as B, try to read A's private row — before I read the policy. Correct-looking code and correct code diverge most sharply where the model can't observe the outcome.

A vess user profile
Every private profile like this one was readable by any signed-in user — until migration 015 replaced the policies with real ones.

Web idioms that fail silently in a native runtime. Two of the same species. The avatar upload used a Blob; React Native's Blob implementation is incomplete, Supabase Storage rejected it silently, and an empty catch {} swallowed the evidence. Uploads “succeeded” and produced nothing. Separately, crypto.randomUUID() — there is no crypto global in Hermes, so it crashed at runtime rather than at build. Both typechecked. Both passed my review.

The rule

TypeScript compiling tells you nothing about a React Native runtime. Nothing is done until it runs on a physical device.

Migration collisions across sessions. We have three collisions in our migrations folder — two 005s, two 015s, two 016s — because separate sessions each numbered “next” against a repo state that had already moved. Then it compounded into three consecutive commits fixing migrations that assumed a schema that didn't exist: making the SQL idempotent, dropping policies before altering a column, and finally skipping a type change because the live database already had it.

The rule

The model cannot see production. Anything gated on state it can't observe gets a human-verified plan, not a generated one.

✦

What stayed human

Taste doesn't delegate. We wanted vess to feel chic, warm, and elevated — an app you'd trust to help plan your next trip, not another AI-generated feed.

A Pinterest mood board for the vess aesthetic
The vess color palette
The vess logo
Days in Procreate and Figma — Pinterest boards, palettes, and the vess logo mark, all created and decided by hand.

Days in Procreate and Figma. Fonts, color palettes, Pinterest boards. In a moment when a new AI-built app launches every other day, we wanted vess to feel considered — and that meant making the decisions ourselves.

It took longer than we'd like to admit — we both come from technical backgrounds — but I found the design work genuinely rewarding in a way engineering usually isn't.

Shipping faster didn't mean shipping thoughtlessly. It meant spending my attention on the twenty decisions that needed judgment instead of the two thousand that needed typing.

✦

What I'd change

I'd audit the inherited schema before building three months on top of it.

base44's generated data model keyed identity on email — created_by, follower_email, user_email, select_emails, all the way out to a route at app/user/[email].tsx. Users' email addresses, in URLs, in a social app. Every RLS policy became a string comparison against auth.jwt() ->> 'email' instead of a UUID foreign key. Nobody can ever change their email, and every downstream feature that needed to know “who can see this row” had to re-derive it from email string-sets on the client.

Moving to auth.uid() would have cost two hours on day one. I inherited it, it worked, and I paid for it for three months. The mistake wasn't the schema — it was treating a working foundation as a decided one.

✦

Close

The habit I kept: the model is extraordinary at the parts of the job that are reading and typing, and indifferent to the parts that are deciding. Knowing which is which — before you start, not after the audit — turns out to be most of the skill.

← Previous course

Page 5 of 8

Next course →