PavtiPro is a billing app for jewelry shops. A salesperson builds an invoice in three steps, adds items like gold, silver or diamond pieces, takes payment, and prints a customer copy and a merchant copy. My teammate built it first as a Next.js web app on Supabase. It worked, but it needed a hosted database and an internet connection to bill a customer.
My part was turning it into a desktop app that runs fully offline. The data now lives in a SQLite file on the shop's own machine, and installers for Windows, macOS and Linux are built and attached to a GitHub release automatically.
What it does
Invoices
Multiple items per invoice, with design number, gold colour, carat, grams and stone weight. 5% VAT is worked out per item and per invoice.
Payments
Cash, credit card, debit card and old gold, split across methods if needed. Advance payments are tracked against the remaining amount due.
Edits and cancellations
An edited invoice links back to the original and records who changed it. Cancellations keep a reason and a timestamp.
Tax-free sales
Tourist details are stored per invoice for tax-free refunds. The request format for the refund provider is built and validated, but not yet sent anywhere.
Who built what
- Invoice builder, payments, advance payments, print layout, edit and cancel logic, tax-free data
- Teammate
- Supabase web version, first Tauri shell and dark mode
- Teammate
- SQLite schema and data layer, local login with bcrypt
- Me
- Static export for the desktop build, cross-platform installers and automated releases
- Me
How it works
Next.js app
Frontend
- Next.js (static export) | Tauri window | Plain files
Repository layer
Data access
- tauri-plugin-sql | SQLite (local machine) | Same interface as before
Database
pavtipro.db
- Nine tables | Created on first launch | Schema migrations
Static export
Tauri loads plain files, so Next.js server actions and middleware had to go. I switched the app to a static export and removed the server-only code.
Same repo interface, new storage
The pages already talked to a set of repository modules. I rewrote those
modules to run SQL through tauri-plugin-sql instead of calling Supabase, then
moved the remaining pages that still called Supabase directly.
Schema on first launch
A database singleton creates the nine tables if they don't exist, then runs a
short list of ALTER TABLE migrations. A migration that fails because the
column already exists is skipped.
Local auth
Supabase Auth was replaced with bcrypt password hashes in SQLite and a small session record in local storage.
Shipping it
release-please
Reads conventional commit messages, opens a release PR, and tags a version when it is merged. The app went to 2.0.0 for the offline version, then 2.1.0.
Build workflow
Runs on each published release. A matrix builds the Windows (.exe, .msi),
macOS (.dmg) and Linux (.AppImage, .deb) installers and uploads them to
the release.
Cleanup
I removed 27 debug, test and setup pages left over from the web version. They depended on Supabase and broke the static build.
Problems worth telling
Cold-start hang
- Problem
- On first launch the app sometimes froze on a blank screen. The Tauri bridge wasn't always ready when the app tried to open the database.
- Fix
- I added a timeout and up to three retries, and made the login check treat a failed load as 'signed out' instead of waiting forever.
Dynamic routes in a static export
- Problem
- Pages like a single invoice view use an ID in the URL, which a static export can't pre-render.
- Fix
- I fixed these along with the type mismatches that surfaced once Supabase's generated types were gone.
Pinned Rust version
- Problem
- CI failed because a dependency needed Rust's 2024 edition.
- Fix
- Unpinning the toolchain fixed it.
What's next
- Send tax-free requests to the provider. The builders, validators and a test against sample requests are already in place.
- Work through the refactor list I wrote after the migration. Top of it: splitting the new-invoice page, which is about 2,600 lines, and reading the business name, tax number and VAT rate from settings instead of code.