March 13, 2025

Vibe coding an invoice app

Most invoice tools didn’t work, so I vibed my own

Fresh is a home-cooked invoicing app
Fresh is a home-cooked invoicing app I built for my own invoicing needs.
💡

tl;dr

  • I couldn’t find an invoice tool that worked for me, so I built Fresh as an invoice tool purely for my own needs
  • Built on Hono, Alpine, Tailwind and Groq, and deployed on Deno Deploy
  • Choosing the right stack and project structure makes a big difference for vibe coding

Invoices are the worst part of consulting.

There are plenty of free invoice generators to choose from. But if I use Stripe Invoices, I can only get paid through Stripe. If I use Wise Invoices, I can only get paid through Wise.

But clients in the real world might choose to pay through anything from PayPal to Wise to ACH or Interac or Venmo or even checks (thanks, Academia!).

Anyone who’s ever worked with clients can tell you that if a client wants to pay you in that certain way, you make sure you can get paid that way!

So what I do instead, is create multiple templates in Google Docs. I duplicate that template for each client, modify the details, and send it over. That gets really tedious, and makes the data annoying to track.

It’s 2025. Why don’t we have great invoice apps?

Well, it turns out that invoice apps are — like todo apps and weather apps and currency apps — in a special, awkward category of apps that are both too easy to build, yet too hard to build well. This means that no one bothers to build free invoice tools. This also means that the only free invoice tools are also lead generators for companies like Stripe and Wise.

And that’s why even in 2025, we’re stuck with using Google Docs for invoices.

Enter vibe coding

With Cursor, Windsurf, and a whole slew of new AI-coding tools we’ve entered a new golden age of vibe coding any home-cooked apps into existence.

Just like home-cooked meals, we can build our own apps as spicy as we’d like, without worrying about any other users.

And that’s why I made Fresh.

A sample invoice on
A sample invoice on Fresh. Invoices can be started from scratch, loaded from an example, generated with AI, or parsed from a PDF or Word file via OCR.

Fresh vibes and home-cooked apps

I designed Fresh to be the fastest invoice tool for myself.

From the ground up, I made sure I could save and load invoices. I wanted to be able to start from an example, recreate from OCR, or use an LLM to draft and estimate hours and generate a realistic invoice based on a simple description. I needed granular undo/redo. I wanted Print Preview. Save to PDF. Calculated Tax rates.

I wanted custom logos to show up at the top left.

I wanted to be able to post a Venmo, Wise, or other payment link — and for a QR Code to appear.

Designers are frequently berated for feature creep — adding new features without considering whether users need them. In this case, I am the user! Whatever I needed, I could just add!

Whatever I didn’t want — features that are common to software, like sign up and log in — I could just leave out!

Most products are designed to appeal to millions of users. For this, they need validation, user research and polish. For Fresh, I don’t need millions of users. I just need myself to love it. Fresh might be a complete nightmare for most users. If it were a paid product, it might have zero paying customers.

But with home-cooked, vibe coded apps, it doesn’t matter anyone. We can cook our perfect meal for ourselves.

Behind the vibes

From start to finish, the project took around two weeks to build. The base functionality actually only took a couple of hours! Most of the time was spent on design, animations, and on progressively refactoring and rebuilding the project to accommodate the growing feature set.

One peculiar behavior with the rise of AI-generated code, is I’m less inclined to “protect” code I’ve previously written, and more excited to give projects a fresh start.

With AI coding tools — because the price of writing code is now so cheap — I’m finding myself excited to “throw away” code and start projects from scratch, if projects start becoming unwieldy.

While Fresh started out as a React app, it was at one point rewritten as an HTMX app, and then into its final resting architecture, as a Hono/Alpine.js app.

The combination of Hono and Alpine neatly separates the UI templating, UI logic, and server functionality in a way that makes the code easy for both me and the AI to understand. The project is split into a main Hono file, a core Alpine app, Alpine utility functions, server functions, and (Alpine-optional) html templates.

/ ├── main.js # Entry point & Hono routes ├── alpine.js # Client-side UI functionality ├── /server # Server-side API handlers ├── /templates # html templates │ └── /partials # Reusable html components └── /utils # Alpine utility functions and helpers

This structural split makes it easy to select which semantic parts of the application to work on. If you wanted to work on animations, you’d select the right templates and partials, and create the corresponding functions in Alpine to handle start and stop events. If you wanted to make the AI-chat component work, you’d create the server-side functions, and hook up the right Alpine utility functions to make those API calls. You can then edit the template to hook up the Alpine functions.

This kind of contextual split makes it easy to not stuff too much context into Cursor. When you give the AI too much code, it gets confused and makes a ton of mistakes.

Separating the code structurally in a way to be able to feed the appropriate templates and logic based on what features you want to build is really powerful.

