Certificate Automator Tool for IEEE CS UTP
How I built a certificate automator for IEEE CS UTP: architecture & lessons learned
As a member of IEEE Computer Society UTP Student Branch, one of the things that bothered our team the most during event organization last year was manually preparing certificates. Someone had to spend hours filling each participant certificate one by one, the same tedious process, painfully slow and prone to mistakes.
So I built a certificate automator that handles bulk PDF generation, stores templates, imports participant data, and sends batch emails, all from a browser.
The constraint: everything had to run on free tiers. This is a student branch tool with no infrastructure budget, so every hosting decision had to be justified by what free plans could realistically support.
The result: what required an entire afternoon of manual work is now reduced to about 5-10 minutes of setup. Testing with 200 participants, certificate generation takes about 30 seconds.
How it works
The workflow mirrors what you’d do manually, just automated: upload a certificate template (PDF or image), position text fields like {{full_name}} and {{event_date}} in a visual drag-and-drop editor, import participants via CSV or Excel, preview a few sample certificates to catch mistakes, then generate the full batch and distribute via download or email.
The system validates templates server-side (extracting dimensions and embedded fonts), deduplicates participants by email, and enforces approval gates before generation and email sending. Once you approve, one click generates all certificates, merges them into a single PDF and ZIP, and optionally sends personalized emails with attachments.
The two-tier architecture
The core architectural decision was splitting the system into two tiers, and the reason comes down to a fundamental constraint: Cloudflare Workers can’t render PDFs.
Workers run in a sandboxed V8 environment with no native modules, no file system, and no access to libraries like PyMuPDF. But Workers are incredibly fast (5ms cold starts), globally distributed, and give you D1, R2, and Queues as native in-process bindings with no network hops.
So I split the system:
Tier 1: Cloudflare Worker (Hono + TypeScript) handles everything except PDF rendering: the REST API with 40+ endpoints, authentication via Cloudflare Access JWTs, the React SPA, job queuing, file storage (R2), the SQLite database (D1), rate limiting via Durable Objects, and email dispatch with dual-provider failover (Brevo primary, Resend fallback).
Tier 2: Python microservice (FastAPI + PyMuPDF) on Koyeb’s free tier handles only PDF rendering. It exposes four endpoints: template validation, thumbnail generation for previews, bulk certificate rendering, and PDF merging. It’s stateless, protected by an API key, and uploads directly to R2. This means the renderer can be redeployed, scaled, or swapped out without touching the rest of the system.
This split lets each tier do what it’s good at. The Worker handles coordination and state management at the edge; the renderer does the CPU-intensive PDF work on a traditional container.
The streaming challenge
When you generate 200 certificates, you can’t just make a synchronous HTTP call and wait. That would block for 30+ seconds and risk timeouts. The Worker has a 128 MB memory limit; the renderer has 512 MB on the free tier. Buffering everything would hit both limits.
The solution was NDJSON streaming (newline-delimited JSON). When you hit generate:
- The Worker immediately returns
202 Acceptedwith a job ID - It splits participants into chunks of 100 and sends each to a Cloudflare Queue
- The queue consumer (with
max_concurrency: 5) calls the Python renderer for each chunk - The renderer processes each certificate, uploads the PDF directly to R2, and emits a single JSON line with the result
- The Worker parses the stream incrementally, updating progress in D1 every 15 certificates
- The frontend polls every 3 seconds and shows a live progress bar
This way, neither side buffers the full batch. Each certificate streams through independently. If something fails, the queue retries that chunk up to 3 times with exponential backoff, and individual failures don’t block the rest of the batch.
The visual editor
The template editor is the most complex piece of the frontend, about 1,000 lines of React with react-konva for canvas rendering. It lets you drag and drop text fields onto your certificate design, with grid snapping, keyboard nudging (arrow keys, Shift for pixel precision), and pinch-to-zoom on touch devices. Fields auto-save with a 2-second debounce.
The tricky part is coordinate mapping. The editor uses an 800px-wide canvas for consistency across screen sizes, but PDFs use points (1 point = 1/72 inch). The renderer has to map canvas coordinates to PDF coordinates while preserving the template’s aspect ratio. The shared @cert/shared package contains a calcCanvasHeight() function that both the editor and renderer use to ensure coordinates match exactly.
Users can also upload custom fonts (TTF/OTF), which get stored in R2 and loaded into the browser via the FontFace API for accurate preview rendering.
What I’d do differently
The polling approach works but isn’t elegant. The Participants page manages five concurrent polling systems (job progress, participant status, campaign status, aggregate completion, and checkbox state). Server-Sent Events from a Durable Object would be cleaner, but the added complexity wasn’t justified for this scale.
Font management is per-template, meaning fonts get re-uploaded and re-transferred for every generation job. A shared font library with content-hash caching would eliminate redundant transfers.
The free-tier constraints are real. The 650-participant cap exists because of R2 storage limits and D1’s parameter limits on bulk operations. The renderer’s 0.1 vCPU means large batches process linearly. Profiling showed PyMuPDF’s insert_textbox() is the bottleneck. With a paid plan, both limits could be relaxed significantly.
What’s next
The goal is to form a small team of DART IEEE CS members to evolve this into a full event management tool, starting with automated participant ingestion via forms. The project will also serve as a reference for a future Software Architecture workshop, with plans to expand adoption to other IEEE UTP Student Branches.
If you have questions about any part of the implementation, feel free to reach out.