Vibecode FreshBooks
track this build5 phases, 15 steps, beginner friendly0%For freelancers, invoices, time tracking, and expense logs are buildable; payments, tax/accounting reports, bank feeds, client portal, and reliability make it more than trivial.
You are building a lean indie version of FreshBooks.
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 =====
# FreshBooks · indie build
Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile.
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 | Python 3.12 with SQLite | one person's books, where correctness matters far more than scale |
| Money | Integer minor units throughout, never floating point | an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one |
| Numbering | A gapless sequence allocated in a transaction | the sequence is part of the record, and a gap has to be explainable |
| Documents | HTML rendered to PDF, stored immutably once sent | the sent PDF is what the client has and must remain exactly that |
| Email | A transactional provider, with every send recorded | when a client says they never received it, the send log is the answer |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Python 3.12 or newer** · free
- Why: Runs the application, the PDF rendering and the reports.
- Get it: python.org or your package manager, then a virtual environment.
- Verify: python3 --version prints 3.12 or higher
- [ ] **An HTML to PDF toolchain** · free
- Why: Invoices are documents someone else keeps, and they must render identically every time.
- Get it: A headless browser print path or a dedicated renderer. Whichever you choose, confirm two renders of the same invoice are byte-identical.
- Verify: Two renders of one invoice produce identical files
- [ ] **A transactional email provider with a verified domain** · free tiers cover a sole trader's volume
- Why: An invoice in a spam folder is an unpaid invoice, and domain verification is what prevents that.
- Get it: Create an account, add the DNS records they specify, take the credentials. Verification takes hours, so start early.
- Verify: An invoice email arrives at three providers and is not in spam
- [ ] **A conversation with whoever does your accounts** · usually free, and it saves more than it costs
- Why: Numbering, credit note handling and what they need in an export are all things they have opinions about, and matching those opinions now is cheaper than reformatting a year of records later.
- Get it: Ask what format they want, whether your numbering scheme is acceptable, and what they need to see for a correction.
- Verify: The export format has been agreed with them
- [ ] **Your existing invoices for the current year** · free
- Why: Numbering must continue from where you are rather than restarting, and importing history is what makes the reports meaningful.
- Get it: Whatever you have: a spreadsheet, another tool's export, or PDFs. Enter at least the numbers and totals so the sequence continues correctly.
- Verify: The next issued invoice continues your existing sequence
## Quick start
Follow Phase 1 in `BUILD_PLAN.md`; it creates the project.
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
- payment collection
- bank imports
- accounting reports
- client portal
- mobile apps
- tax workflows
- support
If one of those is essential to you, that is the reason to keep paying for FreshBooks, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · FreshBooks
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 invoicing for one person: clients, tracked time, sequential invoices and a record you could hand an accountant to replace FreshBooks. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Two rules decide the shape of this and both come from how invoicing actually works
rather than from software preference. Invoice numbers must be sequential without gaps
and must never be reused, because in many jurisdictions the sequence itself is part of
the record. And a sent invoice is not editable: a correction is a credit note plus a new
invoice, because the original was a document someone else received.
This also calculates no tax. Rates, thresholds, reverse charges and place-of-supply rules
vary by jurisdiction and change, and getting them wrong is your liability rather than a
bug. Record the rate a person entered and show the arithmetic; do not determine it.
### Stack (fixed, do not substitute)
- Runtime: Python 3.12 with SQLite · one person's books, where correctness matters far more than scale
- Money: Integer minor units throughout, never floating point · an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one
- Numbering: A gapless sequence allocated in a transaction · the sequence is part of the record, and a gap has to be explainable
- Documents: HTML rendered to PDF, stored immutably once sent · the sent PDF is what the client has and must remain exactly that
- Email: A transactional provider, with every send recorded · when a client says they never received it, the send log is the answer
### Data model (create this before Phase 1)
- clients(id, name, email, address, currency, payment_terms_days, notes)
- projects(id, client_id, name, rate_minor, billable, active)
- time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id)
- invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash)
- invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points
- payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor)
- credit_notes(id, number, invoice_id, issued_on, amount_minor, reason)
- A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly.
### Phase 1 · Money and numbering, before anything else
Build: Store every amount as an integer in minor units. Allocate invoice numbers from a
sequence inside the same transaction that creates the invoice, so a failure cannot
consume a number. Format only for display.
Trap: Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
Trap: Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
Done when: An invoice with several lines totals exactly, matching a hand calculation to the penny. No monetary value is stored as a floating point number, verified by inspecting the schema. A deliberately failed invoice creation consumes no number, verified by checking the sequence. Numbers are strictly increasing with no gaps.
Do not build yet: No clients, time tracking or PDFs.
### Phase 2 · Clients, projects and billable time
Build: Clients with payment terms and currency. Projects with a rate. Time entries with a
description. Invoicing a period marks those entries as invoiced so they cannot be
billed again.
Trap: Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
Done when: Invoicing a period marks its time entries and a second invoice for the same period includes none of them. Deleting a draft invoice releases its entries. A time entry cannot be edited once invoiced, or editing it is recorded with the invoice it affects. Rates come from the project rather than being typed each time.
Do not build yet: No PDFs, sending or payments yet.
### Phase 3 · Issuing, which makes a document permanent
Build: Draft invoices are editable. Issuing renders a PDF, records its hash and locks the
invoice. A correction issues a credit note against it and, if needed, a new invoice.
There is no code path that edits an issued invoice.
Trap: Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
Done when: An issued invoice cannot be edited, asserted by a test attempting it. Issuing stores the PDF and its hash. A credit note reduces the outstanding balance and references its invoice. Re-rendering an issued invoice produces a PDF whose hash matches the stored one.
Do not build yet: No sending or payments yet.
### Phase 4 · Sending, and knowing it was sent
Build: Email the PDF through a transactional provider with a verified domain. Record every
send attempt with its outcome. Handle a bounce visibly. A resend is recorded as a
separate event rather than overwriting the first.
Trap: An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
Done when: An invoice email arrives with its PDF attached at three different mail providers and is not in spam. Every send is recorded with its outcome. A bounce is surfaced against the invoice. A resend appears as an additional event with its own timestamp.
Do not build yet: No payments or reports yet.
### Phase 5 · Payments, including the awkward ones
Build: Record payments against a client and allocate amounts to specific invoices. Support
partial payment, overpayment held as a credit, and one payment across several invoices.
An invoice's state derives from its allocations rather than being set by hand.
Trap: Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
Done when: One payment allocated across three invoices marks all three paid and the payment total matches the bank figure. A partial payment leaves the invoice partly paid with the correct outstanding amount. An overpayment is held as a client credit rather than lost. Invoice state is derived rather than settable.
Do not build yet: Reports and export come last.
### Phase 6 · Reports, export and the accountant's questions
Build: Reports for a period: invoiced, paid, outstanding and aged debt. Export invoices,
payments and credit notes as CSV with the PDFs alongside. Back up the database and the
documents together, and restore them once.
Trap: Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
Done when: A period report's invoiced total matches the sum of issued invoices, verified by hand for one month. Aged debt buckets outstanding invoices by age correctly. Export produces CSV plus every PDF. A restore reproduces invoices whose PDF hashes still match, and the date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Tax calculation and determination. Rates, thresholds and place-of-supply rules vary by jurisdiction and change, and this records a rate someone entered rather than deciding it.
- Online payment collection. Adding a payment processor is a phase of its own with webhooks, reconciliation and fees, and it is worth doing separately rather than badly here.
- Bank feed imports and automatic reconciliation, which is the aggregation problem and a separate paid service.
- Full accounting: ledgers, trial balances and financial statements. This produces the records an accountant works from, not the accounts themselves.
### README must contain
- That invoice numbers are gapless and allocated transactionally, and why a gap matters
- That an issued invoice cannot be edited and corrections are credit notes
- That no tax is calculated and rates are recorded as entered
- That payments allocate across invoices so a single transfer matches the bank
- That backups must include the issued PDFs, not only the database
===== AGENTS.md =====
# Agent instructions · FreshBooks indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, Integer minor units throughout, never floating point, A gapless sequence allocated in a transaction, HTML rendered to PDF, stored immutably once sent, A transactional provider, with every send recorded. 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".
## Known traps
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
===== BUILD_PLAN.md =====
# Build plan · FreshBooks
Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Money and numbering, before anything else
Amounts are exact and invoice numbers are gapless.
### Steps
1. Use integer minor units everywhere
Storage and arithmetic. Formatting happens at display and nowhere else.
Files: `src/money.py`
2. Allocate numbers transactionally
Sequence and insert in one transaction. Test by forcing a failure mid-creation.
Files: `src/numbering.py`
3. Verify totals by hand
One multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on.
Files: `tests/test_totals.py`
### Done when
- [ ] A multi-line invoice totals to the penny against a hand calculation
- [ ] No monetary value is stored as floating point
- [ ] A failed creation consumes no invoice number
- [ ] The sequence is strictly increasing with no gaps
### Watch out
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
## Phase 2 · Clients, projects and billable time
Time you tracked becomes lines on an invoice without being entered twice.
### Steps
1. Model clients, projects and entries
Rate on the project, currency on the client, terms driving the due date.
Files: `src/clients.py`, `src/time.py`
2. Mark entries when invoiced
In the same transaction. Anything else double bills eventually.
Files: `src/invoice.py`
3. Protect invoiced entries
Locked, or edits recorded against the affected invoice. Silent edits break the audit trail.
Files: `src/time.py`
### Done when
- [ ] A second invoice for the same period includes no already-invoiced time
- [ ] Deleting a draft releases its entries
- [ ] An invoiced entry cannot be silently edited
- [ ] Rates come from the project
### Watch out
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
## Phase 3 · Issuing, which makes a document permanent
A sent invoice is fixed, and corrections happen the way accounting expects.
### Steps
1. Lock on issue
State transition, with no update path for issued invoices anywhere in the code.
Files: `src/invoice.py`
2. Store the PDF and its hash
The document the client received. The hash proves it has not changed.
Files: `src/render.py`
3. Make credit notes easy
Their own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead.
Files: `src/credit_notes.py`
### Done when
- [ ] Editing an issued invoice is refused, asserted by test
- [ ] The PDF and its hash are stored on issue
- [ ] A credit note reduces the balance and references its invoice
- [ ] Re-rendering matches the stored hash
### Watch out
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
## Phase 4 · Sending, and knowing it was sent
The client receives the invoice, and you can prove when.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices.
2. Record every send
Attempt, outcome and timestamp. This is the answer when a client says it never arrived.
Files: `src/send.py`
3. Surface bounces
Against the invoice, visibly. A bounce is information you must act on.
Files: `src/webhooks.py`
### Done when
- [ ] An invoice arrives with its PDF at three providers and avoids spam
- [ ] Every send is recorded with its outcome
- [ ] A bounce is visible against the invoice
- [ ] A resend is an additional recorded event
### Watch out
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
## Phase 5 · Payments, including the awkward ones
A single transfer covering three invoices records correctly, and so does a partial payment.
### Steps
1. Model payments and allocations separately
A payment is a real event; allocations distribute it. This is what matches the bank statement.
Files: `src/payments.py`
2. Derive invoice state
From allocations and credit notes. A settable state field will disagree with the arithmetic eventually.
Files: `src/invoice.py`
3. Handle over and under payment
Credit held for the client, partial payment with a correct remainder.
Files: `src/payments.py`
### Done when
- [ ] One payment across three invoices marks all three paid
- [ ] A partial payment leaves the correct outstanding amount
- [ ] An overpayment becomes a client credit
- [ ] Invoice state is derived rather than set
### Watch out
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
## Not in this build
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
## After v1, if you want it
- Recurring invoices, which are a template plus a schedule once issuing is correct
- Payment links through a processor, added as its own phase with webhook reconciliation
- Expense recording with receipts attached, which your accountant will ask for next
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Clients, invoices, payments. Financial records, so back them up properly.
DB_PATH=./data/books.db
# Required. Issued PDFs. Part of the record and part of the backup.
DOCUMENTS_DIR=./data/invoices
# Required. Your numbering scheme. Agree it with your accountant before the first invoice.
INVOICE_NUMBER_FORMAT=INV-{year}-{seq:04d}
# Required. Where the sequence continues from. Set it to follow your existing records.
NEXT_INVOICE_SEQ=1
# Required. Default due date offset. Overridable per client.
DEFAULT_PAYMENT_TERMS_DAYS=14
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. On the verified sending domain, or delivery will suffer.
EMAIL_FROM=invoices@example.com
You are building a lean indie version of FreshBooks.
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 =====
# FreshBooks · indie build
Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile.
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 | Python 3.12 with SQLite | one person's books, where correctness matters far more than scale |
| Money | Integer minor units throughout, never floating point | an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one |
| Numbering | A gapless sequence allocated in a transaction | the sequence is part of the record, and a gap has to be explainable |
| Documents | HTML rendered to PDF, stored immutably once sent | the sent PDF is what the client has and must remain exactly that |
| Email | A transactional provider, with every send recorded | when a client says they never received it, the send log is the answer |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Python 3.12 or newer** · free
- Why: Runs the application, the PDF rendering and the reports.
- Get it: python.org or your package manager, then a virtual environment.
- Verify: python3 --version prints 3.12 or higher
- [ ] **An HTML to PDF toolchain** · free
- Why: Invoices are documents someone else keeps, and they must render identically every time.
- Get it: A headless browser print path or a dedicated renderer. Whichever you choose, confirm two renders of the same invoice are byte-identical.
- Verify: Two renders of one invoice produce identical files
- [ ] **A transactional email provider with a verified domain** · free tiers cover a sole trader's volume
- Why: An invoice in a spam folder is an unpaid invoice, and domain verification is what prevents that.
- Get it: Create an account, add the DNS records they specify, take the credentials. Verification takes hours, so start early.
- Verify: An invoice email arrives at three providers and is not in spam
- [ ] **A conversation with whoever does your accounts** · usually free, and it saves more than it costs
- Why: Numbering, credit note handling and what they need in an export are all things they have opinions about, and matching those opinions now is cheaper than reformatting a year of records later.
- Get it: Ask what format they want, whether your numbering scheme is acceptable, and what they need to see for a correction.
- Verify: The export format has been agreed with them
- [ ] **Your existing invoices for the current year** · free
- Why: Numbering must continue from where you are rather than restarting, and importing history is what makes the reports meaningful.
- Get it: Whatever you have: a spreadsheet, another tool's export, or PDFs. Enter at least the numbers and totals so the sequence continues correctly.
- Verify: The next issued invoice continues your existing sequence
## Quick start
Follow Phase 1 in `BUILD_PLAN.md`; it creates the project.
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
- payment collection
- bank imports
- accounting reports
- client portal
- mobile apps
- tax workflows
- support
If one of those is essential to you, that is the reason to keep paying for FreshBooks, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · FreshBooks
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 invoicing for one person: clients, tracked time, sequential invoices and a record you could hand an accountant to replace FreshBooks. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Two rules decide the shape of this and both come from how invoicing actually works
rather than from software preference. Invoice numbers must be sequential without gaps
and must never be reused, because in many jurisdictions the sequence itself is part of
the record. And a sent invoice is not editable: a correction is a credit note plus a new
invoice, because the original was a document someone else received.
This also calculates no tax. Rates, thresholds, reverse charges and place-of-supply rules
vary by jurisdiction and change, and getting them wrong is your liability rather than a
bug. Record the rate a person entered and show the arithmetic; do not determine it.
### Stack (fixed, do not substitute)
- Runtime: Python 3.12 with SQLite · one person's books, where correctness matters far more than scale
- Money: Integer minor units throughout, never floating point · an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one
- Numbering: A gapless sequence allocated in a transaction · the sequence is part of the record, and a gap has to be explainable
- Documents: HTML rendered to PDF, stored immutably once sent · the sent PDF is what the client has and must remain exactly that
- Email: A transactional provider, with every send recorded · when a client says they never received it, the send log is the answer
### Data model (create this before Phase 1)
- clients(id, name, email, address, currency, payment_terms_days, notes)
- projects(id, client_id, name, rate_minor, billable, active)
- time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id)
- invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash)
- invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points
- payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor)
- credit_notes(id, number, invoice_id, issued_on, amount_minor, reason)
- A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly.
### Phase 1 · Money and numbering, before anything else
Build: Store every amount as an integer in minor units. Allocate invoice numbers from a
sequence inside the same transaction that creates the invoice, so a failure cannot
consume a number. Format only for display.
Trap: Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
Trap: Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
Done when: An invoice with several lines totals exactly, matching a hand calculation to the penny. No monetary value is stored as a floating point number, verified by inspecting the schema. A deliberately failed invoice creation consumes no number, verified by checking the sequence. Numbers are strictly increasing with no gaps.
Do not build yet: No clients, time tracking or PDFs.
### Phase 2 · Clients, projects and billable time
Build: Clients with payment terms and currency. Projects with a rate. Time entries with a
description. Invoicing a period marks those entries as invoiced so they cannot be
billed again.
Trap: Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
Done when: Invoicing a period marks its time entries and a second invoice for the same period includes none of them. Deleting a draft invoice releases its entries. A time entry cannot be edited once invoiced, or editing it is recorded with the invoice it affects. Rates come from the project rather than being typed each time.
Do not build yet: No PDFs, sending or payments yet.
### Phase 3 · Issuing, which makes a document permanent
Build: Draft invoices are editable. Issuing renders a PDF, records its hash and locks the
invoice. A correction issues a credit note against it and, if needed, a new invoice.
There is no code path that edits an issued invoice.
Trap: Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
Done when: An issued invoice cannot be edited, asserted by a test attempting it. Issuing stores the PDF and its hash. A credit note reduces the outstanding balance and references its invoice. Re-rendering an issued invoice produces a PDF whose hash matches the stored one.
Do not build yet: No sending or payments yet.
### Phase 4 · Sending, and knowing it was sent
Build: Email the PDF through a transactional provider with a verified domain. Record every
send attempt with its outcome. Handle a bounce visibly. A resend is recorded as a
separate event rather than overwriting the first.
Trap: An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
Done when: An invoice email arrives with its PDF attached at three different mail providers and is not in spam. Every send is recorded with its outcome. A bounce is surfaced against the invoice. A resend appears as an additional event with its own timestamp.
Do not build yet: No payments or reports yet.
### Phase 5 · Payments, including the awkward ones
Build: Record payments against a client and allocate amounts to specific invoices. Support
partial payment, overpayment held as a credit, and one payment across several invoices.
An invoice's state derives from its allocations rather than being set by hand.
Trap: Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
Done when: One payment allocated across three invoices marks all three paid and the payment total matches the bank figure. A partial payment leaves the invoice partly paid with the correct outstanding amount. An overpayment is held as a client credit rather than lost. Invoice state is derived rather than settable.
Do not build yet: Reports and export come last.
### Phase 6 · Reports, export and the accountant's questions
Build: Reports for a period: invoiced, paid, outstanding and aged debt. Export invoices,
payments and credit notes as CSV with the PDFs alongside. Back up the database and the
documents together, and restore them once.
Trap: Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
Done when: A period report's invoiced total matches the sum of issued invoices, verified by hand for one month. Aged debt buckets outstanding invoices by age correctly. Export produces CSV plus every PDF. A restore reproduces invoices whose PDF hashes still match, and the date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Tax calculation and determination. Rates, thresholds and place-of-supply rules vary by jurisdiction and change, and this records a rate someone entered rather than deciding it.
- Online payment collection. Adding a payment processor is a phase of its own with webhooks, reconciliation and fees, and it is worth doing separately rather than badly here.
- Bank feed imports and automatic reconciliation, which is the aggregation problem and a separate paid service.
- Full accounting: ledgers, trial balances and financial statements. This produces the records an accountant works from, not the accounts themselves.
### README must contain
- That invoice numbers are gapless and allocated transactionally, and why a gap matters
- That an issued invoice cannot be edited and corrections are credit notes
- That no tax is calculated and rates are recorded as entered
- That payments allocate across invoices so a single transfer matches the bank
- That backups must include the issued PDFs, not only the database
===== AGENTS.md =====
# Agent instructions · FreshBooks indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, Integer minor units throughout, never floating point, A gapless sequence allocated in a transaction, HTML rendered to PDF, stored immutably once sent, A transactional provider, with every send recorded. 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".
## Known traps
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
===== BUILD_PLAN.md =====
# Build plan · FreshBooks
Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Money and numbering, before anything else
Amounts are exact and invoice numbers are gapless.
### Steps
1. Use integer minor units everywhere
Storage and arithmetic. Formatting happens at display and nowhere else.
Files: `src/money.py`
2. Allocate numbers transactionally
Sequence and insert in one transaction. Test by forcing a failure mid-creation.
Files: `src/numbering.py`
3. Verify totals by hand
One multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on.
Files: `tests/test_totals.py`
### Done when
- [ ] A multi-line invoice totals to the penny against a hand calculation
- [ ] No monetary value is stored as floating point
- [ ] A failed creation consumes no invoice number
- [ ] The sequence is strictly increasing with no gaps
### Watch out
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
## Phase 2 · Clients, projects and billable time
Time you tracked becomes lines on an invoice without being entered twice.
### Steps
1. Model clients, projects and entries
Rate on the project, currency on the client, terms driving the due date.
Files: `src/clients.py`, `src/time.py`
2. Mark entries when invoiced
In the same transaction. Anything else double bills eventually.
Files: `src/invoice.py`
3. Protect invoiced entries
Locked, or edits recorded against the affected invoice. Silent edits break the audit trail.
Files: `src/time.py`
### Done when
- [ ] A second invoice for the same period includes no already-invoiced time
- [ ] Deleting a draft releases its entries
- [ ] An invoiced entry cannot be silently edited
- [ ] Rates come from the project
### Watch out
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
## Phase 3 · Issuing, which makes a document permanent
A sent invoice is fixed, and corrections happen the way accounting expects.
### Steps
1. Lock on issue
State transition, with no update path for issued invoices anywhere in the code.
Files: `src/invoice.py`
2. Store the PDF and its hash
The document the client received. The hash proves it has not changed.
Files: `src/render.py`
3. Make credit notes easy
Their own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead.
Files: `src/credit_notes.py`
### Done when
- [ ] Editing an issued invoice is refused, asserted by test
- [ ] The PDF and its hash are stored on issue
- [ ] A credit note reduces the balance and references its invoice
- [ ] Re-rendering matches the stored hash
### Watch out
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
## Phase 4 · Sending, and knowing it was sent
The client receives the invoice, and you can prove when.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices.
2. Record every send
Attempt, outcome and timestamp. This is the answer when a client says it never arrived.
Files: `src/send.py`
3. Surface bounces
Against the invoice, visibly. A bounce is information you must act on.
Files: `src/webhooks.py`
### Done when
- [ ] An invoice arrives with its PDF at three providers and avoids spam
- [ ] Every send is recorded with its outcome
- [ ] A bounce is visible against the invoice
- [ ] A resend is an additional recorded event
### Watch out
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
## Phase 5 · Payments, including the awkward ones
A single transfer covering three invoices records correctly, and so does a partial payment.
### Steps
1. Model payments and allocations separately
A payment is a real event; allocations distribute it. This is what matches the bank statement.
Files: `src/payments.py`
2. Derive invoice state
From allocations and credit notes. A settable state field will disagree with the arithmetic eventually.
Files: `src/invoice.py`
3. Handle over and under payment
Credit held for the client, partial payment with a correct remainder.
Files: `src/payments.py`
### Done when
- [ ] One payment across three invoices marks all three paid
- [ ] A partial payment leaves the correct outstanding amount
- [ ] An overpayment becomes a client credit
- [ ] Invoice state is derived rather than set
### Watch out
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
## Not in this build
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
## After v1, if you want it
- Recurring invoices, which are a template plus a schedule once issuing is correct
- Payment links through a processor, added as its own phase with webhook reconciliation
- Expense recording with receipts attached, which your accountant will ask for next
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Clients, invoices, payments. Financial records, so back them up properly.
DB_PATH=./data/books.db
# Required. Issued PDFs. Part of the record and part of the backup.
DOCUMENTS_DIR=./data/invoices
# Required. Your numbering scheme. Agree it with your accountant before the first invoice.
INVOICE_NUMBER_FORMAT=INV-{year}-{seq:04d}
# Required. Where the sequence continues from. Set it to follow your existing records.
NEXT_INVOICE_SEQ=1
# Required. Default due date offset. Overridable per client.
DEFAULT_PAYMENT_TERMS_DAYS=14
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. On the verified sending domain, or delivery will suffer.
EMAIL_FROM=invoices@example.com
You are building a production product version of FreshBooks.
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 =====
# FreshBooks · product brief
## Problem
For freelancers, invoices, time tracking, and expense logs are buildable; payments, tax/accounting reports, bank feeds, client portal, and reliability make it more than trivial.
## Product outcome
Invoicing where the arithmetic is exact, the sequence is gapless, an issued document never changes, payments match your bank statement, and the whole thing exports to something an accountant can reconcile.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- database
- invoice PDF generation
- email service
- Stripe/PayPal optional
- backups
- simple reporting
## Explicit non-goals for v1
- Tax calculation or determination.
- Online payment collection.
- Bank feed imports and reconciliation.
- Full accounting and financial statements.
- payment collection
- bank imports
- accounting reports
- client portal
- mobile apps
- tax workflows
- support
## Success criteria
- Totals match a hand calculation to the penny
- A failed creation is confirmed to consume no number
- Editing an issued invoice is confirmed refused
- One payment across three invoices is confirmed to balance
- A restore preserves matching PDF hashes
===== BRIEF.md =====
# Build brief · FreshBooks
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 invoicing for one person: clients, tracked time, sequential invoices and a record you could hand an accountant to replace FreshBooks. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Two rules decide the shape of this and both come from how invoicing actually works
rather than from software preference. Invoice numbers must be sequential without gaps
and must never be reused, because in many jurisdictions the sequence itself is part of
the record. And a sent invoice is not editable: a correction is a credit note plus a new
invoice, because the original was a document someone else received.
This also calculates no tax. Rates, thresholds, reverse charges and place-of-supply rules
vary by jurisdiction and change, and getting them wrong is your liability rather than a
bug. Record the rate a person entered and show the arithmetic; do not determine it.
### Stack (fixed, do not substitute)
- Runtime: Python 3.12 with SQLite · one person's books, where correctness matters far more than scale
- Money: Integer minor units throughout, never floating point · an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one
- Numbering: A gapless sequence allocated in a transaction · the sequence is part of the record, and a gap has to be explainable
- Documents: HTML rendered to PDF, stored immutably once sent · the sent PDF is what the client has and must remain exactly that
- Email: A transactional provider, with every send recorded · when a client says they never received it, the send log is the answer
### Data model (create this before Phase 1)
- clients(id, name, email, address, currency, payment_terms_days, notes)
- projects(id, client_id, name, rate_minor, billable, active)
- time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id)
- invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash)
- invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points
- payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor)
- credit_notes(id, number, invoice_id, issued_on, amount_minor, reason)
- A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly.
### Phase 1 · Money and numbering, before anything else
Build: Store every amount as an integer in minor units. Allocate invoice numbers from a
sequence inside the same transaction that creates the invoice, so a failure cannot
consume a number. Format only for display.
Trap: Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
Trap: Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
Done when: An invoice with several lines totals exactly, matching a hand calculation to the penny. No monetary value is stored as a floating point number, verified by inspecting the schema. A deliberately failed invoice creation consumes no number, verified by checking the sequence. Numbers are strictly increasing with no gaps.
Do not build yet: No clients, time tracking or PDFs.
### Phase 2 · Clients, projects and billable time
Build: Clients with payment terms and currency. Projects with a rate. Time entries with a
description. Invoicing a period marks those entries as invoiced so they cannot be
billed again.
Trap: Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
Done when: Invoicing a period marks its time entries and a second invoice for the same period includes none of them. Deleting a draft invoice releases its entries. A time entry cannot be edited once invoiced, or editing it is recorded with the invoice it affects. Rates come from the project rather than being typed each time.
Do not build yet: No PDFs, sending or payments yet.
### Phase 3 · Issuing, which makes a document permanent
Build: Draft invoices are editable. Issuing renders a PDF, records its hash and locks the
invoice. A correction issues a credit note against it and, if needed, a new invoice.
There is no code path that edits an issued invoice.
Trap: Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
Done when: An issued invoice cannot be edited, asserted by a test attempting it. Issuing stores the PDF and its hash. A credit note reduces the outstanding balance and references its invoice. Re-rendering an issued invoice produces a PDF whose hash matches the stored one.
Do not build yet: No sending or payments yet.
### Phase 4 · Sending, and knowing it was sent
Build: Email the PDF through a transactional provider with a verified domain. Record every
send attempt with its outcome. Handle a bounce visibly. A resend is recorded as a
separate event rather than overwriting the first.
Trap: An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
Done when: An invoice email arrives with its PDF attached at three different mail providers and is not in spam. Every send is recorded with its outcome. A bounce is surfaced against the invoice. A resend appears as an additional event with its own timestamp.
Do not build yet: No payments or reports yet.
### Phase 5 · Payments, including the awkward ones
Build: Record payments against a client and allocate amounts to specific invoices. Support
partial payment, overpayment held as a credit, and one payment across several invoices.
An invoice's state derives from its allocations rather than being set by hand.
Trap: Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
Done when: One payment allocated across three invoices marks all three paid and the payment total matches the bank figure. A partial payment leaves the invoice partly paid with the correct outstanding amount. An overpayment is held as a client credit rather than lost. Invoice state is derived rather than settable.
Do not build yet: Reports and export come last.
### Phase 6 · Reports, export and the accountant's questions
Build: Reports for a period: invoiced, paid, outstanding and aged debt. Export invoices,
payments and credit notes as CSV with the PDFs alongside. Back up the database and the
documents together, and restore them once.
Trap: Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
Done when: A period report's invoiced total matches the sum of issued invoices, verified by hand for one month. Aged debt buckets outstanding invoices by age correctly. Export produces CSV plus every PDF. A restore reproduces invoices whose PDF hashes still match, and the date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Tax calculation and determination. Rates, thresholds and place-of-supply rules vary by jurisdiction and change, and this records a rate someone entered rather than deciding it.
- Online payment collection. Adding a payment processor is a phase of its own with webhooks, reconciliation and fees, and it is worth doing separately rather than badly here.
- Bank feed imports and automatic reconciliation, which is the aggregation problem and a separate paid service.
- Full accounting: ledgers, trial balances and financial statements. This produces the records an accountant works from, not the accounts themselves.
### README must contain
- That invoice numbers are gapless and allocated transactionally, and why a gap matters
- That an issued invoice cannot be edited and corrections are credit notes
- That no tax is calculated and rates are recorded as entered
- That payments allocate across invoices so a single transfer matches the bank
- That backups must include the issued PDFs, not only the database
===== ARCHITECTURE.md =====
# Architecture · FreshBooks
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Python 3.12 with SQLite | one person's books, where correctness matters far more than scale |
| Money | Integer minor units throughout, never floating point | an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one |
| Numbering | A gapless sequence allocated in a transaction | the sequence is part of the record, and a gap has to be explainable |
| Documents | HTML rendered to PDF, stored immutably once sent | the sent PDF is what the client has and must remain exactly that |
| Email | A transactional provider, with every send recorded | when a client says they never received it, the send log is the answer |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Money | Integer minor units and formatting. | Not swappable for floating point at any point. Every total depends on it. |
| Numbering | The gapless sequence. | The transactional allocation is the correctness property, not the format. |
| Documents | Rendering and immutability after issue. | Any renderer producing identical output. The lock on issue is not negotiable. |
| Payments | Allocation across invoices. | The allocation model is what matches reality. A payment belonging to one invoice cannot represent the common case. |
| Export | Handing records to an accountant. | Format follows their preference, which is why you ask first. |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `DB_PATH` · required · Clients, invoices, payments. Financial records, so back them up properly.
- `DOCUMENTS_DIR` · required · Issued PDFs. Part of the record and part of the backup.
- `INVOICE_NUMBER_FORMAT` · required · Your numbering scheme. Agree it with your accountant before the first invoice.
- `NEXT_INVOICE_SEQ` · required · Where the sequence continues from. Set it to follow your existing records.
- `DEFAULT_PAYMENT_TERMS_DAYS` · required · Default due date offset. Overridable per client.
- `EMAIL_SERVER` · required, secret · Transactional provider credentials.
- `EMAIL_FROM` · required · On the verified sending domain, or delivery will suffer.
## 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 · FreshBooks product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with SQLite, Integer minor units throughout, never floating point, A gapless sequence allocated in a transaction, HTML rendered to PDF, stored immutably once sent, A transactional provider, with every send recorded.
- 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.
## Known traps
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
- Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
===== MILESTONES.md =====
# Delivery milestones · FreshBooks
Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Money and numbering, before anything else
Amounts are exact and invoice numbers are gapless.
### Steps
1. Use integer minor units everywhere
Storage and arithmetic. Formatting happens at display and nowhere else.
Files: `src/money.py`
2. Allocate numbers transactionally
Sequence and insert in one transaction. Test by forcing a failure mid-creation.
Files: `src/numbering.py`
3. Verify totals by hand
One multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on.
Files: `tests/test_totals.py`
### Done when
- [ ] A multi-line invoice totals to the penny against a hand calculation
- [ ] No monetary value is stored as floating point
- [ ] A failed creation consumes no invoice number
- [ ] The sequence is strictly increasing with no gaps
### Watch out
- Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere.
- Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction.
## M2 · Clients, projects and billable time
Time you tracked becomes lines on an invoice without being entered twice.
### Steps
1. Model clients, projects and entries
Rate on the project, currency on the client, terms driving the due date.
Files: `src/clients.py`, `src/time.py`
2. Mark entries when invoiced
In the same transaction. Anything else double bills eventually.
Files: `src/invoice.py`
3. Protect invoiced entries
Locked, or edits recorded against the affected invoice. Silent edits break the audit trail.
Files: `src/time.py`
### Done when
- [ ] A second invoice for the same period includes no already-invoiced time
- [ ] Deleting a draft releases its entries
- [ ] An invoiced entry cannot be silently edited
- [ ] Rates come from the project
### Watch out
- Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice.
## M3 · Issuing, which makes a document permanent
A sent invoice is fixed, and corrections happen the way accounting expects.
### Steps
1. Lock on issue
State transition, with no update path for issued invoices anywhere in the code.
Files: `src/invoice.py`
2. Store the PDF and its hash
The document the client received. The hash proves it has not changed.
Files: `src/render.py`
3. Make credit notes easy
Their own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead.
Files: `src/credit_notes.py`
### Done when
- [ ] Editing an issued invoice is refused, asserted by test
- [ ] The PDF and its hash are stored on issue
- [ ] A credit note reduces the balance and references its invoice
- [ ] Re-rendering matches the stored hash
### Watch out
- Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock.
## M4 · Sending, and knowing it was sent
The client receives the invoice, and you can prove when.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices.
2. Record every send
Attempt, outcome and timestamp. This is the answer when a client says it never arrived.
Files: `src/send.py`
3. Surface bounces
Against the invoice, visibly. A bounce is information you must act on.
Files: `src/webhooks.py`
### Done when
- [ ] An invoice arrives with its PDF at three providers and avoids spam
- [ ] Every send is recorded with its outcome
- [ ] A bounce is visible against the invoice
- [ ] A resend is an additional recorded event
### Watch out
- An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence.
## M5 · Payments, including the awkward ones
A single transfer covering three invoices records correctly, and so does a partial payment.
### Steps
1. Model payments and allocations separately
A payment is a real event; allocations distribute it. This is what matches the bank statement.
Files: `src/payments.py`
2. Derive invoice state
From allocations and credit notes. A settable state field will disagree with the arithmetic eventually.
Files: `src/invoice.py`
3. Handle over and under payment
Credit held for the client, partial payment with a correct remainder.
Files: `src/payments.py`
### Done when
- [ ] One payment across three invoices marks all three paid
- [ ] A partial payment leaves the correct outstanding amount
- [ ] An overpayment becomes a client credit
- [ ] Invoice state is derived rather than set
### Watch out
- Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
## M6 · Reports, export and the accountant's questions (production only)
You can answer what you invoiced, what you were paid and what is outstanding, and hand it over.
### Steps
1. Build the period reports
Invoiced, paid, outstanding, aged. Verify one month by hand.
Files: `src/reports.py`
2. Export everything with documents
CSV plus PDFs. An accountant should not need this application.
Files: `src/export.py`
3. Back up and restore both halves
Database and documents together. Verify hashes after restore.
Files: `scripts/backup.sh`, `README.md`
### Done when
- [ ] Report totals match a hand calculation for one month
- [ ] Aged debt buckets correctly
- [ ] Export includes CSV and every PDF
- [ ] A restore preserves matching PDF hashes and is dated
### Watch out
- Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
===== OPERATIONS.md =====
# Operations · FreshBooks
## Backup
The database and the issued PDFs together, encrypted, off the machine. These are financial records with retention obligations in most places, so the retention period is a decision rather than a preference.
## Restore
Restore both and verify a sample of PDF hashes still match their invoices. A record pointing at a missing document is not a complete restore.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Outstanding balance by age and unsent issued invoices. An invoice issued and never sent is a common and expensive oversight.
## Incident checklist
If an invoice was sent with an error, the credit note path is the answer rather than an edit, and the lock exists to make sure that is the path taken. If numbering has a gap, it will need explaining, which is why allocation is transactional.
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 match a hand calculation to the penny
- [ ] A failed creation is confirmed to consume no number
- [ ] Editing an issued invoice is confirmed refused
- [ ] One payment across three invoices is confirmed to balance
- [ ] A restore preserves matching PDF hashes
## Launch constraint
Do not market omitted FreshBooks 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. Clients, invoices, payments. Financial records, so back them up properly.
DB_PATH=./data/books.db
# Required. Issued PDFs. Part of the record and part of the backup.
DOCUMENTS_DIR=./data/invoices
# Required. Your numbering scheme. Agree it with your accountant before the first invoice.
INVOICE_NUMBER_FORMAT=INV-{year}-{seq:04d}
# Required. Where the sequence continues from. Set it to follow your existing records.
NEXT_INVOICE_SEQ=1
# Required. Default due date offset. Overridable per client.
DEFAULT_PAYMENT_TERMS_DAYS=14
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. On the verified sending domain, or delivery will suffer.
EMAIL_FROM=invoices@example.com
# FreshBooks · indie build Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile. 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 | Python 3.12 with SQLite | one person's books, where correctness matters far more than scale | | Money | Integer minor units throughout, never floating point | an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one | | Numbering | A gapless sequence allocated in a transaction | the sequence is part of the record, and a gap has to be explainable | | Documents | HTML rendered to PDF, stored immutably once sent | the sent PDF is what the client has and must remain exactly that | | Email | A transactional provider, with every send recorded | when a client says they never received it, the send log is the answer | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the application, the PDF rendering and the reports. - Get it: python.org or your package manager, then a virtual environment. - Verify: python3 --version prints 3.12 or higher - [ ] **An HTML to PDF toolchain** · free - Why: Invoices are documents someone else keeps, and they must render identically every time. - Get it: A headless browser print path or a dedicated renderer. Whichever you choose, confirm two renders of the same invoice are byte-identical. - Verify: Two renders of one invoice produce identical files - [ ] **A transactional email provider with a verified domain** · free tiers cover a sole trader's volume - Why: An invoice in a spam folder is an unpaid invoice, and domain verification is what prevents that. - Get it: Create an account, add the DNS records they specify, take the credentials. Verification takes hours, so start early. - Verify: An invoice email arrives at three providers and is not in spam - [ ] **A conversation with whoever does your accounts** · usually free, and it saves more than it costs - Why: Numbering, credit note handling and what they need in an export are all things they have opinions about, and matching those opinions now is cheaper than reformatting a year of records later. - Get it: Ask what format they want, whether your numbering scheme is acceptable, and what they need to see for a correction. - Verify: The export format has been agreed with them - [ ] **Your existing invoices for the current year** · free - Why: Numbering must continue from where you are rather than restarting, and importing history is what makes the reports meaningful. - Get it: Whatever you have: a spreadsheet, another tool's export, or PDFs. Enter at least the numbers and totals so the sequence continues correctly. - Verify: The next issued invoice continues your existing sequence ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Tax calculation or determination. - Online payment collection. - Bank feed imports and reconciliation. - Full accounting and financial statements. - payment collection - bank imports - accounting reports - client portal - mobile apps - tax workflows - support If one of those is essential to you, that is the reason to keep paying for FreshBooks, and the README should say so rather than pretend.
# Build brief · FreshBooks 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 invoicing for one person: clients, tracked time, sequential invoices and a record you could hand an accountant to replace FreshBooks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules decide the shape of this and both come from how invoicing actually works rather than from software preference. Invoice numbers must be sequential without gaps and must never be reused, because in many jurisdictions the sequence itself is part of the record. And a sent invoice is not editable: a correction is a credit note plus a new invoice, because the original was a document someone else received. This also calculates no tax. Rates, thresholds, reverse charges and place-of-supply rules vary by jurisdiction and change, and getting them wrong is your liability rather than a bug. Record the rate a person entered and show the arithmetic; do not determine it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · one person's books, where correctness matters far more than scale - Money: Integer minor units throughout, never floating point · an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one - Numbering: A gapless sequence allocated in a transaction · the sequence is part of the record, and a gap has to be explainable - Documents: HTML rendered to PDF, stored immutably once sent · the sent PDF is what the client has and must remain exactly that - Email: A transactional provider, with every send recorded · when a client says they never received it, the send log is the answer ### Data model (create this before Phase 1) - clients(id, name, email, address, currency, payment_terms_days, notes) - projects(id, client_id, name, rate_minor, billable, active) - time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id) - invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash) - invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points - payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor) - credit_notes(id, number, invoice_id, issued_on, amount_minor, reason) - A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly. ### Phase 1 · Money and numbering, before anything else Build: Store every amount as an integer in minor units. Allocate invoice numbers from a sequence inside the same transaction that creates the invoice, so a failure cannot consume a number. Format only for display. Trap: Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. Trap: Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. Done when: An invoice with several lines totals exactly, matching a hand calculation to the penny. No monetary value is stored as a floating point number, verified by inspecting the schema. A deliberately failed invoice creation consumes no number, verified by checking the sequence. Numbers are strictly increasing with no gaps. Do not build yet: No clients, time tracking or PDFs. ### Phase 2 · Clients, projects and billable time Build: Clients with payment terms and currency. Projects with a rate. Time entries with a description. Invoicing a period marks those entries as invoiced so they cannot be billed again. Trap: Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. Done when: Invoicing a period marks its time entries and a second invoice for the same period includes none of them. Deleting a draft invoice releases its entries. A time entry cannot be edited once invoiced, or editing it is recorded with the invoice it affects. Rates come from the project rather than being typed each time. Do not build yet: No PDFs, sending or payments yet. ### Phase 3 · Issuing, which makes a document permanent Build: Draft invoices are editable. Issuing renders a PDF, records its hash and locks the invoice. A correction issues a credit note against it and, if needed, a new invoice. There is no code path that edits an issued invoice. Trap: Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. Done when: An issued invoice cannot be edited, asserted by a test attempting it. Issuing stores the PDF and its hash. A credit note reduces the outstanding balance and references its invoice. Re-rendering an issued invoice produces a PDF whose hash matches the stored one. Do not build yet: No sending or payments yet. ### Phase 4 · Sending, and knowing it was sent Build: Email the PDF through a transactional provider with a verified domain. Record every send attempt with its outcome. Handle a bounce visibly. A resend is recorded as a separate event rather than overwriting the first. Trap: An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. Done when: An invoice email arrives with its PDF attached at three different mail providers and is not in spam. Every send is recorded with its outcome. A bounce is surfaced against the invoice. A resend appears as an additional event with its own timestamp. Do not build yet: No payments or reports yet. ### Phase 5 · Payments, including the awkward ones Build: Record payments against a client and allocate amounts to specific invoices. Support partial payment, overpayment held as a credit, and one payment across several invoices. An invoice's state derives from its allocations rather than being set by hand. Trap: Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance. Done when: One payment allocated across three invoices marks all three paid and the payment total matches the bank figure. A partial payment leaves the invoice partly paid with the correct outstanding amount. An overpayment is held as a client credit rather than lost. Invoice state is derived rather than settable. Do not build yet: Reports and export come last. ### Phase 6 · Reports, export and the accountant's questions Build: Reports for a period: invoiced, paid, outstanding and aged debt. Export invoices, payments and credit notes as CSV with the PDFs alongside. Back up the database and the documents together, and restore them once. Trap: Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches. Done when: A period report's invoiced total matches the sum of issued invoices, verified by hand for one month. Aged debt buckets outstanding invoices by age correctly. Export produces CSV plus every PDF. A restore reproduces invoices whose PDF hashes still match, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Tax calculation and determination. Rates, thresholds and place-of-supply rules vary by jurisdiction and change, and this records a rate someone entered rather than deciding it. - Online payment collection. Adding a payment processor is a phase of its own with webhooks, reconciliation and fees, and it is worth doing separately rather than badly here. - Bank feed imports and automatic reconciliation, which is the aggregation problem and a separate paid service. - Full accounting: ledgers, trial balances and financial statements. This produces the records an accountant works from, not the accounts themselves. ### README must contain - That invoice numbers are gapless and allocated transactionally, and why a gap matters - That an issued invoice cannot be edited and corrections are credit notes - That no tax is calculated and rates are recorded as entered - That payments allocate across invoices so a single transfer matches the bank - That backups must include the issued PDFs, not only the database
# Agent instructions · FreshBooks indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with SQLite, Integer minor units throughout, never floating point, A gapless sequence allocated in a transaction, HTML rendered to PDF, stored immutably once sent, A transactional provider, with every send recorded. 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". ## Known traps - Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. - Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. - Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. - Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. - An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. - Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance.
# Build plan · FreshBooks Clients, projects and tracked time turning into invoices with a gapless sequence, PDFs emailed and recorded, payments applied against invoices with partial payment handled properly, credit notes for corrections, and exports an accountant can reconcile. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Money and numbering, before anything else Amounts are exact and invoice numbers are gapless. ### Steps 1. Use integer minor units everywhere Storage and arithmetic. Formatting happens at display and nowhere else. Files: `src/money.py` 2. Allocate numbers transactionally Sequence and insert in one transaction. Test by forcing a failure mid-creation. Files: `src/numbering.py` 3. Verify totals by hand One multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on. Files: `tests/test_totals.py` ### Done when - [ ] A multi-line invoice totals to the penny against a hand calculation - [ ] No monetary value is stored as floating point - [ ] A failed creation consumes no invoice number - [ ] The sequence is strictly increasing with no gaps ### Watch out - Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. - Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. ## Phase 2 · Clients, projects and billable time Time you tracked becomes lines on an invoice without being entered twice. ### Steps 1. Model clients, projects and entries Rate on the project, currency on the client, terms driving the due date. Files: `src/clients.py`, `src/time.py` 2. Mark entries when invoiced In the same transaction. Anything else double bills eventually. Files: `src/invoice.py` 3. Protect invoiced entries Locked, or edits recorded against the affected invoice. Silent edits break the audit trail. Files: `src/time.py` ### Done when - [ ] A second invoice for the same period includes no already-invoiced time - [ ] Deleting a draft releases its entries - [ ] An invoiced entry cannot be silently edited - [ ] Rates come from the project ### Watch out - Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. ## Phase 3 · Issuing, which makes a document permanent A sent invoice is fixed, and corrections happen the way accounting expects. ### Steps 1. Lock on issue State transition, with no update path for issued invoices anywhere in the code. Files: `src/invoice.py` 2. Store the PDF and its hash The document the client received. The hash proves it has not changed. Files: `src/render.py` 3. Make credit notes easy Their own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead. Files: `src/credit_notes.py` ### Done when - [ ] Editing an issued invoice is refused, asserted by test - [ ] The PDF and its hash are stored on issue - [ ] A credit note reduces the balance and references its invoice - [ ] Re-rendering matches the stored hash ### Watch out - Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. ## Phase 4 · Sending, and knowing it was sent The client receives the invoice, and you can prove when. ### Steps 1. Verify the sending domain The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices. 2. Record every send Attempt, outcome and timestamp. This is the answer when a client says it never arrived. Files: `src/send.py` 3. Surface bounces Against the invoice, visibly. A bounce is information you must act on. Files: `src/webhooks.py` ### Done when - [ ] An invoice arrives with its PDF at three providers and avoids spam - [ ] Every send is recorded with its outcome - [ ] A bounce is visible against the invoice - [ ] A resend is an additional recorded event ### Watch out - An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. ## Phase 5 · Payments, including the awkward ones A single transfer covering three invoices records correctly, and so does a partial payment. ### Steps 1. Model payments and allocations separately A payment is a real event; allocations distribute it. This is what matches the bank statement. Files: `src/payments.py` 2. Derive invoice state From allocations and credit notes. A settable state field will disagree with the arithmetic eventually. Files: `src/invoice.py` 3. Handle over and under payment Credit held for the client, partial payment with a correct remainder. Files: `src/payments.py` ### Done when - [ ] One payment across three invoices marks all three paid - [ ] A partial payment leaves the correct outstanding amount - [ ] An overpayment becomes a client credit - [ ] Invoice state is derived rather than set ### Watch out - Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance. ## Not in this build - Tax calculation or determination. - Online payment collection. - Bank feed imports and reconciliation. - Full accounting and financial statements. ## After v1, if you want it - Recurring invoices, which are a template plus a schedule once issuing is correct - Payment links through a processor, added as its own phase with webhook reconciliation - Expense recording with receipts attached, which your accountant will ask for next
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Clients, invoices, payments. Financial records, so back them up properly.
DB_PATH=./data/books.db
# Required. Issued PDFs. Part of the record and part of the backup.
DOCUMENTS_DIR=./data/invoices
# Required. Your numbering scheme. Agree it with your accountant before the first invoice.
INVOICE_NUMBER_FORMAT=INV-{year}-{seq:04d}
# Required. Where the sequence continues from. Set it to follow your existing records.
NEXT_INVOICE_SEQ=1
# Required. Default due date offset. Overridable per client.
DEFAULT_PAYMENT_TERMS_DAYS=14
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. On the verified sending domain, or delivery will suffer.
EMAIL_FROM=invoices@example.com
# FreshBooks · product brief ## Problem For freelancers, invoices, time tracking, and expense logs are buildable; payments, tax/accounting reports, bank feeds, client portal, and reliability make it more than trivial. ## Product outcome Invoicing where the arithmetic is exact, the sequence is gapless, an issued document never changes, payments match your bank statement, and the whole thing exports to something an accountant can reconcile. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - database - invoice PDF generation - email service - Stripe/PayPal optional - backups - simple reporting ## Explicit non-goals for v1 - Tax calculation or determination. - Online payment collection. - Bank feed imports and reconciliation. - Full accounting and financial statements. - payment collection - bank imports - accounting reports - client portal - mobile apps - tax workflows - support ## Success criteria - Totals match a hand calculation to the penny - A failed creation is confirmed to consume no number - Editing an issued invoice is confirmed refused - One payment across three invoices is confirmed to balance - A restore preserves matching PDF hashes
# Build brief · FreshBooks 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 invoicing for one person: clients, tracked time, sequential invoices and a record you could hand an accountant to replace FreshBooks. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two rules decide the shape of this and both come from how invoicing actually works rather than from software preference. Invoice numbers must be sequential without gaps and must never be reused, because in many jurisdictions the sequence itself is part of the record. And a sent invoice is not editable: a correction is a credit note plus a new invoice, because the original was a document someone else received. This also calculates no tax. Rates, thresholds, reverse charges and place-of-supply rules vary by jurisdiction and change, and getting them wrong is your liability rather than a bug. Record the rate a person entered and show the arithmetic; do not determine it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with SQLite · one person's books, where correctness matters far more than scale - Money: Integer minor units throughout, never floating point · an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one - Numbering: A gapless sequence allocated in a transaction · the sequence is part of the record, and a gap has to be explainable - Documents: HTML rendered to PDF, stored immutably once sent · the sent PDF is what the client has and must remain exactly that - Email: A transactional provider, with every send recorded · when a client says they never received it, the send log is the answer ### Data model (create this before Phase 1) - clients(id, name, email, address, currency, payment_terms_days, notes) - projects(id, client_id, name, rate_minor, billable, active) - time_entries(id, project_id, started_at, minutes, description, invoiced_on_invoice_id) - invoices(id, number, client_id, issued_on, due_on, currency, state, subtotal_minor, tax_minor, total_minor, pdf_path, pdf_hash) - invoice_lines(id, invoice_id, description, quantity, unit_minor, tax_rate_bp, amount_minor) · tax_rate_bp is a rate someone entered, in basis points - payments(id, client_id, received_on, amount_minor, method, reference), allocations(payment_id, invoice_id, amount_minor) - credit_notes(id, number, invoice_id, issued_on, amount_minor, reason) - A payment is allocated to invoices rather than belonging to one, so a single transfer covering three invoices records correctly. ### Phase 1 · Money and numbering, before anything else Build: Store every amount as an integer in minor units. Allocate invoice numbers from a sequence inside the same transaction that creates the invoice, so a failure cannot consume a number. Format only for display. Trap: Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. Trap: Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. Done when: An invoice with several lines totals exactly, matching a hand calculation to the penny. No monetary value is stored as a floating point number, verified by inspecting the schema. A deliberately failed invoice creation consumes no number, verified by checking the sequence. Numbers are strictly increasing with no gaps. Do not build yet: No clients, time tracking or PDFs. ### Phase 2 · Clients, projects and billable time Build: Clients with payment terms and currency. Projects with a rate. Time entries with a description. Invoicing a period marks those entries as invoiced so they cannot be billed again. Trap: Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. Done when: Invoicing a period marks its time entries and a second invoice for the same period includes none of them. Deleting a draft invoice releases its entries. A time entry cannot be edited once invoiced, or editing it is recorded with the invoice it affects. Rates come from the project rather than being typed each time. Do not build yet: No PDFs, sending or payments yet. ### Phase 3 · Issuing, which makes a document permanent Build: Draft invoices are editable. Issuing renders a PDF, records its hash and locks the invoice. A correction issues a credit note against it and, if needed, a new invoice. There is no code path that edits an issued invoice. Trap: Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. Done when: An issued invoice cannot be edited, asserted by a test attempting it. Issuing stores the PDF and its hash. A credit note reduces the outstanding balance and references its invoice. Re-rendering an issued invoice produces a PDF whose hash matches the stored one. Do not build yet: No sending or payments yet. ### Phase 4 · Sending, and knowing it was sent Build: Email the PDF through a transactional provider with a verified domain. Record every send attempt with its outcome. Handle a bounce visibly. A resend is recorded as a separate event rather than overwriting the first. Trap: An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. Done when: An invoice email arrives with its PDF attached at three different mail providers and is not in spam. Every send is recorded with its outcome. A bounce is surfaced against the invoice. A resend appears as an additional event with its own timestamp. Do not build yet: No payments or reports yet. ### Phase 5 · Payments, including the awkward ones Build: Record payments against a client and allocate amounts to specific invoices. Support partial payment, overpayment held as a credit, and one payment across several invoices. An invoice's state derives from its allocations rather than being set by hand. Trap: Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance. Done when: One payment allocated across three invoices marks all three paid and the payment total matches the bank figure. A partial payment leaves the invoice partly paid with the correct outstanding amount. An overpayment is held as a client credit rather than lost. Invoice state is derived rather than settable. Do not build yet: Reports and export come last. ### Phase 6 · Reports, export and the accountant's questions Build: Reports for a period: invoiced, paid, outstanding and aged debt. Export invoices, payments and credit notes as CSV with the PDFs alongside. Back up the database and the documents together, and restore them once. Trap: Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches. Done when: A period report's invoiced total matches the sum of issued invoices, verified by hand for one month. Aged debt buckets outstanding invoices by age correctly. Export produces CSV plus every PDF. A restore reproduces invoices whose PDF hashes still match, and the date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Tax calculation and determination. Rates, thresholds and place-of-supply rules vary by jurisdiction and change, and this records a rate someone entered rather than deciding it. - Online payment collection. Adding a payment processor is a phase of its own with webhooks, reconciliation and fees, and it is worth doing separately rather than badly here. - Bank feed imports and automatic reconciliation, which is the aggregation problem and a separate paid service. - Full accounting: ledgers, trial balances and financial statements. This produces the records an accountant works from, not the accounts themselves. ### README must contain - That invoice numbers are gapless and allocated transactionally, and why a gap matters - That an issued invoice cannot be edited and corrections are credit notes - That no tax is calculated and rates are recorded as entered - That payments allocate across invoices so a single transfer matches the bank - That backups must include the issued PDFs, not only the database
# Architecture · FreshBooks ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with SQLite | one person's books, where correctness matters far more than scale | | Money | Integer minor units throughout, never floating point | an invoice that disagrees with a payment by a penny is a real problem and a floating point total will eventually produce one | | Numbering | A gapless sequence allocated in a transaction | the sequence is part of the record, and a gap has to be explainable | | Documents | HTML rendered to PDF, stored immutably once sent | the sent PDF is what the client has and must remain exactly that | | Email | A transactional provider, with every send recorded | when a client says they never received it, the send log is the answer | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Money | Integer minor units and formatting. | Not swappable for floating point at any point. Every total depends on it. | | Numbering | The gapless sequence. | The transactional allocation is the correctness property, not the format. | | Documents | Rendering and immutability after issue. | Any renderer producing identical output. The lock on issue is not negotiable. | | Payments | Allocation across invoices. | The allocation model is what matches reality. A payment belonging to one invoice cannot represent the common case. | | Export | Handing records to an accountant. | Format follows their preference, which is why you ask first. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Clients, invoices, payments. Financial records, so back them up properly. - `DOCUMENTS_DIR` · required · Issued PDFs. Part of the record and part of the backup. - `INVOICE_NUMBER_FORMAT` · required · Your numbering scheme. Agree it with your accountant before the first invoice. - `NEXT_INVOICE_SEQ` · required · Where the sequence continues from. Set it to follow your existing records. - `DEFAULT_PAYMENT_TERMS_DAYS` · required · Default due date offset. Overridable per client. - `EMAIL_SERVER` · required, secret · Transactional provider credentials. - `EMAIL_FROM` · required · On the verified sending domain, or delivery will suffer. ## 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 · FreshBooks product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with SQLite, Integer minor units throughout, never floating point, A gapless sequence allocated in a transaction, HTML rendered to PDF, stored immutably once sent, A transactional provider, with every send recorded. - 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. ## Known traps - Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. - Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. - Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. - Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. - An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. - Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance. - Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
# Delivery milestones · FreshBooks Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Money and numbering, before anything else Amounts are exact and invoice numbers are gapless. ### Steps 1. Use integer minor units everywhere Storage and arithmetic. Formatting happens at display and nowhere else. Files: `src/money.py` 2. Allocate numbers transactionally Sequence and insert in one transaction. Test by forcing a failure mid-creation. Files: `src/numbering.py` 3. Verify totals by hand One multi-line invoice, computed by hand, asserted in a test. This is the arithmetic everything rests on. Files: `tests/test_totals.py` ### Done when - [ ] A multi-line invoice totals to the penny against a hand calculation - [ ] No monetary value is stored as floating point - [ ] A failed creation consumes no invoice number - [ ] The sequence is strictly increasing with no gaps ### Watch out - Floating point arithmetic on money produces totals that are off by a penny after enough lines, and reconciling that against a bank payment is an afternoon you will not enjoy. Integer minor units, everywhere. - Allocating a number before the invoice is committed leaves a gap when creation fails, and a gap in an invoice sequence is something you have to explain rather than shrug at. Allocate and insert in one transaction. ## M2 · Clients, projects and billable time Time you tracked becomes lines on an invoice without being entered twice. ### Steps 1. Model clients, projects and entries Rate on the project, currency on the client, terms driving the due date. Files: `src/clients.py`, `src/time.py` 2. Mark entries when invoiced In the same transaction. Anything else double bills eventually. Files: `src/invoice.py` 3. Protect invoiced entries Locked, or edits recorded against the affected invoice. Silent edits break the audit trail. Files: `src/time.py` ### Done when - [ ] A second invoice for the same period includes no already-invoiced time - [ ] Deleting a draft releases its entries - [ ] An invoiced entry cannot be silently edited - [ ] Rates come from the project ### Watch out - Time entries that are not marked when invoiced will be billed twice the next time you invoice that client, and the client is the one who notices. Record the invoice on the entry as part of the same transaction that creates the invoice. ## M3 · Issuing, which makes a document permanent A sent invoice is fixed, and corrections happen the way accounting expects. ### Steps 1. Lock on issue State transition, with no update path for issued invoices anywhere in the code. Files: `src/invoice.py` 2. Store the PDF and its hash The document the client received. The hash proves it has not changed. Files: `src/render.py` 3. Make credit notes easy Their own sequence, referencing the invoice, reducing the balance. Easy enough that nobody wants to edit instead. Files: `src/credit_notes.py` ### Done when - [ ] Editing an issued invoice is refused, asserted by test - [ ] The PDF and its hash are stored on issue - [ ] A credit note reduces the balance and references its invoice - [ ] Re-rendering matches the stored hash ### Watch out - Editing an issued invoice means the copy your client holds and the copy in your records disagree, and if it has been included in a tax return the discrepancy is a real problem. Lock on issue, and make the credit note path as easy as editing would have been, or people will work around the lock. ## M4 · Sending, and knowing it was sent The client receives the invoice, and you can prove when. ### Steps 1. Verify the sending domain The DNS records your provider specifies. Invoices in a spam folder are unpaid invoices. 2. Record every send Attempt, outcome and timestamp. This is the answer when a client says it never arrived. Files: `src/send.py` 3. Surface bounces Against the invoice, visibly. A bounce is information you must act on. Files: `src/webhooks.py` ### Done when - [ ] An invoice arrives with its PDF at three providers and avoids spam - [ ] Every send is recorded with its outcome - [ ] A bounce is visible against the invoice - [ ] A resend is an additional recorded event ### Watch out - An invoice email that lands in spam is indistinguishable from one that was ignored, and chasing payment for something never received damages the relationship. Verify the sending domain properly, and record delivery outcomes so a bounce is visible rather than assumed to be silence. ## M5 · Payments, including the awkward ones A single transfer covering three invoices records correctly, and so does a partial payment. ### Steps 1. Model payments and allocations separately A payment is a real event; allocations distribute it. This is what matches the bank statement. Files: `src/payments.py` 2. Derive invoice state From allocations and credit notes. A settable state field will disagree with the arithmetic eventually. Files: `src/invoice.py` 3. Handle over and under payment Credit held for the client, partial payment with a correct remainder. Files: `src/payments.py` ### Done when - [ ] One payment across three invoices marks all three paid - [ ] A partial payment leaves the correct outstanding amount - [ ] An overpayment becomes a client credit - [ ] Invoice state is derived rather than set ### Watch out - Modelling a payment as belonging to one invoice cannot represent the common case of a client paying three invoices in one transfer, and people then split the payment into fictional parts that no longer match the bank statement. Allocate instead, so the payment matches reality and the invoices still balance. ## M6 · Reports, export and the accountant's questions (production only) You can answer what you invoiced, what you were paid and what is outstanding, and hand it over. ### Steps 1. Build the period reports Invoiced, paid, outstanding, aged. Verify one month by hand. Files: `src/reports.py` 2. Export everything with documents CSV plus PDFs. An accountant should not need this application. Files: `src/export.py` 3. Back up and restore both halves Database and documents together. Verify hashes after restore. Files: `scripts/backup.sh`, `README.md` ### Done when - [ ] Report totals match a hand calculation for one month - [ ] Aged debt buckets correctly - [ ] Export includes CSV and every PDF - [ ] A restore preserves matching PDF hashes and is dated ### Watch out - Backing up the database without the issued PDFs leaves you with records referencing documents that no longer exist, and the PDF is the thing your client and your accountant actually hold. Back up both, and verify a restored invoice's PDF hash still matches.
# Operations · FreshBooks ## Backup The database and the issued PDFs together, encrypted, off the machine. These are financial records with retention obligations in most places, so the retention period is a decision rather than a preference. ## Restore Restore both and verify a sample of PDF hashes still match their invoices. A record pointing at a missing document is not a complete restore. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Outstanding balance by age and unsent issued invoices. An invoice issued and never sent is a common and expensive oversight. ## Incident checklist If an invoice was sent with an error, the credit note path is the answer rather than an edit, and the lock exists to make sure that is the path taken. If numbering has a gap, it will need explaining, which is why allocation is transactional. 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 match a hand calculation to the penny - [ ] A failed creation is confirmed to consume no number - [ ] Editing an issued invoice is confirmed refused - [ ] One payment across three invoices is confirmed to balance - [ ] A restore preserves matching PDF hashes ## Launch constraint Do not market omitted FreshBooks 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. Clients, invoices, payments. Financial records, so back them up properly.
DB_PATH=./data/books.db
# Required. Issued PDFs. Part of the record and part of the backup.
DOCUMENTS_DIR=./data/invoices
# Required. Your numbering scheme. Agree it with your accountant before the first invoice.
INVOICE_NUMBER_FORMAT=INV-{year}-{seq:04d}
# Required. Where the sequence continues from. Set it to follow your existing records.
NEXT_INVOICE_SEQ=1
# Required. Default due date offset. Overridable per client.
DEFAULT_PAYMENT_TERMS_DAYS=14
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. On the verified sending domain, or delivery will suffer.
EMAIL_FROM=invoices@example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because client billing needs to look professional and survive audit/accountant handoff.
xpayment collection
xbank imports
xaccounting reports
xclient portal
xmobile apps
xtax workflows
xsupport
Don't feel like building it? These folks already made it free.
all 4 free alternatives to FreshBooks →· no votes, no pay-to-list · just what's real
FreshBooks pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| lite | $23/workspace | $20.70/workspace | Bill up to 5 clients |
| plus | $43/workspace | $38.70/workspace | Bill up to 50 clients |
| premium | $70/workspace | $63/workspace | Unlimited billable clients |
| select | custom | — | Unlimited billable clients; 2 team-member accounts included; custom support and payment terms |
free tierno free tier; 30-day trial only
billingmonthly + annual (annual is an additional 10% lower than standard monthly rates)
hidden costsCurrent 90%-off-for-3-months prices are introductory, after which rates return to $23/$43/$70. Extra team members cost $11/user/month; Advanced Payments is $20/month; payroll starts at $40/month plus $6/person.
verified 2026-08-10 · source ↗
Vibecode FreshBooks
Kinda. The core of FreshBooks is buildable in a weekend with the prompt on this page, but there are real gaps: payment collection, bank imports. Read the honest list above before committing.
How much does FreshBooks cost?
FreshBooks costs about $23/month (Lite, checked 2026-07-30), which is $276 per year.
What do I lose by replacing FreshBooks?
Honestly: payment collection; bank imports; accounting reports; client portal; mobile apps; tax workflows; support. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to FreshBooks?
Yes: ERPNext (Accounting, invoices, expenses, time and payment requests in one ERP; simplicity did not survive the migration.) Dolibarr (Invoices, expenses, time, payments and a real ledger; it looks more back office than front page.) LedgerSMB (Serious accounting with invoices, expenses, projects and timecards; the interface assumes you came for the ledger.) All 4 curated free alternatives are at vibecodeit.com/freshbooks/alternatives. The prompt is for when you want it exactly your way.