Vibe code your own FreshBooks
Vibe code your own FreshBooks as clients, invoices, and a public PDF. $23–$60 a month is rent you can cancel after one weekend of real bills.
You can vibe code your own FreshBooks as clients, invoices, and a PDF the client can open. FreshBooks is that product. The catalog line is “Invoices out, money in, spreadsheet retired.”
You pay $23–$60 a month for it. That is $276–$720 a year. The basic replacement is 10 hours: clients, invoices, paid tracking, and a PDF download.
I would ship this if I still rented an invoicing suite to bill people I already know. It is marked Weekend. The verdict on the card is “Send the invoice tonight”.
Last time I wrote about building a Canva alternative. That was a teardown. This one is a regular backlog topic. The useful v1 is a numbered invoice, not a set of books.
What you need to vibe code your own FreshBooks
The FreshBooks page splits the work into three rows.
The first row is the one you want this weekend. Clients with a currency and default payment terms. Invoices with line items, a tax rate, notes, and a number assigned at send time. A public page plus a PDF. A paid flag you set when the money lands.
The second row is 4 days. An expense log, receipt uploads, overdue states, and email sending. Those can wait until one real invoice already went from draft to paid.
The third row is 3 months and up. Bank feeds, payroll, double-entry books, taxes. The table labels that row as the part you should skip.
If you tell an agent to “clone FreshBooks”, it aims at the third row. Copy the prompt, then pin it to the first one.
Here is the shape of the data. Nothing else has to exist at 10 hours.
const client = {
id: 'cl-ada',
name: 'Ada Rossi',
email: '[email protected]',
currency: 'USD',
termsDays: 14,
}
const invoice = {
id: 'inv-bootcamp-oct',
clientId: 'cl-ada',
status: 'draft',
number: null,
dueOn: '2026-11-11',
taxBps: 725,
notes: 'October writing hours',
}
const line = {
id: 'li-posts',
invoiceId: 'inv-bootcamp-oct',
description: 'Four blog posts',
quantity: 4,
unitCents: 45000,
}
Ada is a client at Bootcamp.dev. Four posts at $450 is 180000 cents. Tax is 725 basis points, which is 7.25 percent. The number stays null until you send. Then the page and the PDF have to show the same total.
How does FreshBooks work
I am describing the catalog’s model here, not reverse-engineering FreshBooks. An invoicing tool is a list of people you bill, plus numbered documents, plus a record of what they already paid.
The screens on the page are a dashboard, an invoice editor, a public invoice, and an expense list. For 10 hours you can live with the editor, the public page, and a list of sent invoices. Outstanding totals wait for history, so the dashboard sits on the 4-day row.
The data model is Client, Invoice, LineItem, Payment, Expense. This weekend you need Client, Invoice, LineItem, and Payment. Expense belongs on the 4-day row.
The capability line is Astro, D1, and R2. This site is an Astro app on Cloudflare, so that is the path I default to. Node.js plus Astro is the catalog default. Use TanStack Start if React is your weekday language.
Every query is scoped by owner. A guessed invoice id from another account returns nothing: no client name, no amount, no 404 that confirms the slug. The public page uses a long random token, not the sequential number.
Who a FreshBooks alternative is for
The audience line on the page is specific. One freelancer billing a handful of clients.
The goal is a professional invoice, a clear unpaid list, and expenses you can log without an accounting degree. This weekend, that means one currency per invoice and the clients you already email.
If you need bank feeds, payroll, or a general ledger, keep paying. The site puts that rebuild at 3 months on purpose.
The same v1 also replaces Invoice Ninja and Bonsai if what you wanted was invoices you own. One focused build covers whichever of those you currently pay for.
Hours you later turn into a line item are Harvest, $13–$17.50 a person a month, about 4 hours for a timer and a weekly sheet. Keep the timer there. Paste the weekly total into the invoice by hand.
Invoice numbers with no gaps
The second mini-prompt on the page is “Invoice numbers with no gaps and no repeats”.
Drafts have no number. The number is assigned at send time from a per-owner counter. An abandoned draft never burns 2026-014. Two send clicks, or two tabs, must produce one sent invoice or two invoices with consecutive numbers. Never the same number twice.
Use a transaction.
async function sendInvoice(db, invoiceId) {
return db.transaction(async (tx) => {
const invoice = await tx.getInvoice(invoiceId)
if (invoice.status !== 'draft') {
throw new Error('already sent')
}
const number = await tx.nextInvoiceNumber(invoice.ownerId, invoice.year)
const token = crypto.randomUUID()
await tx.markSent(invoiceId, {
number,
token,
sentAt: new Date().toISOString(),
})
return tx.getInvoice(invoiceId)
})
}
After send, the number and the amounts are immutable. A typo becomes a credit-note invoice that points at the original. You do not edit 2026-014 after Ada has seen it.
A prefix and a yearly reset are fine. 2026-001 on January 1 starts a new counter. The increment is still atomic.
The public invoice page
The third mini-prompt is “The public invoice page nobody can guess”.
Ada opens a URL with a long random token, at least 128 bits, generated per invoice. The sequential number stays off the path. Guessing .../15 after .../14 would leak your client list.
Serve the same 404 for a wrong token and a missing invoice. Rate-limit lookups. Make the token revocable if a link leaks, and reissue a new one.
Status is computed on the server at request time. Overdue means today is past dueOn and the unpaid balance is above zero. The banner can show days late. The browser does not get to send a status.
The page has to print. The PDF has to match it. I would render one HTML document and print-to-PDF from that. Two templates will drift by a line item the first week.
Money math without a float
The first mini-prompt is “Money math without a single float”.
Every amount is an integer in minor units. $450.00 is 45000. Quantity times unit price is still an integer if quantity is a whole number. Tax is basis points against the subtotal, rounded once.
Pick the policy and write it down. The catalog’s prompt uses per-line totals first, then tax on the subtotal, round-half-up. The printed lines must sum to the printed total.
function invoiceTotal(lines, taxBps) {
const subtotal = lines.reduce((sum, line) => {
return sum + line.quantity * line.unitCents
}, 0)
const tax = Math.round(subtotal * taxBps / 10000)
return { subtotal, tax, total: subtotal + tax }
}
invoiceTotal([{ quantity: 4, unitCents: 45000 }], 725)
//{ subtotal: 180000, tax: 13050, total: 193050 }
$1,800.00 plus $130.50 tax is $1,930.50. Compute that on the server from stored line items. Never trust a total the browser posted.
Payments are separate rows. After each one, recompute the balance from scratch: sum of non-voided payments versus the locked invoice total. Two half payments flip to paid on the second one. An overpayment stays on the row as a credit you can see. A mistaken payment is a void, not a delete.
What not to build in a FreshBooks clone
Leave out bank connections, payroll, double-entry books, and tax filings.
The non-goal on the page already names those. You are replacing the invoices you currently rent.
Bank feeds are the trap. A login to every bank and a reconcile screen that wants to be QuickBooks is a compliance product. Paste the skip row when the agent starts a “connect your bank” button.
Expenses belong on the 4-day row as a dated list with a category and a receipt file. Receipts stay private behind your login. The public invoice token never opens one.
If the agent starts payroll or a chart of accounts, stop it and paste the non-goal again.
FreshBooks alternative or a timesheet
A bill is not a timer. Harvest is $13–$17.50 a person a month. Start, stop, a weekly grid. Its skip list already says do not build invoicing.
Reimbursements belong in Expensify, $5–$9 a person a month, about 12 hours for a receipt, a category, and a CSV. That money is leaving the company. An invoice is money coming in. Keep the two lists apart.
A quote Ada accepts before you bill is Better Proposals, $13–$42 a person a month, about 12 hours for templates, line items, and a share link. The accepted snapshot can become the invoice lines. Do not add e-sign to v1.
The deal that produced the work is HubSpot CRM, $20–$150 a person a month, about 4 days. The HubSpot walkthrough is contacts, deals, and a board. When the deal is won, open the invoice by hand. Sync can wait.
Household envelopes are YNAB. The YNAB walkthrough is accounts, categories, and typed transactions. That is spending you already have. This post is money someone still owes you.
The 4-day row, after one real payment
Use the tool for two billing cycles as the real inbox, not as a demo. Then look at the second estimate.
An expense log with categories, receipt uploads, and monthly totals next to income. Overdue computed on the server, with days late on the dashboard. Email at send time, from your own transactional provider, with the public link in the body.
The catalog says 4 days for that row. Stop there. The prompt post is the extra line that says do not connect a bank.
Host it, send a real invoice, cancel
The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.
This one wants Astro, D1, and R2, because that is the stack on the card. There is a table for invoices and payments, so take a backup before you delete the paid account. Receipt files later go in R2.
Put the app on a hostname you already own, bills.flaviocopes.com. Add the three clients you invoice this month. Draft one with the real line items. Send it. Open the public link in a private window. Record the payment when it lands. Confirm the editor, the public page, and the PDF all show 2026-014 and the same total.
If you miss overdue banners or an expense list, add them from the 4-day row.
Then cancel FreshBooks, or Invoice Ninja, or Bonsai, whichever one you pay for.
I would not start here as a first vibe-coded app. The first-project post still points at a tonight card. This is the weekend after you have shipped one small tool.
The scope post is the same three-row table if you want the habit, not just this card.
Start now
Go to Build your own FreshBooks. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate only: clients, invoices, paid tracking, PDF download. Ten hours.
Send one real invoice this weekend. Record the payment when Ada pays it.