A small, self-hosted invoicing app: clients, invoices with line items, clean PDFs, email delivery, a dashboard of unpaid invoices, and nightly backups. Runs on localhost only — no accounts, no client portal, no telemetry.
Why not Invoice Ninja? Invoice Ninja is open source and self-hostable, and it's a great platform. This project is the lighter option for someone who just needs to send invoices — a handful of files and one database, not a platform.
- Clients — name, email, address, currency, default rate (prefills unit price on new invoices)
- Invoices — line items (description, qty, unit price, tax %), auto-numbered, notes, status tracking (draft → sent → paid)
- PDF per invoice — generated with puppeteer from an HTML template: your logo, payment terms, and bank details (all from
config.json) - Email — sends the PDF to the client via SMTP (creds in
.env), marks the invoice as sent - Dashboard — unpaid invoices with days outstanding
- Razorpay payment links — optional per invoice; shown on the PDF and in the email. You mark invoices paid by hand when money lands
- Nightly backups — SQLite copied to
backups/YYYY-MM-DD.db, last 30 kept
Out of scope by design: quotes, expenses, multi-user, automatic payment reconciliation.
npm install
cp .env.example .env # fill in SMTP settings
# edit config.json — business name, logo, payment terms, bank details
npm start| Key | Required | Purpose |
|---|---|---|
SMTP_HOST |
yes (for email) | SMTP server, e.g. smtp.gmail.com |
SMTP_PORT |
no (587) | Port; 465 switches to TLS |
SMTP_USER / SMTP_PASS |
yes (for email) | SMTP login |
SMTP_FROM |
no | From address shown to clients |
CHROME_PATH |
no | Chrome/Edge executable for PDFs (auto-detected if unset) |
RAZORPAY_KEY_ID |
no | Only used by npm run paylink |
RAZORPAY_KEY_SECRET |
no | Only used by npm run paylink |
business_name,logo_path— drop alogo.pngin the project root (or point elsewhere). Without a logo the business name is rendered instead.payment_terms,bank_details— printed on every PDF footer.footer_note— closing line on the PDF.
The PDF is rendered from invoice-template.html — plain HTML + CSS, printed with Chrome. Placeholders ({{INVOICE_NUMBER}}, {{BILL_TO}}, {{ITEMS}}, {{TOTAL}}, {{PAYMENT_TERMS}}, {{BANK_DETAILS}}, {{PAYMENT_LINK}}, ...) are replaced at render time. Edit the CSS or markup, refresh the PDF — no rebuild needed. node tools/pdf-verify.js <pdf-path> renders any generated PDF to a PNG and checks for text overflow — useful after template edits.
Invoices are numbered INV-YYYY-NNN — the year comes from the issue date, and the counter is sequential per year (INV-2026-001, INV-2026-002, ...). It resets each January.
Click Send email on an invoice: the PDF is generated and attached, and the status becomes sent. The client's email address must be set.
Two ways:
- Auto —
npm run paylink -- INV-2026-001creates a payment link for the invoice total via the Razorpay API (RAZORPAY_KEY_ID/RAZORPAY_KEY_SECRETrequired) and stores it on the invoice. Note: Razorpay payment links cap at ₹50,000 per link (account-level); for larger invoices use the manual option. - Manual — paste any link into the Payment link field on the invoice edit form.
Either way the link appears in the PDF and the email. When money lands, mark the invoice paid by hand.
The app backs up to backups/YYYY-MM-DD.db when it starts and once per day while it keeps running, keeping the 30 newest files. For a machine that's off at night, schedule npm run backup (e.g. Windows Task Scheduler, daily):
cd C:\path\to\lovoice && npm run backup
Node + Express + better-sqlite3 (single file, zero setup), EJS server-rendered pages, puppeteer-core (uses your installed Chrome/Edge — no browser download), nodemailer. Binds to 127.0.0.1:4840.
lovoice now runs as a SaaS: sign up → free forever (clients, invoices, PDF, email) → ₹499/mo Pro unlocks the payment-recovery workspace (receivables dashboard, auto-dunning at 0/3/7/14 days, Razorpay UPI payment links).
How it works:
- Auth — scrypt password hashes, stateless HMAC-signed session cookies (
tenants.js; stdlib only, no deps). - Multi-tenancy — one SQLite file per user (
data/u<id>.db) instead of a shared schema. Every existing query works untouched, isolation is perfect, backups are per-tenant. Ceiling ~1k tenants; merge into a sharded DB beyond that (ponytail, noted in code). - Billing — Razorpay payment links (the account's Payment Links API is enabled; the Subscriptions API returns 401 feature-gating, so Pro is billed via a ₹499 link per month).
POST /billing/webhookHMAC-verifiespayment_link.paidand grants 30 days rolling. Without keys, Subscribe runs in demo mode and activates locally. - Plan gate —
/receivables*,/invoices/:id/paylink, and the chase routes require Pro (requirePro); free users get bounced to/billing?pro=1. The hourly dunning cron only ticks Pro tenants. - Admin —
ADMIN_EMAIL/ADMIN_PASSin.envprovisions the owner on first boot and adopts the pre-SaaSdata/invoices.dbas their workspace, so existing data survives the upgrade.
fly launch --no-deploy
fly volumes create lovoice_data --size 1 --region bom
fly secrets set SESSION_SECRET=... ADMIN_EMAIL=... ADMIN_PASS=... \
RAZORPAY_KEY_ID=... RAZORPAY_KEY_SECRET=... RAZORPAY_WEBHOOK_SECRET=... \
SMTP_HOST=... SMTP_PORT=... SMTP_USER=... SMTP_PASS=... SMTP_FROM=...
fly deployThen in the Razorpay dashboard → Settings → Webhooks, point payment_link.paid at https://<app>/billing/webhook with RAZORPAY_WEBHOOK_SECRET. (Render free works too but its disk resets on every redeploy — the tenant DBs would be wiped.)
cp .env.example .env # fill in ADMIN_EMAIL/ADMIN_PASS/SESSION_SECRET
npm start # → http://localhost:4840 (login with the admin account)Node + Express + better-sqlite3 (single file, zero setup), EJS server-rendered pages, puppeteer-core (uses your installed Chrome/Edge — no browser download), nodemailer. Binds to 127.0.0.1:4840 (or HOST/PORT env, e.g. 0.0.0.0:8080 on Fly).