Portfolio

I build software for specific people, usually starting with myself. The work here is grouped by intent rather than by date - Experiments, where I sharpen the tools; Products, where I solve real problems; Prototypes, where I'm working out what comes next.

Products
28 Feb 2026

Building for an Audience of One

I had been looking for an app idea to build when my sister handed me one over a weekend afternoon. She has been gardening more seriously each year, and she spent an hour walking me through every problem she runs into. The list came out unfiltered, which is the most useful kind of list to receive.

She has a ninety percent failure rate and has stopped pretending otherwise. Every seed needs different conditions. Some need light, some need darkness, some need to spend two weeks in the fridge before they go anywhere near soil. Her zone matters and the weather varies year to year, so any advice that ignores the specific week she is in is advice she can't use. She searches for plant information constantly and forgets it just as constantly. She wants the system to remember what she has planted and where, and to surface the right thing at the right time without being asked. When she puts in a plant that does poorly in her zip code, she doesn't want a refusal; she wants an alternative she can actually grow. She wants alerts a few days before a frost so she can cover her pansies, not the morning after. She wants a daily or weekly task list that knows about her garden, her location, the sun path across her yard, and the slugs that show up in February if she hasn't already pre-treated for them.

She wasn't describing a chatbot. Chatbots were what she'd been trying to use, and they were failing her in a specific way. They were happy to answer any question she asked, and they had no memory of her garden, no awareness of her week, no ability to surface the thing she should be thinking about before she thought to ask. She didn't need a smarter Q&A interface. She needed a system that knew her garden as well as she did and prompted her on the right day.

I built GardenSage as that system, for her. The audience was deliberately one person.

I also used the project as an excuse to finally do a few things properly that I had been putting off. Real LLM integration with API keys obfuscated through Vercel environment variables. A real database with row-level security through Supabase. Real email authentication through Supabase Auth and Resend. None of this is novel. Doing it correctly end-to-end, on my own dime, with no team to lean on, was the point.

The stack is Next.js with the App Router, Supabase for auth and data and access control, Tailwind and shadcn/ui for the interface, and @dnd-kit for the drag-and-drop garden bed canvas, which ended up being the most tactile part of the app. Next.js was driven by the read-heavy nature of the workload. Plant data, care schedules, weather lookups. Most of what GardenSage does is pull and present, which made server components the natural shape. Supabase covered three things I would otherwise have had to build separately, which is the kind of decision that pays off every week.

I wrote the data model first. SQL migrations for the plant catalog, the companion relationships, and the care schedules came before any React component. The benefit was that the UI always had real data to render against, which surfaced edge cases I would have otherwise discovered weeks later. The cost was that the seed data eventually grew to 138,000 lines of SQL, which is genuinely painful to diff in a code review. I would make the same call again. I would set up better tooling around the migrations earlier.

The Daily Brief was a late addition and turned out to be the feature that sold the product to my sister. It is an LLM-generated summary of what is happening in her garden today and what she should be thinking about. The execution constraint was awkward. Vercel's Hobby tier has no cron jobs, so I couldn't just generate the brief at 6 a.m. for every user. I cached one row per user per day in Supabase and used a stale-while-revalidate pattern to serve the previous day's brief instantly while the new one regenerates in the background. The interface never blocks. The user doesn't know there was a constraint.

There are limits I haven't solved. There is no offline support, which matters more in a yard than I appreciated when I started. The task engine is purely server-side, so there are no push notifications when it's time to water. The drag-and-drop canvas is fluid for small beds and degrades on very large ones. The companion planting logic is currently a join table when it deserves a proper graph data structure. I know what each of these fixes looks like. They are queued.

The discipline that made the project actually ship was unglamorous. I wrote a 28-step execution plan at the start. I followed it phase by phase, ran a build after every phase, and fixed whatever broke before moving on. No skipped steps. No "I'll fix it later" debt that compounds and eventually buries the project. It sounds boring. It is the only reason the app is in production.

My sister now has a tool that remembers her garden layout, tracks what she planted and where, and tells her when to cover her pansies before the frost hits. It solves problems for exactly one person, which I suspect is why it works as well as it does. The clearest design constraint I have ever had on a project was knowing the user well enough to call her on the phone when I was unsure.

Building for an Audience of One screenshot 1
Building for an Audience of One screenshot 2
View Project
Next.js 14SupabaseTailwind CSSLLMsdnd-kitResend
Products
13 Nov 2025

A Bedtime Tool, Iterated in Use

For a long time my five-year-old daughter Mira wanted the same story every night. The cast was fixed. SparklyButt the rainbow unicorn doing antics and finding new friends along the way. Ron the dinosaur. Chef Pep the talking pizza chef and Slice the small talking pizza. Boo the friendly ghost. The plots changed; the cast didn't, much. When my three-year-old joined the ritual, the world expanded to include King Throne the toilet king, Sir Squirm the fancy lizard, Wizard Zag, Roy the silly rainbow, Puff the talking pillow, and Carl the talking castle. The nightly story had become a small ceremony in our house with a roster I could not have invented on my own.

I started using a chatbot to help me generate the stories. It worked beautifully for about thirty-five nights. Then it started to fall apart in a way that taught me something. The model would forget what color SparklyButt was. Ron would lose his pajamas. By story forty I was spending more time correcting the model than telling the story. The ceremony was turning into a chore. The tool wasn't bad; the use case had outgrown what a chat session could hold.

