Certificate Generator
Completed
Society · Backend

Certificate Generator

Creative Computing Society · 2024

Bulk certificate pipeline: reads a participant spreadsheet, stamps names onto a PDF template with embedded Gotham fonts via pdf-lib, attaches a QR carrying a self-describing `id|name|reason|date` payload, and mails each one out. Runs clustered across CPUs behind a rate limiter. An encrypted-link verification store is modelled on the certificate document and was never wired up — the batch that shipped never wrote a record, and the decryption step in both verification routes is commented out.

Built with
Node.jsNode.js
ExpressExpress
pdf-libpdf-lib
MongoDBMongoDB
xlsxxlsx
DockerDocker
Project Details

STATUS
Completed
ORGANISATION

Creative Computing Society

YEAR

2024

TYPE

Society · Backend

TAGS
PDF
Automation
Events

Every society event ends with the same job: several hundred people who each need a PDF with their own name on it, mailed to their own address, and a way for a recruiter to check the thing is real six months later. Doing that by hand is a lost afternoon and a typo in someone's name. This turns a spreadsheet into sent mail.

Two jobs in one process

The awkward part of certificate tooling is that it is really two programs with opposite lifetimes. One is a batch job that runs for twenty minutes once per event and then never again. The other is a web service that has to answer a verification link two years later, when the event is forgotten and the person checking is a recruiter who will not try twice.

They are the same codebase here, and the environment decides which one boots. In production the app connects to Mongo and forks a worker per CPU to serve verification. In development it stays single-process and kicks off the Excel batch immediately, which is how you actually iterate on template coordinates - change a number, save, watch the next PDF land.

The pipeline

  • Spreadsheet in. xlsx reads the first sheet of the participant workbook and converts it to JSON. Each row carries a name, an email and the event they attended.
  • Stamping the template. pdf-lib loads the event's PDF template, registers @pdf-lib/fontkit and embeds Gotham Bold from a TTF on disk, so the certificate keeps the society's actual typeface rather than falling back to Helvetica.
  • Centred text without guesswork. Both the recipient name and the reason line are measured with widthOfTextAtSize and offset by half their own width from a fixed anchor point - so a long name and a short one both sit centred, which is the whole difference between generated and typeset.
  • A QR that means something. Each certificate gets a QR encoding id|name|reason|date, rendered through qrcode.toDataURL, decoded from base64 and embedded as a PNG at 0.6 scale in the corner.
  • Verification route. The Mongoose model exposes a virtual url of /certificate/<encrypted-id>/download, and encryption.js wraps the document id in AES-256-CTR with a 16-byte IV prefixed to the ciphertext - so the public link need not be a guessable Mongo ObjectId. A showCertificate route serves the found and not-found pages, and the found page builds its own download link by appending to the current URL, so one record serves both the human check and the file.
  • Mailed as it goes. Nodemailer sends the PDF as an attachment with a per-event thank-you template, with a 1,500 ms pause between rows so a few hundred sends do not trip the provider's rate limit. Every row is wrapped in its own try/catch, so one bad address costs one certificate rather than the run.
  • One merged archive. Every generated buffer is also collected and merged with copyPages into a single allcertificates.pdf for printing or archival.
  • Multiple templates checked in - participation and achievement variants for the base design, a 2024 refresh and a Saturnalia pair, six PDFs in all - selected per run, alongside eighteen weights of Gotham in OTF and TTF.

Running it as a service

The verification side has to stay up long after the batch job finishes, so the same process is a hardened Express server:

  • Clustered across CPUs. cluster.fork() per core, with an exit handler that respawns any worker that dies, so a bad request cannot take the verification endpoint down.
  • Rate limited at 100 requests per 15-minute window.
  • Tee'd logging. console.log is monkey-patched to prefix an ISO timestamp and append to logs/logs.txt as well as stdout, with the log directory served statically for quick inspection.
  • Maintenance switch and health check. A middleware short-circuits every route when MAINTINANCE_MODE is set, and /healthcheck reports both liveness and that flag.
  • Environment-split boot. In production the app connects to Mongo and forks the cluster; in development it runs single-process and kicks off the Excel batch immediately.
  • Docker and Compose for deployment: Node 20, one service, port 4876, restart: always, and the logs directory bind-mounted to the host so the file survives a container replacement.

The tradeoff in the clustering

Forking per core is the right call for the verification half and the wrong call for the batch half, and the code resolves that by only clustering in production. But the cluster leaks into one place it should not: the rate limiter is registered before the fork and keeps its counters in process memory, so the real ceiling is 100 requests per window per worker. On an eight-core box that is 800. For an endpoint whose entire job is serving a public link, that is a defensible miss - the alternative is a Redis store and an extra dependency for a service that gets a few hundred hits a year - but it is a miss, and the number in the config is not the number in effect.

Honest scope

The Saturnalia run is the copy on disk, and reading it back shows where a batch tool drifts from its own design. The decryption step in both verification routes is commented out and the id is validated as a raw ObjectId instead, so the encrypted-link scheme is defined on the model and disabled in the controller. The IV is generated once when the module loads rather than per call, which for CTR mode means every id encrypted by a given process shares a keystream. The batch assigns each certificate an eight-character base36 id and never writes the record to Mongo at all, so that run produced and mailed PDFs whose QR payload is self-describing text rather than a resolvable link. The maintenance flag is compared against a boolean when the environment hands it a string, so the switch never engages. And the issue date is a hardcoded string with the computed one commented out above it - correct for one event, wrong for the next.

None of that stopped several hundred certificates going out with the right names on them, which was the job. But it is a good illustration of the failure mode of event tooling: the parts that run once get tested by running, and the parts that matter later get tested by nobody.

Project Details

STATUS
Completed
ORGANISATION

Creative Computing Society

YEAR

2024

TYPE

Society · Backend

TAGS
PDF
Automation
Events