Compare this structure with React/Next apps for example, which combines more logic and layout into structural components. This structure makes sense for massive projects where you might have dozens to hundreds of components. For smaller “home-cooked” apps, I find that splitting the layout templates from the logic itself be easier to feed to the AI for code generation.

Using lightweight tools like Alpine is also easy for deployment — you don’t need heavier tools like Vercel, and instead can deploy on services like Val.town or Deno Deploy. You can even omit server-side Hono completely, and serve the entire Alpine app as a static html page.

(I like being able to access my own tools from anywhere, so I chose for it to be hosted. I also chose the Deno Deploy/Hono combination so I get nice logging and other perks)

Where AI comes in

At the risk of being ostracized from the league of UX designers, Chat UI actually has its time and place!
At the risk of being ostracized from the league of UX designers, Chat UI actually has its time and place!

The first few versions of Fresh actually didn’t contain any AI elements.

What? That’s right! I actually decided to build an AI-free tool (for once). After all, why do you need any sorts of AI in an invoicing app?

Well, trouble turned up when I was figuring out how to convert Word and PDF files to the invoice schema. It just turns out that using an LLM for extracting invoice data into JSON is way easier than any other method. It also turns out that using a service like Groq makes it pretty much free. At first I was hesitant to use an open source Llama model (llama-3.3-70b-versatile — compared to the smarter but way pricier Claude models), but the error rate is very low (I haven’t come across any errors yet, but my sample size is tiny and I don’t have evals. I also always check each uploaded document, since getting invoices right is kind of critical).

It also turns out that many PDFs are just jpeg wrappers… many don’t contain any text! This means that OCR is necessary to extract any kind of data. There are a ton of OCR options out there. You can use OpenAI, Gemini, Claude, or Mistrals OCR models. You can use old school OCR tools. You can use Datalab’s Marker OCR tool (the apparently undisputed king of OCRs). I chose Groq’s Llama 3.2 vision model (llama-3.2-90b-vision-preview) because… it’s fast, and pretty much free. I can choose a cheaper model because I know I’ll review the OCR’s outputs every single time. The OCR is just there to fill in the template for me.

The other feature I was surprised I wanted was… chat?

As a real UX designer, I should be raging against chat. But it turns out that a Chat UI is actually provides some benefits. It’s a simple way to show the thinking state of the AI: in this case it tells me what the AI did and why. It’s also a great way for the AI to output the invoice data itself. In my case, I’m displaying the changes in the keys/values generated by the AI, to give me a quick snapshot of what changes have been applied by the AI. Another benefit of course is being able to chat with the AI model to give it corrections, updates and changes.

You can just ask the AI for changes — including estimates and whatever other kinds of changes you need. Sometimes this is faster than filling out form fields…!
You can just ask the AI for changes — including estimates and whatever other kinds of changes you need. Sometimes this is faster than filling out form fields…!

The most surprising feature I found myself needing was chat: it can give you very realistic invoice estimates based on a rough prompt that you give it. You can actually use this system to estimate the size and scope of jobs.

You can discuss and estimate new line items and make adjustments with the help of the AI — something I didn’t anticipate I needed initially, but has been really useful.

A future of vibes?

So a confession — I didn’t completely vibe code Fresh.

I refactored it manually; I rejected many of Claude’s changes; I called Claude dumb many times, rolled up my sleeves, and fixed bugs manually.

So I didn’t completely code Fresh with vibes. But before Claude and Cursor, I’d never used htmx before. I’d never used Alpine before. I’d never built a PDF preview and saving system, and I sure as heck never used Canvas before (those are some menacing API docs).

Canvas-based PDF previews lets me style the invoices with TailwindCSS. What you see on the preview is what you get! Before Claude/Cursor I’d never considered doing anything in Canvas.
Canvas-based PDF previews lets me style the invoices with TailwindCSS. What you see on the preview is what you get! Before Claude/Cursor I’d never considered doing anything in Canvas.

I ask for features, I generate code, and then I think about the code and restructure much of it manually (AI is terrible at refactoring and fixing code).

I think periodic restructuring is absolutely vital for the system to not go absolutely bonkers. And I fear for those who purely code by vibes. AI has a tendency to patch mistakes with layers and layers of duct tape, never fixing the leaks themselves. And in the end you have a mess that’s impossible to untangle — even with the help of AI.

There’s a whole lot of features I’d love to see in Fresh — an updated invoice data model; a proper sidebar of saved invoices; a database I can sync my invoices to; a Claude Artifacts-like system for showing multiple versions of invoices; currency conversion with live currency data; sharing editable links with clients, and a whole lot more.

Building those out wouldn’t necessarily be hard, but they would take time and mental energy. What’s nice about vibe coding your own home-cooked apps is that you can just choose to stop building features when you want. You don’t have to improve the UX, or expand the feature set.

With home-cooked, vibe coded apps you can just be satisfied with what you cooked, and call it a day!