That weekend I built DreamWeaver. Narrow on purpose. Store the character specs the way I want them. Define a few scenario templates. Generate the story and its accompanying images in one pass without me having to remind anyone of anything.

The single hardest problem was visual consistency, and it was hard in a way I hadn't expected. Language models hold character descriptions in narrative reasonably well. Image models do not. The tenth picture of SparklyButt looked nothing like the first. I was using Google's Nano Banana for the images, and the work that mattered turned out to be the prompt engineering, not the model choice. I locked down silhouette, palette, posture, and lighting in language so precise the model had no room to drift. After enough iteration I was getting ninety-nine percent visual consistency on every generation. The lesson was that consistency, not creativity, was the actual product feature my kids cared about.

The app evolved through use. I built a character manager because I needed one. I added a silliness slider with three positions, mild, balanced, and crazy, because some nights called for gentle whimsy and others required the rainbow unicorn to do something deeply unhinged and I had no good way to communicate that to the model otherwise. Mobile-first was a logistical choice; bedtime happens on a phone in a dim room while one person tries to remain conscious. Night mode came after the very first session, when I turned off the lights, loaded the app, and Mira and I were blinded by a wall of white. The interface ended up tactile in a way I didn't plan, with sticker shadows and paper textures and soft transitions, because anything more clinical felt wrong in a sleepy five-year-old's hands.

The story generation is the easy part now. Making the experience belong in a particular room, at a particular hour, with two particular kids, took everything I had.

A Bedtime Tool, Iterated in Use screenshot 1
A Bedtime Tool, Iterated in Use screenshot 2
A Bedtime Tool, Iterated in Use screenshot 3
A Bedtime Tool, Iterated in Use screenshot 4
A Bedtime Tool, Iterated in Use screenshot 5
ReactPrompt EngineeringAccessibilityTailwind CSS
Products
11 Nov 2025

The Subway Tool That Got Me Hired

In 2013 I was living in Brooklyn and commuting to a job at Bank of America in midtown, riding the 4 train from Borough Hall to Grand Central twice a day. The commute itself was fine. What I wanted was advance warning when it wasn't going to be fine, so I could leave the apartment a little earlier or stay at my desk a little longer or just resign myself to being late to the gym.

The MTA's website at the time had a deterministic way of communicating delays. Specific lines on a specific page, with status text that updated when something went wrong. The problem was getting to that page when I needed it. Mobile data in 2013 was slow and expensive, the MTA's site was not really designed for phones, and pulling out my phone on a crowded platform to wait for a desktop site to render was not the experience I wanted. If I was away from my desk, getting an update was almost impossible.

I was also an early Kickstarter backer of the Pebble. When it arrived I was looking for real uses for it, the way I had been looking for real uses for every piece of hardware I had gotten excited about for years. I had written scripts to try to buy a Nintendo Wii in 2009, I had done the same for the Nexus 4 in 2012, and the Pebble was in that lineage. Those three pieces of hardware are roughly when tech stopped being a hobby for me and started being what I wanted to do for a living.

So I built the subway tool for the Pebble. The setup was less elegant than I would build today and more interesting because of it. I used Tasker on my Android phone to run a script on a schedule. The script scraped the MTA's status page, parsed the delay lines and the status details, and assembled them into a short message string. Tasker then handed that string to the Pebble Notifier plugin, which pushed it to the watch as a notification. There was no native Pebble app, no API, nothing on the watch side that knew anything about the subway. The watch was a glanceable display surface. All the work was happening on my phone.

The interesting design problem turned out to be what the message said. The first version just told me there was "subway delay," which was useless because it didn't tell me whether to care. The second version included the reason, something like "delay on 4, 5, 6 due to earlier train stall." The third version added the specific impacted lines as the lead, so I could tell at a glance whether it mattered for me that morning. That iteration took most of the three weeks I spent on the project. The watch part was the easy part.

The thing that broke the project, briefly, was that the MTA changed something on their website and my scraping script started returning garbage. I had been parsing for specific positions in the HTML, which is the wrong way to do it, and the fix was to make the script look for the right content rather than the right location. I should have known better. I learned to know better.

A few months later I was interviewing at Amazon. The interviewers asked what I liked about tech, and I said something about how the parts I cared about most were where it interfaced with daily life. They asked for an example. I held up my wrist. We talked through how I had built the thing, not in deep technical detail but enough that they could tell I actually had. The role was for a TPM at Amazon Cloud Drive, which would later become Amazon Photos. There was no real connection between what I had built and what the team did. I got the offer anyway.

I kept using the watch and the app for as long as I lived in New York, iterating it occasionally when the MTA changed their site, until eventually some Android apps started doing richer subway notifications natively and I let it go. I moved on from the Pebble before the company folded.

The lesson I took from it has stayed with me. Build for yourself first. Build inside a constraint that forces real decisions. Use what you build long enough to know what is wrong with it. None of it was sophisticated. It was the only project on my resume that I could talk about with the specificity of someone who had actually lived with the thing they made, and that turned out to be the thing that mattered.

The Subway Tool That Got Me Hired screenshot 1
WearablesPebbleTaskerAutomationScriptingProduct Design