How my PHP billing system evolved from Factur-X generation to a full autonomous business workflow
A few months ago, I wrote about how I integrated Factur-X EN16931 into a PHP billing system without a database, without an ERP and without a SaaS dependency.
At the time, the article focused mainly on the technical chain:
- generating the invoice PDF,
- building the EN16931 XML,
- embedding the XML into a PDF/A-3 document,
- producing a valid Factur-X file.
That was already a serious milestone.
But since then, the system has grown far beyond simple Factur-X generation.
It is now a complete autonomous billing workflow, designed to cover the full lifecycle of quotes, invoices, payments, deposits, expenses, fiscal logic, client types and future transmission to a Plateforme Agréée through API integration.
Still without a database.
Still without a SaaS dependency.
Still deployed on a standard PHP/Apache hosting environment.
Features
Global System
- Multi-user management with independent sessions and operation logging
- FR / EN bilingual interface
- Generate documents in French or English
- Automatic VAT legal notices for France, the European Union and international transactions
- Generate Factur-X invoices
- Automatically archive documents
- No subscription and no SaaS dependency
Quote Module
- Quote generation
- Online electronic signature
- Support for both private and business customers
- Manage service activities and goods sales
- Apply global discounts
- Deposit management with automatic calculation and direct integration into quotes
Invoice Module
- Automatic client data retrieval from archived quotes and invoices
- Invoice modification before issuance with deterministic recalculation of amounts, deposits and discounts
- Manage deposits and automatically recalculate the remaining balance due
- Convert a signed quote into a Factur-X invoice
Paid Invoice Module
- Track invoices awaiting payment
- Generate and send paid invoices only after payment has actually been received
- Revenue export in CSV format
- Archived invoice export in ZIP format
Deposit Module
- Deposit tracking with monthly closing and CSV export of paid deposits
Expense Module:
- Record and track business expenses
- Attach supporting documents to expenses
- Expense export in CSV format
All modules are connected through flat files, metadata and a shared backend logic.
There is still no database.
The system stores its data through structured folders, PDF files, JSON metadata and CSV journals.
This approach keeps the deployment simple and makes the data directly accessible on the client's own server.
The original goal
The first objective was simple:
Build a billing system that could generate compliant Factur-X invoices from a self-hosted PHP application.
Not by sending financial data to a third-party SaaS.
Not by migrating to a full ERP.
Not by depending on a remote invoicing platform.
The system had to remain autonomous, understandable, deployable and controllable.
That principle has not changed.
What changed is the scope.
The project is no longer only about generating a compliant file.
It is now about guiding the user through the correct business logic before the invoice is even generated.
Why no database?
This is a deliberate design choice.
For the type of businesses this system targets, a database is not always necessary.
The system needs to store:
- quotes,
- invoices,
- signed documents,
- paid invoices,
- metadata,
- counters,
- expenses,
- deposits,
- revenue journals.
A structured file system is enough for this.
Each document is archived in a predictable location.
Each quote or invoice has its own metadata file.
Each annual revenue journal is stored as CSV.
Each expense attachment is stored with the related expense entry.
The result is simple:
- no database migration
- no database backup complexity
- no vendor lock-in
- no hidden data layer
- no SaaS subscription
The client keeps direct control over the generated documents and the associated business records.
Quote generation
The first module handles quote creation.
The user fills in:
- issuer details
- client details
- client type
- operation type
- quote lines
- VAT data
- bank details
- optional insurance information
- language
- currency
The system calculates totals in real time:
- amount excluding VAT
- VAT
- amount including VAT
- global discount
- deposit
- balance due
The quote is generated as a PDF and archived on the server.
If an email address is provided, the client receives a secure link to consult and sign the quote online.
Online quote signature
Each generated quote receives a secure signature token.
The client can open the quote in a browser and sign it directly using:
- a mouse
- a touchscreen
- a stylus
The signature is saved as a PNG file and linked to the quote metadata.
The signed quote is then archived.
The system also records technical traceability data such as:
- signature timestamp
- document hash
- hashed IP information
- token status
This allows the quote lifecycle to remain clear without requiring an external signature platform.
Invoice generation
The system can generate invoices in two ways.
First, from a signed quote.
Second, directly from the invoice form.
In both cases, the backend validates the submitted data, calculates totals, generates the invoice number and archives the final document.
The invoice number uses a sequential yearly counter protected by file locking.
This prevents two invoices from receiving the same number during concurrent operations.
The invoice is generated as PDF and, when Factur-X is enabled, converted into a PDF/A-3 file with embedded EN16931 XML.
Factur-X EN16931
Factur-X remains one of the core parts of the system.
The generation chain is split into two phases.
First, PHP builds the invoice data and generates the structured XML.
Second, a Python script injects the XML into the PDF and produces the final PDF/A-3 document.
The final output is a human-readable PDF with a machine-readable XML file embedded inside it.
The system uses the Factur-X EN16931 Comfort profile.
The PDF is not just a visual document.
It becomes a structured electronic invoice ready for transmission.
Business logic before file generation
The biggest evolution since the first article is not technical.
It is functional.
The system now prevents many errors before they reach the backend.
The interface adapts dynamically depending on the context.
For example:
- a French professional client shows SIREN and SIRET fields
- an EU client focuses on the VAT number
- a non-EU client can use a generic company identifier
- a private individual does not see unnecessary business identification fields
- VAT can be blocked or forced depending on the client zone
- insurance fields only appear when the user chooses to display them
- country logic affects the available fiscal fields
This matters because compliance should not depend on the user knowing every rule.
The system should guide the user toward the correct case.
Client type logic
The system distinguishes between:
- business clients
- individual clients
This is now central to the architecture.
It affects:
- visible fields
- fiscal identifiers
- future e-reporting logic
- revenue exports
- invoice metadata
For French clients, SIREN and SIRET are relevant.
For EU clients, the VAT number is the key identifier.
For non-EU clients, a generic company ID is used when needed.
For individual clients, unnecessary company fields are hidden.
This avoids asking the user to fill irrelevant information.
VAT and legal notices
The system automatically determines the correct VAT treatment based on:
- issuer status
- client country
- client type
- VAT number
- operation type
- VAT zone
It can handle:
- French domestic VAT
- micro-enterprise VAT exemption
- intra-EU B2B reverse charge
- intra-EU supply of goods
- exports outside the EU
- services outside the EU
- operations without applicable VAT
The user never selects the legal notice manually.
The system determines the correct notice and inserts it automatically into both the invoice workflow and the Factur-X generation process.
Examples include:
- Article 293B CGI (French VAT exemption)
- Article 283-2 CGI (reverse charge mechanism)
- Article 262 ter I CGI (intra-community supply of goods)
- Article 262-I CGI (exports outside the European Union)
This is important.
A legal VAT notice is not cosmetic text.
It explains why VAT is applied, not applied or transferred to the buyer.
More importantly, it removes one of the most common invoicing mistakes: applying the wrong VAT treatment or legal justification.
The user does not need to know which legal notice applies.
The system determines it automatically.
Services and goods
The system now distinguishes between:
- services
- sale of goods
This matters because the applicable legal mentions can differ.
For example, an intra-EU B2B service is not treated the same way as an intra-EU supply of goods.
The interface exposes the choice, but the backend remains responsible for applying the correct logic.
The user selects the business context.
The system applies the rules.
Insurance fields
Professional insurance information can be included when needed.
The interface does not display those fields permanently.
The user chooses whether to show them.
If enabled, the following fields become available:
- insurer name
- policy number
- insurer address
- postal code
- city
- country
This keeps the interface clean while preserving the ability to include professional insurance details when required.
Deposits
Deposits are now tracked directly in the system.
When a quote receives a deposit, the amount can be registered and accumulated.
If the quote is later selected for invoicing, the existing deposit is automatically pre-filled in the invoice form.
The system then recalculates:
- total excluding VAT
- VAT
- total including VAT
- deposit already paid
- remaining balance
There is also a dedicated deposit tracking module.
It displays deposits by status:
- not invoiced
- invoice generated but pending payment
- paid
A monthly closing process allows paid deposits to be archived and exported.
Paid invoices
When an invoice is generated, the system also prepares a paid version.
This document is stored separately and remains pending until the user confirms payment.
The payment module scans pending invoices and displays only unpaid items.
When the user marks an invoice as paid, the system automatically:
- moves the paid invoice to the paid archive
- updates the metadata
- records the payment date
- appends the revenue journal
- sends the paid invoice to the client by email if configured
This creates a clear internal workflow:
Invoice generated.
Payment pending.
Payment confirmed.
Paid invoice archived.
Revenue recorded.
Revenue journal
Each confirmed payment is written into an annual CSV revenue journal.
The journal includes key information such as:
- payment date
- invoice number
- client identifier
- client name
- client type
- amount excluding VAT
- amount including VAT
- currency
- VAT zone
This is important for reporting and future e-reporting logic.
The system already stores the distinction between business and individual clients, which will matter for the French e-invoicing reform.
Business clients are part of the e-invoicing flow.
Individual clients are more likely to fall under e-reporting.
The data is already there.
Expense management
The system also includes an expense module.
The user can record professional expenses with:
- date
- supplier
- category
- invoice number
- amount
- VAT
- optional attachment
Attachments can be PDF, JPG or PNG.
They are stored on the server and can be viewed from the interface.
Expenses can be filtered by month and year.
They can also be exported as CSV.
This turns the system into more than an invoicing tool.
It becomes a small administrative workspace for both revenue and expense tracking.
Client lookup and auto-fill
The system scans archived metadata to retrieve existing client information.
The lookup can use:
- client name
- SIREN
- SIRET
- VAT number
- quote number
When a known client is detected, the form can be pre-filled automatically.
This avoids repeated manual entry and reduces errors.
When a quote is selected during invoice generation, the system can also reload the related quote lines and deposit information.
International country handling
Countries are centralized in a dedicated country list.
The interface can display country names while the backend works with ISO codes.
This allows the system to keep a clean separation between:
- user-friendly display
- backend fiscal logic
The country affects:
- VAT zone
- visible fields
- legal mentions
- e-invoicing or e-reporting preparation
- tax behavior
The system supports 249 countries.
Multi-user access
The system now supports multiple users.
Each user has individual credentials.
Each connection opens an independent session.
Access is protected by:
- secure PHP sessions
- HTTP only cookies
- secure cookies
- SameSite settings
- anti-brute force protection
- session regeneration after login
The goal is not to turn the system into a complex SaaS platform.
The goal is to make a self-hosted business tool usable by more than one authorized person.
Security model
Security is handled through several layers:
- authenticated internal interfaces
- no public access to administrative modules
- tokenized quote signature links
- 30-day expiration for signature links
- SHA256 document hashes
- hashed IP traceability
- protected PDF access
- no-store headers
- noindex on internal pages
- strict file path handling
- locked counters
- validated POST requests
The system does not expose raw storage paths to users.
PDFs can be served through secure internal endpoints when needed.
No SaaS dependency
One of the most important design decisions is still the absence of SaaS dependency.
The system does not require a third-party billing platform to generate documents.
It does not require a cloud ERP.
It does not require a remote invoice generator.
It can run on the client's own hosting environment.
This is useful for organizations that want to retain control over:
- financial documents
- client data
- invoice archives
- business workflows
- server deployment
- long-term access
The software does not force the client into a subscription model.
Why this matters with the French e-invoicing reform
The French e-invoicing reform does not only require businesses to generate invoices.
It changes how invoices are exchanged, transmitted and reported.
For B2B transactions, the invoice must go through a Platforme Agréée.
For B2C and international transactions, e-reporting becomes a separate concern.
This is where the system architecture becomes important.
The system already knows:
- whether the client is a business or an individual
- whether the client is in France, in the EU or outside the EU
- whether a VAT number exists
- what VAT zone applies
- what amounts were invoiced
- what amounts were paid
- what date the payment was recorded
This does not magically solve every e-reporting detail.
But it means the data model is already prepared.
Future PA integration
The next logical step is API transmission to a Plateforme Agréée.
The system is already positioned for this.
The workflow would be simple for the user:
Create invoice.
Generate Factur-X.
Click send.
The system sends the invoice to the PA through the client's own API credentials.
In this model, the client keeps their own PA account.
The software does not become a PA reseller.
The software only connects to the PA selected and configured by the client.
For example, with a B2Brouter integration, the client would provide:
- API Key
- Project ID
The system would then use those credentials to transmit documents through the client's own account.
The user experience remains simple.
The regulatory complexity is handled behind the button.
Stripe and product sales
One of the most difficult open questions today concerns payment providers such as Stripe.
Many SaaS products and digital businesses let Stripe generate invoices directly.
This creates a serious question under the French reform:
If Stripe generates the invoice, who produces the compliant electronic invoice?
If the invoice must be generated elsewhere, what happens to numbering, refunds, credit notes and reporting?
At this stage, the clearest approach for a small independent software vendor may be to separate payment from invoicing.
Stripe can be used as a payment processor only.
The official invoice should be generated by the billing system.
However, this creates technical and operational complexity:
- webhook handling
- internal invoice numbering
- payment reconciliation
- product delivery
- possible credit notes
- future PA transmission
For a small catalog of digital products, replacing automated purchase buttons with a contact-first workflow can sometimes be more reasonable.
Contact.
Invoice.
Bank transfer.
Delivery.
It is less automated, but much clearer from a compliance standpoint.
What changed since the first version
The first version of the system proved that Factur-X generation could be integrated into a PHP billing stack.
The current version goes much further.
It now includes:
- full quote lifecycle
- online quote signature
- direct invoicing
- invoice from signed quote
- Factur-X generation
- payment tracking
- paid invoice archiving
- deposit tracking
- monthly deposit closing
- revenue CSV exports
- expense management
- attachment archiving
- multi-user authentication
- international VAT logic
- services and goods distinction
- client type logic
- contextual fiscal fields
- insurance information
- real-time totals
- bilingual FR/EN interface
- dark and light mode
- PWA access
- future PA/API readiness
The system evolved from a document generator into a complete business workflow.
The most important lesson
The hard part was not generating a PDF.
The hard part was not even embedding XML into PDF/A-3.
The hard part was building the business logic around the invoice.
A billing system must understand context.
Who is the client?
Where is the client located?
Is the client a business or an individual?
Is the issuer subject to VAT?
Is the transaction a service or a sale of goods?
Is VAT applicable?
Is a legal mention required?
Was a deposit received?
Has the invoice been paid?
Should the transaction later be transmitted or reported?
This is where many simple invoice generators stop being useful.
They can generate a document.
But they do not guide the user through the business rules.
If you'd like to see the system in action, including several real-world workflows and demonstrations, you can explore it here:
https://palks-studio.com/en/invoicing-without-saas
Conclusion
This project started as a Factur-X integration.
It became a full autonomous billing system.
The architecture remains simple:
PHP.
Python for Factur-X injection.
Flat files.
PDF.
JSON metadata.
CSV journals.
No database.
No ERP.
No SaaS dependency.
But the business logic behind it is no longer simple.
It now handles quotes, invoices, signatures, payments, deposits, expenses, VAT zones, client types, fiscal identifiers, automatic legal notices, Factur-X generation and future PA transmission.
This is the difference between a tool that creates invoices and a system that understands the invoicing workflow.
The next step is clear:
connect the existing Factur-X workflow to a PA API, while keeping the same principle that guided the project from the beginning.
The client keeps control of their data.
The system remains autonomous.
The complexity stays behind the interface.
And the user simply creates the invoice and sends it.

Top comments (0)