Disclosure: I'm the developer of VeloxFactory, the product this post is about. It's commercial, not open source. This post is about the engineering, not a sales pitch, but you should know who's talking.
It started with a label
My background is logistics IT. Warehouses, ERPs, label printers that have to respond while a forklift is waiting. In February 2026 I needed a simple thing: generate an article label as a PDF, on demand, from whatever system happened to be running at the time.
The templates already existed. They were .jrxml files from Jaspersoft Studio, because that's what a lot of companies have used for years. The obvious way to render them is the JasperReports Java library. But that means a JVM on the server and Java glue code, and none of the systems I was working with were Java.
So the first version was modest: a small Laravel API that takes a template name and some parameters and returns a PDF.
POST /api/v1/report-config/A5_KanBan/render
Authorization: Bearer <token>
Content-Type: application/json
{ "outputType": "base64", "parameters": { "P_ARTICLE_NUMBER": "4561287-154" } }
That request is still the core of the product today. Everything else grew around it.
Decision one: no JVM
Instead of wrapping the Java engine, I used JasperPHP, a pure PHP implementation of the .jrxml format on top of TCPDF. The trade-off was clear from the start:
- Win: the whole stack is PHP and a database. It runs on a cheap VM, on-premise, in Docker, and on small single-board machines on the shop floor.
-
Cost: a PHP implementation covers the common
.jrxmlfeatures, not everything JasperReports can do.
I still think that was the right call for the target audience. But "covers the common features" turned out to be a promise I had to keep actively. More on that below.
How it grew
Every module exists because a real use case asked for it:
-
A frontend. Uploading
.jrxmlfiles by API is fine for developers, not for the people who maintain templates. The upload now detects parameters, fields and image resources automatically and generates a preview thumbnail. - Live SQL data. Sending data with the request works when the caller already has it. Often it doesn't, so templates can read from MySQL, MariaDB, PostgreSQL or SQL Server at render time.
- History. In production, "did that label get printed?" is a daily question. Every render now stores request, response, PDF and thumbnail with a trace id, and can be re-printed or re-sent from the record.
- Printing. A PDF on screen doesn't help a warehouse. Renders can go straight into a print task that a Windows print service picks up.
- Scan2Print. Scan a barcode on a phone or handheld, get the label at the printer.
- Mailing. Some documents go to people, not printers: mail templates with placeholders, a queue, rate limits per mailer.
- Scheduling. Recurring reports by cron, with a preview of the next runs and a log for every run.
- An MCP server. More on that at the end.
What started as "just get me a label PDF" is now a suite with nine modules.
The numbers that made it real
Two production setups shaped most of the design:
- Labels at a trading company with its own logistics: over 4,000 labels per day from ERP data on SQL Server. A warehouse employee scans an article barcode, the scan mask sends one API call with the article number, VeloxFactory reads the article data live and sends the label to the print server. Typically about 800 ms from scan to label. The scan mask itself was built in a day, because all the hard parts were already in the API.
- Compliance documents at a supplier of home and lifestyle products: the company needed documents for the EU Packaging and Packaging Waste Regulation (PPWR). Up to 12 documents per article from four templates, for about 2,200 articles. The ERP integration needed no changes to VeloxFactory itself, which was the best validation of the API design I could have asked for.
Lesson 1: printing is a distributed systems problem
The first print service was a small .NET console app. It connected via WebSocket (Laravel Reverb) and printed whatever arrived. It worked in testing. In production it had three problems that only show up over days, not minutes:
- It only polled once, at startup. After that it relied on WebSocket events alone. Any event during a reconnect was lost until the next restart.
- It didn't answer the server's pings. Reverb drops idle connections after 30 seconds, so the service reconnected regularly, and every reconnect was a window for lost events.
- It swallowed errors when confirming a print. If printing worked but the confirmation call failed, the task stayed "pending" and got printed again after the next restart. Double labels are not a cosmetic bug in a warehouse.
Print Service 2.0 is a rewrite around those lessons:
- WebSocket for speed, plus a periodic safety poll for completeness.
- A local journal of printed jobs, so a job is never printed twice, even if the confirmation fails.
- It runs headless from the Windows Task Scheduler, and the same executable opens a UI for setup, monitoring and statistics.
The general rule I took from it: assume every message can be lost or delivered twice, and design so that neither matters.
Lesson 2: owning the render engine
Using JasperPHP as a dependency worked until real templates started to look slightly wrong. Not broken, just wrong enough that a customer notices. So in September 2026 I forked it and started fixing things properly. A few examples:
Geometry was cast to integers. A rectangle with height="0.8" became 0 and disappeared. On a 65 x 38 mm label, that's the separator line everyone expects to see. All coordinates are floats now.
Defaults didn't match JasperReports. Rectangles defaulted to transparent with no border, while JasperReports defaults to opaque, white, with a 1 pt pen. Colour priority was also reversed. Templates designed in Jaspersoft Studio looked different in the PDF than in the designer.
String concatenation in expressions. Text field expressions are written in Java syntax, and Java evaluates + from left to right with type switching:
1 + 2 + "a" // "3a"
"a" + 1 + 2 // "a12"
I replaced the old evaluation path for text fields with a small tokenizer and parser that follows exactly those rules. Anything it can't parse falls back to the old path and leaves a debug message, so existing reports don't break.
Page layout edge cases. Column headers on every page, <break> elements in group footers, lastPageFooter, and the summary band on its own page, with and without isSummaryWithPageHeaderAndFooter. Each one is tested against a reference report and compared with what JasperReports does.
The most useful outcome wasn't a single fix. It was a public page in the docs that lists exactly which bands, elements, styles and expressions the engine supports, and where it deviates. "Covers the common features" is now something you can check before you rely on it.
Lesson 3: API first pays off twice
Every function in the frontend is also available in the REST API. That felt like extra work at the beginning. It paid off twice:
- Integrations. The Scan2Print mask and the ERP integration for the compliance documents were both built against the public API without any special endpoints.
-
AI. When MCP came along, the MCP server became a thin layer over the existing API. It now exposes 89 tools, so an assistant can create a template config, render a document and send it to a printer through the same API everyone else uses. A separate skill file teaches the assistant the conventions, for example how to write a
.jrxmlthat the render engine handles well.
Nothing about the AI integration required a backdoor. If it's not in the API, the assistant can't do it either.
Lesson 4: security is part of the product
A report engine parses user-uploaded XML, connects to databases and sends mail. That's a lot of attack surface for a "small" tool. A structured security review in September led to a list of concrete changes, for example:
-
.jrxmlfiles are parsed without network access and with a size limit, which closes the door on XML external entity tricks. - Database and mailer usernames are only shown decrypted to admins.
- Broadcast channels for print tasks require the matching permission.
None of this is visible in a demo. All of it matters the first time a customer's IT department asks.
Where it is now
- Nine modules: templates, rendering, data, print and Scan2Print, Print Service, mailing, scheduling, history, AI and MCP.
- Laravel 12, PHP render engine (our JasperPHP fork) on TCPDF, no JVM, no Jaspersoft Server.
- In production for labels, bills of materials and compliance documents.
- Self-hosted or managed hosting in Germany.
If you want to see it without signing up, the try-out page renders a QR label, an article hangtag or a delivery note on a real instance and shows the API request in five languages: https://veloxfactory.dev/tryout
Docs: https://docs.veloxfactory.dev/books/veloxfactory
What I'd like to hear from you
If you've moved away from JasperReports Server or Crystal Reports, or decided against it: what made the decision, and what would you need to see before trusting a non-Java engine with your templates? I'm reading every comment.
Top comments (0)