skip to content
Kiran S Baliga
← Back to selected work
Pencil · Engineering case study

A more dependable foundation for AI-powered advertising.

At Pencil, I worked on the infrastructure behind the creative workflow: how tenant data is accessed, how permissions compose, and how an ad report becomes a PDF.

Associate Software Engineer KeyValue Software Systems · Client engagement: Pencil
Sep 2023–Feb 2026 trypencil.com ↗

Node.js · Python · Google Cloud SQL · Custom ORM · Puppeteer · Docker

The context

Pencil is a B2B AI ad platform for agencies working across Google, Meta, TikTok and LinkedIn. Underneath those workflows, data access and document exports had to remain predictable as tenant-specific requirements grew.

The work wasn’t a wholesale rewrite. It was a series of changes to make the existing platform faster and safer to evolve: fewer expensive joins, consistent tenant boundaries, flexible custom fields and a rendering pipeline we could control.

Architecture 01

Put tenant boundaries in the data layer

I built a custom ORM / data-access layer across Node.js and Python, with automatic tenant scoping and soft-delete enforcement. These rules became infrastructure, rather than conditions every caller had to remember.

We reduced costly joins around real read workloads. Metadata-driven custom fields allowed the model to vary by tenant, while fine-grained RBAC supported composable AND / OR permissions instead of a growing set of one-off checks.

Cloud SQL migrations were rolled out incrementally. I coordinated the migration and downtime with the lead, using tenant-level feature flags to control the transition rather than switching every customer at once.

Result: 30% faster API responses. The lesson was to optimize for the queries the product actually makes, not an idealized model.

Architecture 02

Move document rendering out of the browser

Exporting an ad report was originally tied to the client session: React rendered the page, a client-side library walked the DOM, and the browser generated a PDF on the fly. That worked for a single user looking at a simple table. It didn’t work for high-resolution proofs, large datasets, or scheduled batch exports.

I rebuilt the export pipeline as a standalone service running headless Puppeteer in a containerized worker pool. Templates were isolated from the web app, and assets were pre-warmed so exports could scale independently of the interactive UI.

BEFORE Browser UI → react-to-pdf → Download
AFTER
Export request → Sandboxed Puppeteer worker → PDF Preloaded fonts + cache + print CSS

What changed

65%

Reduction in PDF rendering processing time.

~1.5 sec

Click-to-PDF download time.

30–40%

Reduction in end-to-end export pipeline latency — a separate measure from rendering processing time.

30%

Faster API responses with query-level optimization and automated tenant scoping.

What I’d carry into the next system

  • Design around real reads: A cleaner abstraction is only useful if it serves the workloads people depend on.
  • Make the safe path the default: Tenant scoping and deletion rules should be embedded in the infrastructure, not scattered across application code.
  • Own the rendering environment: A document is an output with its own requirements; it shouldn’t be an accidental screenshot of the UI.
  • Modernize incrementally: Migrations and tenant-level flags let us improve the system without treating every change as a big-bang replacement.

Working through a similar engineering problem? Let’s talk about it.