More Projects
59 in total
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.
Project Details
Creative Computing Society
2024
Society · Backend
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.
xlsxreads 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-libloads the event's PDF template, registers@pdf-lib/fontkitand 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
widthOfTextAtSizeand 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 throughqrcode.toDataURL, decoded from base64 and embedded as a PNG at 0.6 scale in the corner. - Verification route. The Mongoose model exposes a virtual
urlof/certificate/<encrypted-id>/download, andencryption.jswraps 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. AshowCertificateroute 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
copyPagesinto a singleallcertificates.pdffor 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.logis monkey-patched to prefix an ISO timestamp and append tologs/logs.txtas 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_MODEis set, and/healthcheckreports 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
Creative Computing Society
2024
Society · Backend