Vibecode Invoice Ninja
track this build8 phases, 16 steps, beginner friendly0%Invoice Ninja is open source, so the hosted subscription can be replaced by self-hosting if you can handle ops, updates, payment config, and backups.
You are building a lean indie version of Invoice Ninja.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Invoice Ninja · indie build
A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | forms, PDFs, mail: the framework earns its place |
| Money | Integer cents | an invoice that rounds differently from your books is a conversation you do not want |
| PDF | Puppeteer rendering an HTML template | your own template, exact layout; a full browser is the cost |
| Hosting | localhost, or a VPS behind Caddy | invoices are private; localhost is enough for one person |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **Your business details and a logo** · free
- Why: Every PDF carries them: legal name, address, tax id if any, bank or payment details, payment terms, and a logo file.
- Get it: Write them into config/business.json; a PNG or SVG logo at least 400px wide.
- [ ] **Puppeteer's Chromium download (about 170 MB)** · free, 170 MB of disk
- Why: PDF rendering needs a browser. npm install puppeteer downloads it; on a small server you may need extra system libraries.
- Get it: npm install puppeteer downloads Chromium. On Ubuntu servers install the libraries Puppeteer's troubleshooting page lists.
- Verify: node -e "require('puppeteer').launch().then(b=>b.close())" exits without error
- [ ] **SMTP credentials to send invoices** · free tiers exist
- Why: Phase 5 emails the PDF to clients.
- Get it: Fastmail, Postmark or your mail host: host, port, user, password as an app password.
- [ ] **A Stripe account for payment links (optional)** (optional) · free; fees per payment
- Why: Phase 6 puts a pay-online link on each invoice.
- Get it: dashboard.stripe.com > Developers > API keys. Use test mode until the flow works.
- [ ] **Your tax rates and numbering rule** · free
- Why: This app does arithmetic on rates you type in; it does not know your jurisdiction. Decide the rates and the invoice number format now.
- Get it: Write down the VAT or sales tax rates you charge and the prefix format, e.g. INV-2026-001.
## Quick start
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
- managed hosting
- upgrades
- backups
- support
- payment setup convenience
- reduced ops risk
If one of those is essential to you, that is the reason to keep paying for Invoice Ninja, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Invoice Ninja
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a small invoicing app to replace hosted Invoice Ninja. Build it in
phases, in the order below. Do not write the whole app in one pass. Finish a
phase, run its "Done when" check, fix what fails, and only then start the next
phase.
### Before you start
Invoice Ninja is itself open source and self-hostable. If you want the whole
platform · quotes, expenses, projects, a client portal · deploy theirs and stop
reading. This build is the lighter option for someone who needs invoices, not a
platform. Say that in the README rather than pretending this is a replacement for
all of it.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost:4840.
- Money as integer minor units (cents). Never a float · an invoice that rounds to
a cent differently from your accounting system is a conversation with a client
you do not want to have.
- Per-invoice currency stored on the invoice, never inferred at render time.
### Data model (create this before Phase 1)
- `clients`: id, name, email, address, currency, default_rate_cents, notes
- `invoices`: id, number (unique), client_id, issue_date, due_date, currency,
status ('draft' | 'sent' | 'paid' | 'void'), notes, terms, sent_at, paid_at,
stripe_link, snapshot (JSON)
- `line_items`: id, invoice_id, position, description, quantity, unit_price_cents,
tax_rate
`snapshot` is not optional. When an invoice is sent it must freeze the client's
name, address and every line as they were at that moment. Editing a client's
address must never change what a sent invoice says · that is a document you gave
someone, and silently rewriting history is the bug that ends in a dispute.
### Phase 1 · Clients
Build: client CRUD with currency and default rate.
Done when: a client saves and reloads with the currency intact, and deleting a
client with invoices is refused rather than cascading.
Do not build yet: invoices.
### Phase 2 · Invoices and line items
Build: invoice creation with line items, quantity, unit price and per-line tax
rate. Compute the subtotal, tax and total in integer cents, rounding once at the
end. Auto-number as `INV-2026-001`, allocated inside a transaction so two
invoices created in the same second cannot collide, and never reused after a
delete · gaps are fine, duplicates are not.
Done when: a three-line invoice with mixed tax rates totals correctly to the
cent, two invoices created concurrently get distinct numbers, and deleting the
latest invoice does not cause the next one to reuse its number.
### Phase 3 · Status and the snapshot
Build: the lifecycle · draft is editable, sending freezes the snapshot and makes
it read-only, paid and void are terminal. Editing a sent invoice must require an
explicit "revise" action that voids and reissues rather than mutating.
Done when: changing a client's address after sending leaves the sent invoice's
PDF and HTML unchanged, and a sent invoice cannot be silently edited.
### Phase 4 · PDF
Build: a clean PDF per invoice rendered from an HTML template with Puppeteer ·
logo, terms, bank details from a config file. Render from the snapshot, not from
live joins. Puppeteer pulls a full browser; if the deploy target cannot carry
that, say so and use a lighter HTML-to-PDF path, but do not hand-assemble PDF
syntax.
Done when: the PDF is one page for a short invoice, totals match the screen
exactly, it opens correctly in a phone mail client, and regenerating a sent
invoice's PDF a month later produces a byte-identical document.
### Phase 5 · Email
Build: send the PDF over SMTP from `.env`, record `sent_at`, and keep a log of
send attempts with their errors. Never mark an invoice sent when the send failed.
Done when: a real send arrives with the PDF attached and readable, a failed send
leaves the invoice in draft with the error visible, and the same invoice can be
resent without renumbering.
### Phase 6 · Payment links
Build: an optional Stripe payment link per invoice (key in `.env`), included in
the PDF and the email. Payments are marked paid by hand when the money lands ·
building webhook reconciliation is a bigger job than it looks and getting it half
right means an invoice marked paid that is not.
Done when: the link opens a correct amount in the right currency, and the manual
mark-paid records `paid_at` and locks the invoice.
### Phase 7 · Dashboard and books
Build: unpaid invoices with days outstanding, totals by month and by client, and
a CSV export of everything · the export is what makes this handover-able to an
accountant, and it is the difference between a tool and a trap.
Done when: days outstanding is correct across a month boundary, and the CSV
reconciles with the invoice table to the cent.
### Phase 8 · Backup and deploy
Build: a nightly copy of the database plus the generated PDFs to `backups/`,
keeping 30, a documented restore performed once, and the README.
Done when: a restore brings back both the data and the PDFs of sent invoices.
### Out of scope (and why)
- Quotes, expenses, multi-user, and a client portal · that is the platform, and
the platform is free to self-host if you need it.
- Automatic payment reconciliation.
- Tax compliance. This app does arithmetic on rates you type in. It does not know
your jurisdiction's rules, and it is not e-invoicing compliant anywhere. Say
this in the README and check what your country requires before invoicing.
### README must contain
- The numbering rule, and why gaps are acceptable but duplicates are not.
- Where PDFs live and the note that they are outside the database for backups.
- The tax disclaimer, in plain language.
===== AGENTS.md =====
# Agent instructions · Invoice Ninja indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, Puppeteer rendering an HTML template, localhost, or a VPS behind Caddy. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Invoice Ninja
A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Clients
Clients with currency and rate, and a delete that refuses to orphan invoices.
### Steps
1. Create the project and the tables
clients (id, name, email, address, currency, default_rate_cents, notes), invoices (id, number unique, client_id, issue_date, due_date, currency, status, notes, terms, sent_at, paid_at, stripe_link, snapshot JSON), line_items (id, invoice_id, position, description, quantity, unit_price_cents, tax_rate).
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
2. Build client CRUD
### Done when
- [ ] A client saves and reloads with the currency intact
- [ ] Deleting a client with invoices is refused
## Phase 2 · Invoices and line items
Totals to the cent and numbers that never collide or reuse.
### Steps
1. Build invoice creation with line items
Subtotal, tax and total in integer cents, rounding once at the end.
2. Allocate numbers inside a transaction
INV-2026-001 style, never reused after a delete; gaps are fine, duplicates are not.
### Done when
- [ ] A three-line invoice with mixed tax rates totals correctly to the cent
- [ ] Two invoices created concurrently get distinct numbers
- [ ] Deleting the latest invoice does not cause the next to reuse its number
## Phase 3 · Status and the snapshot
Sending freezes the document; editing after requires a revision.
### Steps
1. Implement draft, sent, paid and void with the snapshot frozen on send
Copy the client's name, address and every line into snapshot; render sent invoices from it only.
2. Add a revise action that voids and reissues under a new number
The only way to change a sent invoice.
### Done when
- [ ] Changing a client's address after sending leaves the sent invoice's PDF and HTML unchanged
- [ ] A sent invoice cannot be silently edited
## Phase 4 · PDF
A clean one-page PDF from your template, byte-identical when regenerated.
### Steps
1. Install Puppeteer and write the HTML template
Logo, business details, line table, totals, terms and payment details from config/business.json.
Files: `templates/invoice.html`
```sh
npm install puppeteer@23
```
2. Render from the snapshot to PDF_DIR
Reuse one browser instance; close pages in finally.
### Done when
- [ ] A short invoice is one page
- [ ] Totals match the screen exactly
- [ ] It opens correctly in a phone mail client
- [ ] Regenerating a sent invoice's PDF later is byte-identical
## Phase 5 · Email
Sent means sent; failure leaves it a draft with the error visible.
### Steps
1. Send the PDF over SMTP_URL and record sent_at only on success
```sh
npm install nodemailer@6
```
2. Log every send attempt and allow resending without renumbering
### Done when
- [ ] A real send arrives with the PDF attached and readable
- [ ] A failed send leaves the invoice in draft with the error shown
- [ ] The same invoice resends without a new number
## Phase 6 · Payment links
A Stripe link per invoice, paid marked by hand.
### Steps
1. Create a Stripe Payment Link for the invoice total and store it
```sh
npm install stripe@17
```
2. Include it in the PDF and the email; mark paid by hand records paid_at and locks
### Done when
- [ ] The link opens a correct amount in the right currency
- [ ] Mark paid records paid_at and locks the invoice
## Phase 7 · Dashboard and books
What is unpaid, and an export your accountant can use.
### Steps
1. Build the unpaid dashboard with days outstanding and totals by month and client
2. Stream a CSV export of everything
The export is what makes this handover-able to an accountant.
### Done when
- [ ] Days outstanding is correct across a month boundary
- [ ] The CSV reconciles with the invoice table to the cent
## Phase 8 · Backup and deploy
Database and PDFs backed up together, one restore performed.
### Steps
1. Nightly copy of the database plus PDF_DIR, thirty kept; restore once
```sh
tar czf backups/invoices-$(date +%F).tgz data/
```
2. Write the README
The numbering rule, where PDFs live, and the tax disclaimer in plain language.
Files: `README.md`
### Done when
- [ ] A restore brings back both data and the PDFs of sent invoices
- [ ] The README carries the tax disclaimer
## Not in this build
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
## After v1, if you want it
- Recurring invoices generated on a schedule
- A read-only client link to view and pay an invoice
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1.
PORT=4840
# Required. SQLite file.
DATABASE_PATH=./data/invoices.db
# Required. Generated PDFs, kept forever.
PDF_DIR=./data/pdf
# Required · secret. From your SMTP provider.
SMTP_URL=smtps://user:pass@smtp.fastmail.com:465
# Required. From address on invoice emails.
MAIL_FROM='You' <billing@yourdomain.com>
# Optional · secret. Stripe dashboard, test mode first. Empty disables payment links.
STRIPE_SECRET_KEY=sk_test_...
# Required. Prefix for numbers: INV-2026-001.
INVOICE_PREFIX=INV
You are building a lean indie version of Invoice Ninja.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Invoice Ninja · indie build
A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | forms, PDFs, mail: the framework earns its place |
| Money | Integer cents | an invoice that rounds differently from your books is a conversation you do not want |
| PDF | Puppeteer rendering an HTML template | your own template, exact layout; a full browser is the cost |
| Hosting | localhost, or a VPS behind Caddy | invoices are private; localhost is enough for one person |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **Your business details and a logo** · free
- Why: Every PDF carries them: legal name, address, tax id if any, bank or payment details, payment terms, and a logo file.
- Get it: Write them into config/business.json; a PNG or SVG logo at least 400px wide.
- [ ] **Puppeteer's Chromium download (about 170 MB)** · free, 170 MB of disk
- Why: PDF rendering needs a browser. npm install puppeteer downloads it; on a small server you may need extra system libraries.
- Get it: npm install puppeteer downloads Chromium. On Ubuntu servers install the libraries Puppeteer's troubleshooting page lists.
- Verify: node -e "require('puppeteer').launch().then(b=>b.close())" exits without error
- [ ] **SMTP credentials to send invoices** · free tiers exist
- Why: Phase 5 emails the PDF to clients.
- Get it: Fastmail, Postmark or your mail host: host, port, user, password as an app password.
- [ ] **A Stripe account for payment links (optional)** (optional) · free; fees per payment
- Why: Phase 6 puts a pay-online link on each invoice.
- Get it: dashboard.stripe.com > Developers > API keys. Use test mode until the flow works.
- [ ] **Your tax rates and numbering rule** · free
- Why: This app does arithmetic on rates you type in; it does not know your jurisdiction. Decide the rates and the invoice number format now.
- Get it: Write down the VAT or sales tax rates you charge and the prefix format, e.g. INV-2026-001.
## Quick start
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
- managed hosting
- upgrades
- backups
- support
- payment setup convenience
- reduced ops risk
If one of those is essential to you, that is the reason to keep paying for Invoice Ninja, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Invoice Ninja
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a small invoicing app to replace hosted Invoice Ninja. Build it in
phases, in the order below. Do not write the whole app in one pass. Finish a
phase, run its "Done when" check, fix what fails, and only then start the next
phase.
### Before you start
Invoice Ninja is itself open source and self-hostable. If you want the whole
platform · quotes, expenses, projects, a client portal · deploy theirs and stop
reading. This build is the lighter option for someone who needs invoices, not a
platform. Say that in the README rather than pretending this is a replacement for
all of it.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost:4840.
- Money as integer minor units (cents). Never a float · an invoice that rounds to
a cent differently from your accounting system is a conversation with a client
you do not want to have.
- Per-invoice currency stored on the invoice, never inferred at render time.
### Data model (create this before Phase 1)
- `clients`: id, name, email, address, currency, default_rate_cents, notes
- `invoices`: id, number (unique), client_id, issue_date, due_date, currency,
status ('draft' | 'sent' | 'paid' | 'void'), notes, terms, sent_at, paid_at,
stripe_link, snapshot (JSON)
- `line_items`: id, invoice_id, position, description, quantity, unit_price_cents,
tax_rate
`snapshot` is not optional. When an invoice is sent it must freeze the client's
name, address and every line as they were at that moment. Editing a client's
address must never change what a sent invoice says · that is a document you gave
someone, and silently rewriting history is the bug that ends in a dispute.
### Phase 1 · Clients
Build: client CRUD with currency and default rate.
Done when: a client saves and reloads with the currency intact, and deleting a
client with invoices is refused rather than cascading.
Do not build yet: invoices.
### Phase 2 · Invoices and line items
Build: invoice creation with line items, quantity, unit price and per-line tax
rate. Compute the subtotal, tax and total in integer cents, rounding once at the
end. Auto-number as `INV-2026-001`, allocated inside a transaction so two
invoices created in the same second cannot collide, and never reused after a
delete · gaps are fine, duplicates are not.
Done when: a three-line invoice with mixed tax rates totals correctly to the
cent, two invoices created concurrently get distinct numbers, and deleting the
latest invoice does not cause the next one to reuse its number.
### Phase 3 · Status and the snapshot
Build: the lifecycle · draft is editable, sending freezes the snapshot and makes
it read-only, paid and void are terminal. Editing a sent invoice must require an
explicit "revise" action that voids and reissues rather than mutating.
Done when: changing a client's address after sending leaves the sent invoice's
PDF and HTML unchanged, and a sent invoice cannot be silently edited.
### Phase 4 · PDF
Build: a clean PDF per invoice rendered from an HTML template with Puppeteer ·
logo, terms, bank details from a config file. Render from the snapshot, not from
live joins. Puppeteer pulls a full browser; if the deploy target cannot carry
that, say so and use a lighter HTML-to-PDF path, but do not hand-assemble PDF
syntax.
Done when: the PDF is one page for a short invoice, totals match the screen
exactly, it opens correctly in a phone mail client, and regenerating a sent
invoice's PDF a month later produces a byte-identical document.
### Phase 5 · Email
Build: send the PDF over SMTP from `.env`, record `sent_at`, and keep a log of
send attempts with their errors. Never mark an invoice sent when the send failed.
Done when: a real send arrives with the PDF attached and readable, a failed send
leaves the invoice in draft with the error visible, and the same invoice can be
resent without renumbering.
### Phase 6 · Payment links
Build: an optional Stripe payment link per invoice (key in `.env`), included in
the PDF and the email. Payments are marked paid by hand when the money lands ·
building webhook reconciliation is a bigger job than it looks and getting it half
right means an invoice marked paid that is not.
Done when: the link opens a correct amount in the right currency, and the manual
mark-paid records `paid_at` and locks the invoice.
### Phase 7 · Dashboard and books
Build: unpaid invoices with days outstanding, totals by month and by client, and
a CSV export of everything · the export is what makes this handover-able to an
accountant, and it is the difference between a tool and a trap.
Done when: days outstanding is correct across a month boundary, and the CSV
reconciles with the invoice table to the cent.
### Phase 8 · Backup and deploy
Build: a nightly copy of the database plus the generated PDFs to `backups/`,
keeping 30, a documented restore performed once, and the README.
Done when: a restore brings back both the data and the PDFs of sent invoices.
### Out of scope (and why)
- Quotes, expenses, multi-user, and a client portal · that is the platform, and
the platform is free to self-host if you need it.
- Automatic payment reconciliation.
- Tax compliance. This app does arithmetic on rates you type in. It does not know
your jurisdiction's rules, and it is not e-invoicing compliant anywhere. Say
this in the README and check what your country requires before invoicing.
### README must contain
- The numbering rule, and why gaps are acceptable but duplicates are not.
- Where PDFs live and the note that they are outside the database for backups.
- The tax disclaimer, in plain language.
===== AGENTS.md =====
# Agent instructions · Invoice Ninja indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, Puppeteer rendering an HTML template, localhost, or a VPS behind Caddy. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Invoice Ninja
A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Clients
Clients with currency and rate, and a delete that refuses to orphan invoices.
### Steps
1. Create the project and the tables
clients (id, name, email, address, currency, default_rate_cents, notes), invoices (id, number unique, client_id, issue_date, due_date, currency, status, notes, terms, sent_at, paid_at, stripe_link, snapshot JSON), line_items (id, invoice_id, position, description, quantity, unit_price_cents, tax_rate).
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
2. Build client CRUD
### Done when
- [ ] A client saves and reloads with the currency intact
- [ ] Deleting a client with invoices is refused
## Phase 2 · Invoices and line items
Totals to the cent and numbers that never collide or reuse.
### Steps
1. Build invoice creation with line items
Subtotal, tax and total in integer cents, rounding once at the end.
2. Allocate numbers inside a transaction
INV-2026-001 style, never reused after a delete; gaps are fine, duplicates are not.
### Done when
- [ ] A three-line invoice with mixed tax rates totals correctly to the cent
- [ ] Two invoices created concurrently get distinct numbers
- [ ] Deleting the latest invoice does not cause the next to reuse its number
## Phase 3 · Status and the snapshot
Sending freezes the document; editing after requires a revision.
### Steps
1. Implement draft, sent, paid and void with the snapshot frozen on send
Copy the client's name, address and every line into snapshot; render sent invoices from it only.
2. Add a revise action that voids and reissues under a new number
The only way to change a sent invoice.
### Done when
- [ ] Changing a client's address after sending leaves the sent invoice's PDF and HTML unchanged
- [ ] A sent invoice cannot be silently edited
## Phase 4 · PDF
A clean one-page PDF from your template, byte-identical when regenerated.
### Steps
1. Install Puppeteer and write the HTML template
Logo, business details, line table, totals, terms and payment details from config/business.json.
Files: `templates/invoice.html`
```sh
npm install puppeteer@23
```
2. Render from the snapshot to PDF_DIR
Reuse one browser instance; close pages in finally.
### Done when
- [ ] A short invoice is one page
- [ ] Totals match the screen exactly
- [ ] It opens correctly in a phone mail client
- [ ] Regenerating a sent invoice's PDF later is byte-identical
## Phase 5 · Email
Sent means sent; failure leaves it a draft with the error visible.
### Steps
1. Send the PDF over SMTP_URL and record sent_at only on success
```sh
npm install nodemailer@6
```
2. Log every send attempt and allow resending without renumbering
### Done when
- [ ] A real send arrives with the PDF attached and readable
- [ ] A failed send leaves the invoice in draft with the error shown
- [ ] The same invoice resends without a new number
## Phase 6 · Payment links
A Stripe link per invoice, paid marked by hand.
### Steps
1. Create a Stripe Payment Link for the invoice total and store it
```sh
npm install stripe@17
```
2. Include it in the PDF and the email; mark paid by hand records paid_at and locks
### Done when
- [ ] The link opens a correct amount in the right currency
- [ ] Mark paid records paid_at and locks the invoice
## Phase 7 · Dashboard and books
What is unpaid, and an export your accountant can use.
### Steps
1. Build the unpaid dashboard with days outstanding and totals by month and client
2. Stream a CSV export of everything
The export is what makes this handover-able to an accountant.
### Done when
- [ ] Days outstanding is correct across a month boundary
- [ ] The CSV reconciles with the invoice table to the cent
## Phase 8 · Backup and deploy
Database and PDFs backed up together, one restore performed.
### Steps
1. Nightly copy of the database plus PDF_DIR, thirty kept; restore once
```sh
tar czf backups/invoices-$(date +%F).tgz data/
```
2. Write the README
The numbering rule, where PDFs live, and the tax disclaimer in plain language.
Files: `README.md`
### Done when
- [ ] A restore brings back both data and the PDFs of sent invoices
- [ ] The README carries the tax disclaimer
## Not in this build
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
## After v1, if you want it
- Recurring invoices generated on a schedule
- A read-only client link to view and pay an invoice
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1.
PORT=4840
# Required. SQLite file.
DATABASE_PATH=./data/invoices.db
# Required. Generated PDFs, kept forever.
PDF_DIR=./data/pdf
# Required · secret. From your SMTP provider.
SMTP_URL=smtps://user:pass@smtp.fastmail.com:465
# Required. From address on invoice emails.
MAIL_FROM='You' <billing@yourdomain.com>
# Optional · secret. Stripe dashboard, test mode first. Empty disables payment links.
STRIPE_SECRET_KEY=sk_test_...
# Required. Prefix for numbers: INV-2026-001.
INVOICE_PREFIX=INV
You are building a production product version of Invoice Ninja.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Invoice Ninja · product brief
## Problem
Invoice Ninja is open source, so the hosted subscription can be replaced by self-hosting if you can handle ops, updates, payment config, and backups.
## Product outcome
Invoicing you could run for a small studio: frozen documents, exact numbers, delivery you can prove, books that export.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- VPS/Docker
- domain/HTTPS
- database
- email service
- Stripe/PayPal credentials
- backups
## Explicit non-goals for v1
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
- managed hosting
- upgrades
- backups
- support
- payment setup convenience
- reduced ops risk
## Success criteria
- Totals verified to the cent on a fixture with mixed tax rates
- Snapshot immutability verified
- One restore drill performed
- Tax disclaimer in the README
===== BRIEF.md =====
# Build brief · Invoice Ninja
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a small invoicing app to replace hosted Invoice Ninja. Build it in
phases, in the order below. Do not write the whole app in one pass. Finish a
phase, run its "Done when" check, fix what fails, and only then start the next
phase.
### Before you start
Invoice Ninja is itself open source and self-hostable. If you want the whole
platform · quotes, expenses, projects, a client portal · deploy theirs and stop
reading. This build is the lighter option for someone who needs invoices, not a
platform. Say that in the README rather than pretending this is a replacement for
all of it.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost:4840.
- Money as integer minor units (cents). Never a float · an invoice that rounds to
a cent differently from your accounting system is a conversation with a client
you do not want to have.
- Per-invoice currency stored on the invoice, never inferred at render time.
### Data model (create this before Phase 1)
- `clients`: id, name, email, address, currency, default_rate_cents, notes
- `invoices`: id, number (unique), client_id, issue_date, due_date, currency,
status ('draft' | 'sent' | 'paid' | 'void'), notes, terms, sent_at, paid_at,
stripe_link, snapshot (JSON)
- `line_items`: id, invoice_id, position, description, quantity, unit_price_cents,
tax_rate
`snapshot` is not optional. When an invoice is sent it must freeze the client's
name, address and every line as they were at that moment. Editing a client's
address must never change what a sent invoice says · that is a document you gave
someone, and silently rewriting history is the bug that ends in a dispute.
### Phase 1 · Clients
Build: client CRUD with currency and default rate.
Done when: a client saves and reloads with the currency intact, and deleting a
client with invoices is refused rather than cascading.
Do not build yet: invoices.
### Phase 2 · Invoices and line items
Build: invoice creation with line items, quantity, unit price and per-line tax
rate. Compute the subtotal, tax and total in integer cents, rounding once at the
end. Auto-number as `INV-2026-001`, allocated inside a transaction so two
invoices created in the same second cannot collide, and never reused after a
delete · gaps are fine, duplicates are not.
Done when: a three-line invoice with mixed tax rates totals correctly to the
cent, two invoices created concurrently get distinct numbers, and deleting the
latest invoice does not cause the next one to reuse its number.
### Phase 3 · Status and the snapshot
Build: the lifecycle · draft is editable, sending freezes the snapshot and makes
it read-only, paid and void are terminal. Editing a sent invoice must require an
explicit "revise" action that voids and reissues rather than mutating.
Done when: changing a client's address after sending leaves the sent invoice's
PDF and HTML unchanged, and a sent invoice cannot be silently edited.
### Phase 4 · PDF
Build: a clean PDF per invoice rendered from an HTML template with Puppeteer ·
logo, terms, bank details from a config file. Render from the snapshot, not from
live joins. Puppeteer pulls a full browser; if the deploy target cannot carry
that, say so and use a lighter HTML-to-PDF path, but do not hand-assemble PDF
syntax.
Done when: the PDF is one page for a short invoice, totals match the screen
exactly, it opens correctly in a phone mail client, and regenerating a sent
invoice's PDF a month later produces a byte-identical document.
### Phase 5 · Email
Build: send the PDF over SMTP from `.env`, record `sent_at`, and keep a log of
send attempts with their errors. Never mark an invoice sent when the send failed.
Done when: a real send arrives with the PDF attached and readable, a failed send
leaves the invoice in draft with the error visible, and the same invoice can be
resent without renumbering.
### Phase 6 · Payment links
Build: an optional Stripe payment link per invoice (key in `.env`), included in
the PDF and the email. Payments are marked paid by hand when the money lands ·
building webhook reconciliation is a bigger job than it looks and getting it half
right means an invoice marked paid that is not.
Done when: the link opens a correct amount in the right currency, and the manual
mark-paid records `paid_at` and locks the invoice.
### Phase 7 · Dashboard and books
Build: unpaid invoices with days outstanding, totals by month and by client, and
a CSV export of everything · the export is what makes this handover-able to an
accountant, and it is the difference between a tool and a trap.
Done when: days outstanding is correct across a month boundary, and the CSV
reconciles with the invoice table to the cent.
### Phase 8 · Backup and deploy
Build: a nightly copy of the database plus the generated PDFs to `backups/`,
keeping 30, a documented restore performed once, and the README.
Done when: a restore brings back both the data and the PDFs of sent invoices.
### Out of scope (and why)
- Quotes, expenses, multi-user, and a client portal · that is the platform, and
the platform is free to self-host if you need it.
- Automatic payment reconciliation.
- Tax compliance. This app does arithmetic on rates you type in. It does not know
your jurisdiction's rules, and it is not e-invoicing compliant anywhere. Say
this in the README and check what your country requires before invoicing.
### README must contain
- The numbering rule, and why gaps are acceptable but duplicates are not.
- Where PDFs live and the note that they are outside the database for backups.
- The tax disclaimer, in plain language.
===== ARCHITECTURE.md =====
# Architecture · Invoice Ninja
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | forms, PDFs, mail: the framework earns its place |
| Money | Integer cents | an invoice that rounds differently from your books is a conversation you do not want |
| PDF | Puppeteer rendering an HTML template | your own template, exact layout; a full browser is the cost |
| Hosting | localhost, or a VPS behind Caddy | invoices are private; localhost is enough for one person |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Ledger | clients, invoices, line items, numbering | The core |
| Snapshot | freezing at send and rendering from it | The rule that makes documents trustworthy; never bypass |
| Render | the HTML template and Puppeteer | A lighter HTML-to-PDF path if the server cannot carry Chromium |
| Deliver | SMTP sends and the attempt log | A provider API behind the same send() |
| Payments | Stripe links and manual mark-paid | Webhook reconciliation later, carefully |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `PORT` · required · Bound to 127.0.0.1.
- `DATABASE_PATH` · required · SQLite file.
- `PDF_DIR` · required · Generated PDFs, kept forever.
- `SMTP_URL` · required, secret · From your SMTP provider.
- `MAIL_FROM` · required · From address on invoice emails.
- `STRIPE_SECRET_KEY` · optional, secret · Stripe dashboard, test mode first. Empty disables payment links.
- `INVOICE_PREFIX` · required · Prefix for numbers: INV-2026-001.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Invoice Ninja product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, Puppeteer rendering an HTML template, localhost, or a VPS behind Caddy.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
===== MILESTONES.md =====
# Delivery milestones · Invoice Ninja
Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Clients
Clients with currency and rate, and a delete that refuses to orphan invoices.
### Steps
1. Create the project and the tables
clients (id, name, email, address, currency, default_rate_cents, notes), invoices (id, number unique, client_id, issue_date, due_date, currency, status, notes, terms, sent_at, paid_at, stripe_link, snapshot JSON), line_items (id, invoice_id, position, description, quantity, unit_price_cents, tax_rate).
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
2. Build client CRUD
### Done when
- [ ] A client saves and reloads with the currency intact
- [ ] Deleting a client with invoices is refused
## M2 · Invoices and line items
Totals to the cent and numbers that never collide or reuse.
### Steps
1. Build invoice creation with line items
Subtotal, tax and total in integer cents, rounding once at the end.
2. Allocate numbers inside a transaction
INV-2026-001 style, never reused after a delete; gaps are fine, duplicates are not.
### Done when
- [ ] A three-line invoice with mixed tax rates totals correctly to the cent
- [ ] Two invoices created concurrently get distinct numbers
- [ ] Deleting the latest invoice does not cause the next to reuse its number
## M3 · Status and the snapshot
Sending freezes the document; editing after requires a revision.
### Steps
1. Implement draft, sent, paid and void with the snapshot frozen on send
Copy the client's name, address and every line into snapshot; render sent invoices from it only.
2. Add a revise action that voids and reissues under a new number
The only way to change a sent invoice.
### Done when
- [ ] Changing a client's address after sending leaves the sent invoice's PDF and HTML unchanged
- [ ] A sent invoice cannot be silently edited
## M4 · PDF
A clean one-page PDF from your template, byte-identical when regenerated.
### Steps
1. Install Puppeteer and write the HTML template
Logo, business details, line table, totals, terms and payment details from config/business.json.
Files: `templates/invoice.html`
```sh
npm install puppeteer@23
```
2. Render from the snapshot to PDF_DIR
Reuse one browser instance; close pages in finally.
### Done when
- [ ] A short invoice is one page
- [ ] Totals match the screen exactly
- [ ] It opens correctly in a phone mail client
- [ ] Regenerating a sent invoice's PDF later is byte-identical
## M5 · Email
Sent means sent; failure leaves it a draft with the error visible.
### Steps
1. Send the PDF over SMTP_URL and record sent_at only on success
```sh
npm install nodemailer@6
```
2. Log every send attempt and allow resending without renumbering
### Done when
- [ ] A real send arrives with the PDF attached and readable
- [ ] A failed send leaves the invoice in draft with the error shown
- [ ] The same invoice resends without a new number
## M6 · Payment links
A Stripe link per invoice, paid marked by hand.
### Steps
1. Create a Stripe Payment Link for the invoice total and store it
```sh
npm install stripe@17
```
2. Include it in the PDF and the email; mark paid by hand records paid_at and locks
### Done when
- [ ] The link opens a correct amount in the right currency
- [ ] Mark paid records paid_at and locks the invoice
## M7 · Dashboard and books
What is unpaid, and an export your accountant can use.
### Steps
1. Build the unpaid dashboard with days outstanding and totals by month and client
2. Stream a CSV export of everything
The export is what makes this handover-able to an accountant.
### Done when
- [ ] Days outstanding is correct across a month boundary
- [ ] The CSV reconciles with the invoice table to the cent
## M8 · Backup and deploy
Database and PDFs backed up together, one restore performed.
### Steps
1. Nightly copy of the database plus PDF_DIR, thirty kept; restore once
```sh
tar czf backups/invoices-$(date +%F).tgz data/
```
2. Write the README
The numbering rule, where PDFs live, and the tax disclaimer in plain language.
Files: `README.md`
### Done when
- [ ] A restore brings back both data and the PDFs of sent invoices
- [ ] The README carries the tax disclaimer
## M9 · Operate it like a product (production only)
Only for the product-builder path: know when the invoicing app is down, never lose the database, and keep the server patched.
### Steps
1. Add a /healthz endpoint and an external uptime check against it
Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices.
2. Write structured request logs and rotate them
One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight.
3. Back the SQLite file up off the machine nightly and test a restore
SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it.
```sh
sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'"
rclone copy /tmp/app-$(date +%F).db remote:backups/
```
4. Lock the box down
Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure.
### Done when
- [ ] Stopping the service triggers an uptime alert within a few minutes
- [ ] A restore from last night's backup contains yesterday's data
- [ ] A port scan from another machine shows only 22, 80 and 443
===== OPERATIONS.md =====
# Operations · Invoice Ninja
## Backup
tar of data/ nightly off the box.
## Restore
Extract, start, open a sent invoice's PDF.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Uptime on /healthz when hosted; failed sends are visible in the UI.
## Incident checklist
Leaked SMTP or Stripe keys: rotate at the provider and update .env. Never edit a sent invoice to fix an error; revise it.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Totals verified to the cent on a fixture with mixed tax rates
- [ ] Snapshot immutability verified
- [ ] One restore drill performed
- [ ] Tax disclaimer in the README
## Launch constraint
Do not market omitted Invoice Ninja capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1.
PORT=4840
# Required. SQLite file.
DATABASE_PATH=./data/invoices.db
# Required. Generated PDFs, kept forever.
PDF_DIR=./data/pdf
# Required · secret. From your SMTP provider.
SMTP_URL=smtps://user:pass@smtp.fastmail.com:465
# Required. From address on invoice emails.
MAIL_FROM='You' <billing@yourdomain.com>
# Optional · secret. Stripe dashboard, test mode first. Empty disables payment links.
STRIPE_SECRET_KEY=sk_test_...
# Required. Prefix for numbers: INV-2026-001.
INVOICE_PREFIX=INV
# Invoice Ninja · indie build
A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | forms, PDFs, mail: the framework earns its place |
| Money | Integer cents | an invoice that rounds differently from your books is a conversation you do not want |
| PDF | Puppeteer rendering an HTML template | your own template, exact layout; a full browser is the cost |
| Hosting | localhost, or a VPS behind Caddy | invoices are private; localhost is enough for one person |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **Your business details and a logo** · free
- Why: Every PDF carries them: legal name, address, tax id if any, bank or payment details, payment terms, and a logo file.
- Get it: Write them into config/business.json; a PNG or SVG logo at least 400px wide.
- [ ] **Puppeteer's Chromium download (about 170 MB)** · free, 170 MB of disk
- Why: PDF rendering needs a browser. npm install puppeteer downloads it; on a small server you may need extra system libraries.
- Get it: npm install puppeteer downloads Chromium. On Ubuntu servers install the libraries Puppeteer's troubleshooting page lists.
- Verify: node -e "require('puppeteer').launch().then(b=>b.close())" exits without error
- [ ] **SMTP credentials to send invoices** · free tiers exist
- Why: Phase 5 emails the PDF to clients.
- Get it: Fastmail, Postmark or your mail host: host, port, user, password as an app password.
- [ ] **A Stripe account for payment links (optional)** (optional) · free; fees per payment
- Why: Phase 6 puts a pay-online link on each invoice.
- Get it: dashboard.stripe.com > Developers > API keys. Use test mode until the flow works.
- [ ] **Your tax rates and numbering rule** · free
- Why: This app does arithmetic on rates you type in; it does not know your jurisdiction. Decide the rates and the invoice number format now.
- Get it: Write down the VAT or sales tax rates you charge and the prefix format, e.g. INV-2026-001.
## Quick start
```sh
mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir -p data/pdf config && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host.
- Automatic payment reconciliation.
- Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere.
- managed hosting
- upgrades
- backups
- support
- payment setup convenience
- reduced ops risk
If one of those is essential to you, that is the reason to keep paying for Invoice Ninja, and the README should say so rather than pretend.# Build brief · Invoice Ninja
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a small invoicing app to replace hosted Invoice Ninja. Build it in
phases, in the order below. Do not write the whole app in one pass. Finish a
phase, run its "Done when" check, fix what fails, and only then start the next
phase.
### Before you start
Invoice Ninja is itself open source and self-hostable. If you want the whole
platform · quotes, expenses, projects, a client portal · deploy theirs and stop
reading. This build is the lighter option for someone who needs invoices, not a
platform. Say that in the README rather than pretending this is a replacement for
all of it.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost:4840.
- Money as integer minor units (cents). Never a float · an invoice that rounds to
a cent differently from your accounting system is a conversation with a client
you do not want to have.
- Per-invoice currency stored on the invoice, never inferred at render time.
### Data model (create this before Phase 1)
- `clients`: id, name, email, address, currency, default_rate_cents, notes
- `invoices`: id, number (unique), client_id, issue_date, due_date, currency,
status ('draft' | 'sent' | 'paid' | 'void'), notes, terms, sent_at, paid_at,
stripe_link, snapshot (JSON)
- `line_items`: id, invoice_id, position, description, quantity, unit_price_cents,
tax_rate
`snapshot` is not optional. When an invoice is sent it must freeze the client's
name, address and every line as they were at that moment. Editing a client's
address must never change what a sent invoice says · that is a document you gave
someone, and silently rewriting history is the bug that ends in a dispute.
### Phase 1 · Clients
Build: client CRUD with currency and default rate.
Done when: a client saves and reloads with the currency intact, and deleting a
client with invoices is refused rather than cascading.
Do not build yet: invoices.
### Phase 2 · Invoices and line items
Build: invoice creation with line items, quantity, unit price and per-line tax
rate. Compute the subtotal, tax and total in integer cents, rounding once at the
end. Auto-number as `INV-2026-001`, allocated inside a transaction so two
invoices created in the same second cannot collide, and never reused after a
delete · gaps are fine, duplicates are not.
Done when: a three-line invoice with mixed tax rates totals correctly to the
cent, two invoices created concurrently get distinct numbers, and deleting the
latest invoice does not cause the next one to reuse its number.
### Phase 3 · Status and the snapshot
Build: the lifecycle · draft is editable, sending freezes the snapshot and makes
it read-only, paid and void are terminal. Editing a sent invoice must require an
explicit "revise" action that voids and reissues rather than mutating.
Done when: changing a client's address after sending leaves the sent invoice's
PDF and HTML unchanged, and a sent invoice cannot be silently edited.
### Phase 4 · PDF
Build: a clean PDF per invoice rendered from an HTML template with Puppeteer ·
logo, terms, bank details from a config file. Render from the snapshot, not from
live joins. Puppeteer pulls a full browser; if the deploy target cannot carry
that, say so and use a lighter HTML-to-PDF path, but do not hand-assemble PDF
syntax.
Done when: the PDF is one page for a short invoice, totals match the screen
exactly, it opens correctly in a phone mail client, and regenerating a sent
invoice's PDF a month later produces a byte-identical document.
### Phase 5 · Email
Build: send the PDF over SMTP from `.env`, record `sent_at`, and keep a log of
send attempts with their errors. Never mark an invoice sent when the send failed.
Done when: a real send arrives with the PDF attached and readable, a failed send
leaves the invoice in draft with the error visible, and the same invoice can be
resent without renumbering.
### Phase 6 · Payment links
Build: an optional Stripe payment link per invoice (key in `.env`), included in
the PDF and the email. Payments are marked paid by hand when the money lands ·
building webhook reconciliation is a bigger job than it looks and getting it half
right means an invoice marked paid that is not.
Done when: the link opens a correct amount in the right currency, and the manual
mark-paid records `paid_at` and locks the invoice.
### Phase 7 · Dashboard and books
Build: unpaid invoices with days outstanding, totals by month and by client, and
a CSV export of everything · the export is what makes this handover-able to an
accountant, and it is the difference between a tool and a trap.
Done when: days outstanding is correct across a month boundary, and the CSV
reconciles with the invoice table to the cent.
### Phase 8 · Backup and deploy
Build: a nightly copy of the database plus the generated PDFs to `backups/`,
keeping 30, a documented restore performed once, and the README.
Done when: a restore brings back both the data and the PDFs of sent invoices.
### Out of scope (and why)
- Quotes, expenses, multi-user, and a client portal · that is the platform, and
the platform is free to self-host if you need it.
- Automatic payment reconciliation.
- Tax compliance. This app does arithmetic on rates you type in. It does not know
your jurisdiction's rules, and it is not e-invoicing compliant anywhere. Say
this in the README and check what your country requires before invoicing.
### README must contain
- The numbering rule, and why gaps are acceptable but duplicates are not.
- Where PDFs live and the note that they are outside the database for backups.
- The tax disclaimer, in plain language.# Agent instructions · Invoice Ninja indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, Puppeteer rendering an HTML template, localhost, or a VPS behind Caddy. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Invoice Ninja A small invoicing app for one freelancer: clients, invoices with line items and tax, numbers that never repeat, a snapshot frozen at send time so a sent invoice can never silently change, a clean PDF, email delivery, optional Stripe payment links, and a CSV your accountant can use. If you want the whole platform, Invoice Ninja itself is open source; this is the lighter option. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Clients Clients with currency and rate, and a delete that refuses to orphan invoices. ### Steps 1. Create the project and the tables clients (id, name, email, address, currency, default_rate_cents, notes), invoices (id, number unique, client_id, issue_date, due_date, currency, status, notes, terms, sent_at, paid_at, stripe_link, snapshot JSON), line_items (id, invoice_id, position, description, quantity, unit_price_cents, tax_rate). ```sh mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir -p data/pdf config && cp .env.example .env ``` 2. Build client CRUD ### Done when - [ ] A client saves and reloads with the currency intact - [ ] Deleting a client with invoices is refused ## Phase 2 · Invoices and line items Totals to the cent and numbers that never collide or reuse. ### Steps 1. Build invoice creation with line items Subtotal, tax and total in integer cents, rounding once at the end. 2. Allocate numbers inside a transaction INV-2026-001 style, never reused after a delete; gaps are fine, duplicates are not. ### Done when - [ ] A three-line invoice with mixed tax rates totals correctly to the cent - [ ] Two invoices created concurrently get distinct numbers - [ ] Deleting the latest invoice does not cause the next to reuse its number ## Phase 3 · Status and the snapshot Sending freezes the document; editing after requires a revision. ### Steps 1. Implement draft, sent, paid and void with the snapshot frozen on send Copy the client's name, address and every line into snapshot; render sent invoices from it only. 2. Add a revise action that voids and reissues under a new number The only way to change a sent invoice. ### Done when - [ ] Changing a client's address after sending leaves the sent invoice's PDF and HTML unchanged - [ ] A sent invoice cannot be silently edited ## Phase 4 · PDF A clean one-page PDF from your template, byte-identical when regenerated. ### Steps 1. Install Puppeteer and write the HTML template Logo, business details, line table, totals, terms and payment details from config/business.json. Files: `templates/invoice.html` ```sh npm install puppeteer@23 ``` 2. Render from the snapshot to PDF_DIR Reuse one browser instance; close pages in finally. ### Done when - [ ] A short invoice is one page - [ ] Totals match the screen exactly - [ ] It opens correctly in a phone mail client - [ ] Regenerating a sent invoice's PDF later is byte-identical ## Phase 5 · Email Sent means sent; failure leaves it a draft with the error visible. ### Steps 1. Send the PDF over SMTP_URL and record sent_at only on success ```sh npm install nodemailer@6 ``` 2. Log every send attempt and allow resending without renumbering ### Done when - [ ] A real send arrives with the PDF attached and readable - [ ] A failed send leaves the invoice in draft with the error shown - [ ] The same invoice resends without a new number ## Phase 6 · Payment links A Stripe link per invoice, paid marked by hand. ### Steps 1. Create a Stripe Payment Link for the invoice total and store it ```sh npm install stripe@17 ``` 2. Include it in the PDF and the email; mark paid by hand records paid_at and locks ### Done when - [ ] The link opens a correct amount in the right currency - [ ] Mark paid records paid_at and locks the invoice ## Phase 7 · Dashboard and books What is unpaid, and an export your accountant can use. ### Steps 1. Build the unpaid dashboard with days outstanding and totals by month and client 2. Stream a CSV export of everything The export is what makes this handover-able to an accountant. ### Done when - [ ] Days outstanding is correct across a month boundary - [ ] The CSV reconciles with the invoice table to the cent ## Phase 8 · Backup and deploy Database and PDFs backed up together, one restore performed. ### Steps 1. Nightly copy of the database plus PDF_DIR, thirty kept; restore once ```sh tar czf backups/invoices-$(date +%F).tgz data/ ``` 2. Write the README The numbering rule, where PDFs live, and the tax disclaimer in plain language. Files: `README.md` ### Done when - [ ] A restore brings back both data and the PDFs of sent invoices - [ ] The README carries the tax disclaimer ## Not in this build - Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host. - Automatic payment reconciliation. - Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere. ## After v1, if you want it - Recurring invoices generated on a schedule - A read-only client link to view and pay an invoice
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1. PORT=4840 # Required. SQLite file. DATABASE_PATH=./data/invoices.db # Required. Generated PDFs, kept forever. PDF_DIR=./data/pdf # Required · secret. From your SMTP provider. SMTP_URL=smtps://user:pass@smtp.fastmail.com:465 # Required. From address on invoice emails. MAIL_FROM='You' <billing@yourdomain.com> # Optional · secret. Stripe dashboard, test mode first. Empty disables payment links. STRIPE_SECRET_KEY=sk_test_... # Required. Prefix for numbers: INV-2026-001. INVOICE_PREFIX=INV
# Invoice Ninja · product brief ## Problem Invoice Ninja is open source, so the hosted subscription can be replaced by self-hosting if you can handle ops, updates, payment config, and backups. ## Product outcome Invoicing you could run for a small studio: frozen documents, exact numbers, delivery you can prove, books that export. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - VPS/Docker - domain/HTTPS - database - email service - Stripe/PayPal credentials - backups ## Explicit non-goals for v1 - Quotes, expenses, multi-user, a client portal: that is the platform, free to self-host. - Automatic payment reconciliation. - Tax compliance. This does arithmetic on rates you type; it is not e-invoicing compliant anywhere. - managed hosting - upgrades - backups - support - payment setup convenience - reduced ops risk ## Success criteria - Totals verified to the cent on a fixture with mixed tax rates - Snapshot immutability verified - One restore drill performed - Tax disclaimer in the README
# Build brief · Invoice Ninja
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a small invoicing app to replace hosted Invoice Ninja. Build it in
phases, in the order below. Do not write the whole app in one pass. Finish a
phase, run its "Done when" check, fix what fails, and only then start the next
phase.
### Before you start
Invoice Ninja is itself open source and self-hostable. If you want the whole
platform · quotes, expenses, projects, a client portal · deploy theirs and stop
reading. This build is the lighter option for someone who needs invoices, not a
platform. Say that in the README rather than pretending this is a replacement for
all of it.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost:4840.
- Money as integer minor units (cents). Never a float · an invoice that rounds to
a cent differently from your accounting system is a conversation with a client
you do not want to have.
- Per-invoice currency stored on the invoice, never inferred at render time.
### Data model (create this before Phase 1)
- `clients`: id, name, email, address, currency, default_rate_cents, notes
- `invoices`: id, number (unique), client_id, issue_date, due_date, currency,
status ('draft' | 'sent' | 'paid' | 'void'), notes, terms, sent_at, paid_at,
stripe_link, snapshot (JSON)
- `line_items`: id, invoice_id, position, description, quantity, unit_price_cents,
tax_rate
`snapshot` is not optional. When an invoice is sent it must freeze the client's
name, address and every line as they were at that moment. Editing a client's
address must never change what a sent invoice says · that is a document you gave
someone, and silently rewriting history is the bug that ends in a dispute.
### Phase 1 · Clients
Build: client CRUD with currency and default rate.
Done when: a client saves and reloads with the currency intact, and deleting a
client with invoices is refused rather than cascading.
Do not build yet: invoices.
### Phase 2 · Invoices and line items
Build: invoice creation with line items, quantity, unit price and per-line tax
rate. Compute the subtotal, tax and total in integer cents, rounding once at the
end. Auto-number as `INV-2026-001`, allocated inside a transaction so two
invoices created in the same second cannot collide, and never reused after a
delete · gaps are fine, duplicates are not.
Done when: a three-line invoice with mixed tax rates totals correctly to the
cent, two invoices created concurrently get distinct numbers, and deleting the
latest invoice does not cause the next one to reuse its number.
### Phase 3 · Status and the snapshot
Build: the lifecycle · draft is editable, sending freezes the snapshot and makes
it read-only, paid and void are terminal. Editing a sent invoice must require an
explicit "revise" action that voids and reissues rather than mutating.
Done when: changing a client's address after sending leaves the sent invoice's
PDF and HTML unchanged, and a sent invoice cannot be silently edited.
### Phase 4 · PDF
Build: a clean PDF per invoice rendered from an HTML template with Puppeteer ·
logo, terms, bank details from a config file. Render from the snapshot, not from
live joins. Puppeteer pulls a full browser; if the deploy target cannot carry
that, say so and use a lighter HTML-to-PDF path, but do not hand-assemble PDF
syntax.
Done when: the PDF is one page for a short invoice, totals match the screen
exactly, it opens correctly in a phone mail client, and regenerating a sent
invoice's PDF a month later produces a byte-identical document.
### Phase 5 · Email
Build: send the PDF over SMTP from `.env`, record `sent_at`, and keep a log of
send attempts with their errors. Never mark an invoice sent when the send failed.
Done when: a real send arrives with the PDF attached and readable, a failed send
leaves the invoice in draft with the error visible, and the same invoice can be
resent without renumbering.
### Phase 6 · Payment links
Build: an optional Stripe payment link per invoice (key in `.env`), included in
the PDF and the email. Payments are marked paid by hand when the money lands ·
building webhook reconciliation is a bigger job than it looks and getting it half
right means an invoice marked paid that is not.
Done when: the link opens a correct amount in the right currency, and the manual
mark-paid records `paid_at` and locks the invoice.
### Phase 7 · Dashboard and books
Build: unpaid invoices with days outstanding, totals by month and by client, and
a CSV export of everything · the export is what makes this handover-able to an
accountant, and it is the difference between a tool and a trap.
Done when: days outstanding is correct across a month boundary, and the CSV
reconciles with the invoice table to the cent.
### Phase 8 · Backup and deploy
Build: a nightly copy of the database plus the generated PDFs to `backups/`,
keeping 30, a documented restore performed once, and the README.
Done when: a restore brings back both the data and the PDFs of sent invoices.
### Out of scope (and why)
- Quotes, expenses, multi-user, and a client portal · that is the platform, and
the platform is free to self-host if you need it.
- Automatic payment reconciliation.
- Tax compliance. This app does arithmetic on rates you type in. It does not know
your jurisdiction's rules, and it is not e-invoicing compliant anywhere. Say
this in the README and check what your country requires before invoicing.
### README must contain
- The numbering rule, and why gaps are acceptable but duplicates are not.
- Where PDFs live and the note that they are outside the database for backups.
- The tax disclaimer, in plain language.# Architecture · Invoice Ninja ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with Express and better-sqlite3 | forms, PDFs, mail: the framework earns its place | | Money | Integer cents | an invoice that rounds differently from your books is a conversation you do not want | | PDF | Puppeteer rendering an HTML template | your own template, exact layout; a full browser is the cost | | Hosting | localhost, or a VPS behind Caddy | invoices are private; localhost is enough for one person | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Ledger | clients, invoices, line items, numbering | The core | | Snapshot | freezing at send and rendering from it | The rule that makes documents trustworthy; never bypass | | Render | the HTML template and Puppeteer | A lighter HTML-to-PDF path if the server cannot carry Chromium | | Deliver | SMTP sends and the attempt log | A provider API behind the same send() | | Payments | Stripe links and manual mark-paid | Webhook reconciliation later, carefully | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Bound to 127.0.0.1. - `DATABASE_PATH` · required · SQLite file. - `PDF_DIR` · required · Generated PDFs, kept forever. - `SMTP_URL` · required, secret · From your SMTP provider. - `MAIL_FROM` · required · From address on invoice emails. - `STRIPE_SECRET_KEY` · optional, secret · Stripe dashboard, test mode first. Empty disables payment links. - `INVOICE_PREFIX` · required · Prefix for numbers: INV-2026-001. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Invoice Ninja product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, Integer cents, Puppeteer rendering an HTML template, localhost, or a VPS behind Caddy. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Invoice Ninja Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Clients Clients with currency and rate, and a delete that refuses to orphan invoices. ### Steps 1. Create the project and the tables clients (id, name, email, address, currency, default_rate_cents, notes), invoices (id, number unique, client_id, issue_date, due_date, currency, status, notes, terms, sent_at, paid_at, stripe_link, snapshot JSON), line_items (id, invoice_id, position, description, quantity, unit_price_cents, tax_rate). ```sh mkdir invoices && cd invoices && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir -p data/pdf config && cp .env.example .env ``` 2. Build client CRUD ### Done when - [ ] A client saves and reloads with the currency intact - [ ] Deleting a client with invoices is refused ## M2 · Invoices and line items Totals to the cent and numbers that never collide or reuse. ### Steps 1. Build invoice creation with line items Subtotal, tax and total in integer cents, rounding once at the end. 2. Allocate numbers inside a transaction INV-2026-001 style, never reused after a delete; gaps are fine, duplicates are not. ### Done when - [ ] A three-line invoice with mixed tax rates totals correctly to the cent - [ ] Two invoices created concurrently get distinct numbers - [ ] Deleting the latest invoice does not cause the next to reuse its number ## M3 · Status and the snapshot Sending freezes the document; editing after requires a revision. ### Steps 1. Implement draft, sent, paid and void with the snapshot frozen on send Copy the client's name, address and every line into snapshot; render sent invoices from it only. 2. Add a revise action that voids and reissues under a new number The only way to change a sent invoice. ### Done when - [ ] Changing a client's address after sending leaves the sent invoice's PDF and HTML unchanged - [ ] A sent invoice cannot be silently edited ## M4 · PDF A clean one-page PDF from your template, byte-identical when regenerated. ### Steps 1. Install Puppeteer and write the HTML template Logo, business details, line table, totals, terms and payment details from config/business.json. Files: `templates/invoice.html` ```sh npm install puppeteer@23 ``` 2. Render from the snapshot to PDF_DIR Reuse one browser instance; close pages in finally. ### Done when - [ ] A short invoice is one page - [ ] Totals match the screen exactly - [ ] It opens correctly in a phone mail client - [ ] Regenerating a sent invoice's PDF later is byte-identical ## M5 · Email Sent means sent; failure leaves it a draft with the error visible. ### Steps 1. Send the PDF over SMTP_URL and record sent_at only on success ```sh npm install nodemailer@6 ``` 2. Log every send attempt and allow resending without renumbering ### Done when - [ ] A real send arrives with the PDF attached and readable - [ ] A failed send leaves the invoice in draft with the error shown - [ ] The same invoice resends without a new number ## M6 · Payment links A Stripe link per invoice, paid marked by hand. ### Steps 1. Create a Stripe Payment Link for the invoice total and store it ```sh npm install stripe@17 ``` 2. Include it in the PDF and the email; mark paid by hand records paid_at and locks ### Done when - [ ] The link opens a correct amount in the right currency - [ ] Mark paid records paid_at and locks the invoice ## M7 · Dashboard and books What is unpaid, and an export your accountant can use. ### Steps 1. Build the unpaid dashboard with days outstanding and totals by month and client 2. Stream a CSV export of everything The export is what makes this handover-able to an accountant. ### Done when - [ ] Days outstanding is correct across a month boundary - [ ] The CSV reconciles with the invoice table to the cent ## M8 · Backup and deploy Database and PDFs backed up together, one restore performed. ### Steps 1. Nightly copy of the database plus PDF_DIR, thirty kept; restore once ```sh tar czf backups/invoices-$(date +%F).tgz data/ ``` 2. Write the README The numbering rule, where PDFs live, and the tax disclaimer in plain language. Files: `README.md` ### Done when - [ ] A restore brings back both data and the PDFs of sent invoices - [ ] The README carries the tax disclaimer ## M9 · Operate it like a product (production only) Only for the product-builder path: know when the invoicing app is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Invoice Ninja ## Backup tar of data/ nightly off the box. ## Restore Extract, start, open a sent invoice's PDF. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on /healthz when hosted; failed sends are visible in the UI. ## Incident checklist Leaked SMTP or Stripe keys: rotate at the provider and update .env. Never edit a sent invoice to fix an error; revise it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Totals verified to the cent on a fixture with mixed tax rates - [ ] Snapshot immutability verified - [ ] One restore drill performed - [ ] Tax disclaimer in the README ## Launch constraint Do not market omitted Invoice Ninja capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1. PORT=4840 # Required. SQLite file. DATABASE_PATH=./data/invoices.db # Required. Generated PDFs, kept forever. PDF_DIR=./data/pdf # Required · secret. From your SMTP provider. SMTP_URL=smtps://user:pass@smtp.fastmail.com:465 # Required. From address on invoice emails. MAIL_FROM='You' <billing@yourdomain.com> # Optional · secret. Stripe dashboard, test mode first. Empty disables payment links. STRIPE_SECRET_KEY=sk_test_... # Required. Prefix for numbers: INV-2026-001. INVOICE_PREFIX=INV
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because billing software is boring and should not break on invoice day.
xmanaged hosting
xupgrades
xbackups
xsupport
xpayment setup convenience
xreduced ops risk
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Invoice Ninja →· no votes, no pay-to-list · just what's real
Invoice Ninja pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | Up to 5 clients, unlimited invoices and 4 invoice templates. |
| ninja pro | $14/workspace | $11.67/workspace | Unlimited clients/invoices, 11 templates and up to 10 interlinked companies per login. |
| enterprise | $18/workspace | $15/workspace | 1–2 users at the starting price; scales to 51–100 users at $300/month or $250/month annual equivalent; 250 PEPPOL credits included. |
| premium business | — | $23.33/workspace | Up to 100 users plus migration, custom reports and developer concierge. |
free tier5 clients, unlimited invoices and 4 invoice templates.
billingmonthly + annual (2 months free); annual charged as one yearly payment; 30-day money-back promise
hidden costsPayment-gateway processing fees are separate. Enterprise prices rise with user count, from $18/month for 1–2 users to $300/month for 51–100; only 250 PEPPOL credits are included and extra-credit pricing was not public.
verified 2026-08-11 · source ↗
Is Invoice Ninja free?
The free plan covers up to 5 clients with unlimited invoicing. Paid is Pro at $14/mo (checked 2026-08-07).
Vibecode Invoice Ninja
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Invoice Ninja replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Invoice Ninja cost?
Invoice Ninja costs about $14/month (Pro, checked 2026-08-07), which is $168 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Invoice Ninja?
Honestly: managed hosting; upgrades; backups; support; payment setup convenience; reduced ops risk. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Invoice Ninja?
Yes: ERPNext (Quotes, invoices, payments, expenses and a portal inside a much larger ERP than anyone ordered.) InvoiceShelf (Invoices, estimates, expenses, payments and a client portal; the bill arrives as Docker maintenance instead.) Invoice Ninja self-hosted (The same invoicing stack on your own server; billing is free, the license is source-available, and removing its branding costs extra.) All 4 curated free alternatives are at vibecodeit.com/invoice-ninja/alternatives. The prompt is for when you want it exactly your way.