Nicojuhari logoNicojuhari
All work

EPUfix

An invoicing and bookkeeping app for self-employed people in Austria, built so the money rules live in the database, not only in the UI.

Role
Solo founder and engineer: product, design, code, operations
When
2026 - now
  • Next.js 16
  • React 19
  • TypeScript
  • Supabase (Postgres)
  • Stripe
  • next-intl
  • Tailwind CSS
  • Vitest
  • Vercel
Postgres tables
20
database triggers
42
migrations
31
lines of TypeScript
~43k

The problem

One-person businesses in Austria (EPU) juggle offers in Word, invoices in Excel and receipts in a folder. Once a year they hand it all to a tax advisor.

I run two small businesses myself, so I built the tool I needed: an offer becomes an invoice, a payment becomes income, and each income lands in the right line of the Austrian tax form (E1a). It is in German and English.

What I built

  1. The database enforces the rules

    Every table has row-level security, so a user only ever sees their own workspace. Workspaces are created by one database function that also decides who gets a free trial. Users can't write billing fields at all.

    Access after a subscription ends is checked by triggers on every table you can change. If a server action forgets a check, the database still refuses the write.

  2. Money moves in one step or not at all

    Turning an offer into an invoice, or recording a payment, runs as one Postgres function. It locks the row, checks the state, copies the items, assigns the next invoice number and books the income together.

    Invoice status (unpaid, partly paid, paid) is owned by a trigger. Once a payment exists, the invoice lines and the payment booking are locked. Line totals are rounded by one rule that exists twice, in SQL and in TypeScript, and both give the same result.

  3. Austrian invoice law as code

    An Austrian invoice needs different details depending on its size and the seller: small-business VAT exemption, reverse charge for EU business clients, the €400 and €10,000 thresholds, service dates, tax numbers.

    These rules are plain functions that block an incomplete invoice and print the legal note it needs. Issued invoices can't be deleted, because the law says they must be kept.

  4. A tax estimate you can follow

    Income tax and social insurance (SVS) are calculated from year-by-year tables for 2024 to 2026, checked against the official forms. The SVS part models the provisional payments and the back-payment that follows in later years.

    One function feeds both the dashboard and the tax report, so the two numbers can never disagree. It is always shown as an estimate.

  5. Stripe webhooks that survive the real world

    The webhook checks the signature, records each event so a retry is never processed twice, and reads the subscription fresh from Stripe, because events can arrive out of order.

Trade-offs

  • No PDF library: invoices print from one shared layout with print CSS. Less code, and the PDF looks exactly like the screen.
  • A small ZIP and CSV writer in-house instead of new dependencies. The tax advisor package (3 CSV files in a ZIP) is built in the browser.
  • Multi-currency was built, then removed. A guarded migration refused to run if any non-euro data existed.
  • Clear scope: not accounting software, no bank sync. Those would make the product harder to trust, not easier to use.

What I’d do next

The test suite is thin (Vitest on the tax math and formatters). Next: tests around the invoice and payment functions in the database.

Want to see the code?

I’m happy to walk you through it in an interview.

Next case studyIdeal Credit Management