A small self-hosted invoicing app: record customers, draft invoices, mark them paid, export to PDF via the browser's print dialog. Single-user by design, passwordless sign-in.
- Passwordless login — WebAuthn / passkey (Touch ID, Windows Hello, security key) with TOTP fallback
- Customers — bill-to address, default currency, default notes, per-customer invoice prefix and number format (e.g.
PLG{yyyymm},NWINV{seq:3}) - Sender profiles — your name and address printed at the top of the invoice
- Invoices — line items with optional tax, search, filters (customer, status), pagination
- Status — draft / sent / paid; overdue is derived from due date
- Row actions — view PDF, edit, duplicate, mark as paid / sent / draft, delete (3-dot menu)
- Print-to-PDF — invoice opens as a styled HTML page; export with the toolbar button or
Ctrl/Cmd+P - SQLite — single file (
data/app.db), no external services - Light & dark theme — toggle in the topbar, persisted in
localStorage
- PHP 8.3+, SQLite (PDO)
- lbuchs/webauthn — passkeys
- pragmarx/google2fa + bacon/bacon-qr-code — TOTP & QR
- Vanilla HTML / CSS / JS frontend (Inter / JetBrains Mono via Google Fonts), no build step
Requires PHP 8.3+ with extensions: pdo_sqlite, gd, mbstring, openssl, curl, json.
composer install --no-devStart the dev server:
php console serve # default 127.0.0.1:8000
php console serve 9000 0.0.0.0 # custom port + bind to LAN
composer serve # alias for `php console serve`Apply pending DB migrations explicitly (also runs automatically on first request):
php console migrateVisit http://127.0.0.1:8000 — the first request redirects to /install:
- Pick a username and display name
- Register a passkey
- Optionally add a TOTP backup
- Finish — you're signed in
- Customers (
/customers) — modal-driven add/edit - Senders (
/senders) — modal-driven add/edit; one is the default - Invoices (
/) — search, filter by customer/status, paginate; "+ New invoice" opens the editor - PDF — row menu → View PDF → toolbar Save as PDF (or
Ctrl/Cmd+P)
In Chrome's print dialog: set Margins → None and Background graphics → on for a clean A4 sheet.
public/ web root (router + /assets)
src/
Auth/ Session, WebAuthn, TOTP services
Controllers/ Invoice / Customer / Sender / Auth / Setup / Settings
Domain/ Db, Invoices, Customers
Http/ Kernel (router), View, CSRF, Icons
templates/ PHP view templates
data/ SQLite DB + install lock (gitignored)
migrations/ versioned schema/data migrations (run by console migrate)
console CLI entry-point (php console serve|migrate|help)
- WebAuthn requires HTTPS or
localhost. The RP ID is the request hostname with the port stripped — changing hostnames invalidates already-registered passkeys. - The full database (account, passkey credentials, TOTP secret, customers, invoices) lives in
data/app.db. Back it up. - Schema migrations are applied automatically on the first DB call (
Db::pdo()insrc/Domain/Db.php). - The
data/folder is gitignored except for.gitkeep. Anything you drop in there (*.db, custom scripts) stays out of git.