I run a one-person business and I do not use accounting software. For a long time, this was not a problem. Invoices arrived by email, I saved them and sent them to my accountant.
This changed when Polish B2B invoicing moved into a central government system. A purchase invoice can now appear in the system without appearing in my inbox. I needed a simple tool that would check for new documents twice a day, prepare readable PDF versions and send one notification.
This is how KSeF Watch started.
The idea was small. The project quickly showed that even a small application may require serious decisions about architecture, security and responsibility.
Design before code
I did not start by generating screens or writing the first endpoint. I started with a design document: architecture, data model, deployment, threats, platform limits and a decision log.
At the end, I added a list of assumptions that had not been clearly confirmed. There were forty.
Some were technical. Others concerned the product, the law or expected user behaviour. Many numbers looked like proper decisions: session lifetime, file retention, synchronisation frequency or attachment limits. Once collected in one place, some of them were clearly only reasonable guesses.
This is what separates AI-assisted engineering from simple vibecoding for me. The important question is not how much code the model writes. It is whether the project leaves only working software, or also decisions, assumptions and risks that another person can question.
A side project does not need to become a SaaS
Only after preparing the design did I research the market properly. This was the wrong order.
I found several products doing almost the same thing. Some were very cheap, others were free, and large accounting platforms had started adding a similar function to their standard plans.
The business conclusion was simple: there was little reason to build another small SaaS based only on this feature.
I built the application anyway. Only the purpose changed. The project would solve my own problem, test a specific way of working and provide a public example of code and architectural decisions.
I removed pricing plans, billing and growth targets. The cost and responsibility did not disappear.
The application still depends on an external government system. If the API changes, someone has to update the integration. If authentication stops working, someone has to find the reason. If a scheduled job fails, someone should notice before a user starts looking for a missing invoice.
Hosting, email, storage and monitoring may be cheap, but they are not completely free. The more important cost is responsibility. The tool handles credentials that provide access to financial documents. The fact that the user pays nothing does not make security less important.
Free for the user is not free for the author.
Why I chose Cloudflare
I am a big fan of Cloudflare. This is not an advertisement, and I do not receive any commission.
The platform offers a large amount of infrastructure at a good price: Workers, queues, D1, KV, R2, managed browser rendering and email services. For a small application, this makes it possible to run the complete system without building a complex cloud environment.
Low infrastructure cost does not mean there are no trade-offs. Part of the cost moves from operations to design and implementation.
KSeF Watch was designed for Cloudflare Workers from the beginning. It was never migrated from Node.js. The limits of the edge runtime had to be considered before the code was written.
During authentication, the external system provided an X.509 public
certificate, while WebCrypto expected a key in SPKI format. The
certificate could not be passed directly to
crypto.subtle.importKey(). It had to be parsed, the
key extracted and only then imported.
XML processing required similar care. The runtime did not provide
the browser-style DOMParser, and Node.js libraries
could not be selected without checking their dependencies. The XML
parser became a platform decision, not a small implementation
detail.
Email required a larger architectural choice. A normal SMTP client was not a natural option because the runtime did not provide the standard TCP environment expected by such libraries. Email had to use an HTTP API or a native platform service.
Execution limits also affected background processing. Downloading many invoices, generating PDFs and sending everything in one task could use too much CPU time and create too many external requests. The work had to be divided into smaller units and passed through queues.
Cloudflare was still a good choice for this project. It was necessary, however, to accept that cheaper infrastructure does not always mean simpler implementation.
PDF generation without a normal server
PDF generation sounds simple until the application runs without a local file system, an installed browser and unlimited CPU time.
I considered four approaches.
A managed browser can render HTML and CSS and print the result to PDF. It gives good control over layout and naturally handles fonts and national characters. It is slower, however, and depends on a platform service that may not be available in the same form during local development.
Building the PDF directly in JavaScript is fast and does not require an external renderer. In return, the application must handle text positions, tables, page breaks and embedded fonts. Standard PDF fonts do not include every national character, so Polish text may require a custom font.
A container with a traditional tool provides the most freedom, but it adds an image to maintain, heavier deployment and possible cold starts. For a small application, it removes part of the simplicity that made edge attractive.
An external PDF service is the fastest to implement, but it becomes another provider and another place where invoice data is processed.
I selected managed browser rendering and placed it behind a small interface. The rest of the application does not need to know how the document is produced. The renderer can be replaced later if necessary.
An important problem appeared only when I looked at the complete delivery process. The PDF had to be attached to an email, and the email service limited the full message to 5 MiB. Base64 encoding adds about one third to attachment size, so the real budget is lower.
My first estimates were also wrong. I expected about 200 KB per document. A simple visualisation without images was closer to 20–50 KB. One real measurement changed the design more than the earlier ideas about compression and splitting messages.
Generate one real document first. Then measure it.
The security promise you cannot make
The most difficult decision concerned credentials.
KSeF Watch works automatically. A background job must use the credential without the user being present. This leads to a simple conclusion: if the application can decrypt a secret, it must have access to a key that makes decryption possible.
A person who controls deployment can potentially change the code and make it reveal the secret. Encryption does not remove this fact.
It can protect data in the database, backups and logs. It can stop support staff or database administrators from reading it. It cannot make the secret unreadable to the application while still allowing an autonomous job to use it.
This is why the promise that nobody, including administrators, can access the credential is too strong for such a system.
Real zero-knowledge would require the user to be present for every decryption. This can work in an interactive application, but not in a system that runs scheduled jobs.
The honest promise is narrower. The credential can be encrypted with a key unavailable to operational staff and database administrators. It can be decrypted only by an isolated component, for a defined operation, and every use can be recorded.
This does not make access impossible. It makes access difficult, controlled and visible.
An administrator should not be able to open a database tool and read the secret. Extracting it should require a deliberate code change, a deployment and a trace in the review and audit process.
This is a weaker promise than “nobody can ever read it”.
It is also true and verifiable.
What remained from a small application
KSeF Watch solved a simple problem: it reports new invoices and delivers readable versions.
The most valuable result was not the feature itself.
The project showed that a cheap stack may require more careful design, a free tool still creates responsibility, and good security architecture does not depend on absolute promises.
Not every side project needs to become a SaaS. It may support learning, show how you work and solve a narrow problem.
It is still important to know where its real costs are.
The project is available publicly: github.com/50bitSolutions/ksefwatch