Web Applications & Automation

CertiBatch, Bulk Certificate Generation for Events

Built during my tenure as Public Relations Officer of the Australasian Institute of Mining and Metallurgy (AusIMM) Tarkwa Student Chapter. Our 11th Annual Conference needed over 500 personalised certificates, so I wrote a Python tool to produce them, then rebuilt that tool as a web app so the next PRO never has to write code at all.

Role
Architect, Designer & Developer (AI‑assisted)
Context
AusIMM Tarkwa Student Chapter, PRO
Scale
500+ certificates per event
Status
Live, in use
CertiBatch field placement screen, showing a certificate previewed with a real student's name and event

Live at bulk-cert-generator.vercel.app. Source on GitHub.

The problem

Every conference ends the same way. The talks finish, the photographs are taken, and someone has to produce a certificate for every person who attended. For the AusIMM Tarkwa Student Chapter's 11th Annual Conference, that was over 500 certificates, each carrying a different name, and all of them needed before students went home.

Doing that by hand is not a small job, it is an impossible one. Editing a design file 500 times introduces mistakes into exactly the artefact nobody should get wrong: a person's name on a document they will keep.

So I wrote a Python script. It read a spreadsheet, drew each name onto the certificate design, and wrote out the files. It worked, and the conference had its certificates.

To be clear about how it was built: the Python was written with AI coding tools rather than by hand. I can read and adapt code, but I am not a developer by training. That changed how fast the code appeared, not what had to be decided, and the deciding is what the rest of this page is about.

The part that actually needed solving

The script was the easy half. The hard half is that the script only works if the person holding it can write Python, and the chapter elects a new PRO every year.

Why a script was the wrong deliverable

A one-off script solves one event. The next PRO inherits a file they cannot read, for a design that has changed, with column headings that no longer match. In practice they do not inherit a tool at all, they inherit the same problem and a reason to feel bad about it.

So I rebuilt it as CertiBatch: the same job, delivered as something an organiser can use without knowing what a script is.

What it does

Four steps, and nothing in between that needs explaining.

  • Upload data. A CSV or Excel file, one row per certificate. Column headings are matched loosely, so a heading typed Full Name, full name or FULL_NAME all resolves to the same field, and the spreadsheet is shown back as a preview so mistakes surface before 500 files are made.
  • Upload template. Any PNG or JPG design, or one of the built-in certificate layouts.
  • Place fields. Drag each column onto the artwork, set the typeface, size and colour, and preview it with a real record so you see the finished certificate rather than a placeholder.
  • Generate. Every certificate is rendered at print resolution and packaged as a ZIP, each file named after the person on it.
CertiBatch data upload, showing the spreadsheet preview in a cell grid
Uploaded data shown back before anything is generated
CertiBatch template gallery with ready-made certificate designs
Built-in designs, or bring your own artwork
CertiBatch field placement screen with draggable data fields on the certificate
Placing fields once, for the whole batch

The engineering problem underneath

The interesting constraint is that the browser and the server have to agree about a certificate neither of them can see at the same size.

The editor shows the design scaled down to fit a laptop screen. The server renders it at full print resolution, often several thousand pixels across. If field positions were stored as screen pixels, every certificate would print with the text in the wrong place.

So positions are stored as fractions of the artwork, from 0 to 1, and never as pixels. A name placed halfway across and just under halfway down sits at the same point on a 900-pixel preview and a 3500-pixel print file. The same principle governs type size, which is stored as a fraction of the artwork's height. The eight bundled typefaces are loaded twice, once as web fonts for the canvas and once as font files for the renderer, so that the face you place with is the face that prints.

The second constraint is time. Five hundred certificates take minutes to render, and every proxy between a browser and a server will drop a request long before that. Generation therefore does not happen inside the request: submitting a batch returns immediately, the work runs on a background worker, and the browser polls for progress and then downloads. That boundary is a single interface, so the thread pool behind it can become a proper job queue without touching a route.

Designing for people, not for me

The app is aimed at an organiser the week before an event, so several decisions went against what would have been easier to build.

  • Every step is reachable from the start. Nothing is locked behind an upload. Each screen is a real page that renders its full layout with nothing loaded and explains inline what it still needs, because being able to look around before committing is how people decide whether to trust a tool.
  • No controls that do nothing. Anything shown is either wired up or clearly marked as unavailable.
  • Errors are written for the person reading them, who is an event organiser and not a developer, so they say what to do rather than what failed.
  • The owner can add certificate designs from inside the app, which commits them for the whole site. That was the last piece of the original problem: the tool cannot depend on its author staying available.

Privacy as an architectural constraint

A list of students is a list of real people who did not choose to be in my database, so the design goal was that there should be nothing to leak.

There are no accounts and no user database. A job is a random identifier and a temporary folder, with nothing recorded that ties a name to a person or a session. Everything auto-deletes: thirty minutes after the ZIP is downloaded, or within an hour if it never is. The grace window exists so a dropped download can be retried rather than forcing a re-run of the whole batch.

The container that runs it mounts job storage in memory rather than on disk, so the retention promise is enforced by the platform and not only by code.

Tools & methods

Python FastAPI Pillow pandas React TypeScript Konva Tailwind CSS Docker Background job processing Normalised coordinate systems AI-assisted development

Where it stands

CertiBatch is live and in use, with the four-step flow, a gallery of ready-made designs, background generation and ZIP delivery all working end to end. It is covered by an automated test suite that includes a full 500-row run, because the failure I care about is the one that appears only at scale, the night before an event.

What it replaced was a script that solved one conference. What it is now is a tool the chapter keeps.

Try it at bulk-cert-generator.vercel.app, sample data and designs are built in, so it runs without uploading anything.

Running an event that needs certificates, or have a manual process worth automating?

Get in touch