Bulk Sign Printing: Render Worker Architecture
Technical project walkthrough — a greenfield build I led to replace browser-based printing with a Playwright-powered render service.
Live demo — see the render worker in action
During the walkthrough, you can try it: visit the Benchmark at Garden Center showcase and click View Printable Output. That triggers the full flow — print-sign page, Inngest, render-worker, PDF download. No sign-in required.
Try it — Benchmark Showcase1. Problem Overview
Independent garden centers need to print plant signs in bulk. Staff paste wholesale plant lists into Plantfolio, AI transforms them into professional signage, and they print the entire collection at once — one PDF with hundreds of signs. Single-sign printing is supported too when a quick replacement is needed. Output must be print-ready for standard home and office printers: crisp, correctly sized, matching the on-screen preview. No special equipment; letter or A4 paper.
We started with CSS and browser-based printing. It was not working well enough. CSS print media queries do not support modern effects like filter: blur(), box-shadow, and other visual treatments that make signs look polished in the browser. You also cannot predict which browser the user will use, leading to inconsistent output.
The goal was to deliver high-resolution signs that look exactly like the in-browser preview, and to support batch printing — dozens or hundreds of signs at once, not one at a time. Saving time through bulk printing was a core requirement. This case study is about finding a better solution.
2. Role & Team Context
Founder, solo operation — full ownership from problem definition through implementation and deployment. The solution needed to be maintainable and self-contained, with clear handoff potential if the team grows. The render service is intentionally isolated so another team could own it independently; I designed the boundaries accordingly.
3. Challenges & Constraints
- Multiple template options, not full design canvas control — Users choose from predefined sign templates rather than a free-form design canvas. A tldraw-style canvas would require a complete rendering engine change; dynamic HTML templates are far easier to manage and fit the product scope.
- Serverless is not viable — Playwright requires a full browser, similar to a test environment. Hosting that on a serverless function would not make sense.
- Acceptable latency — Render time is longer than real-time, but bulk printing does not require instant response. The lag is acceptable for this use case.
4. Process & Decision Path
- CSS print (starting point) — Tried first. Insufficient: missing effects, browser inconsistency, and unpredictable output. Needed something better.
- tldraw / full design canvas — Evaluated and rejected. The product offers multiple template options, not full design canvas control. A canvas-based approach would require rewriting the rendering pipeline; per-sign dynamic logic (variable icons, description length, font size by content) is trivial in HTML.
- Playwright / headless browser — Chosen as the better solution. WYSIWYG output, same HTML/CSS as the preview, works with existing dynamic templates. Renders exactly what the browser would show.
- Isolated service — Express app on Railway. Clear ownership boundary; another team could maintain it.
- Inngest — Async orchestration so the dashboard stays responsive; no long-running request on the main app.
5. Solution Summary
An isolated render-worker service (Express + Playwright) loads the print-sign URL in headless Chromium. It screenshots each sign container, assembles a single PDF or a ZIP of individual PDFs, then uploads to blob storage. The client polls for job completion and downloads directly from the blob URL.
6. Technical Details
End-to-end flow
- User clicks bulk print in the dashboard.
- Sign-render API creates a job, stores initial state in Redis, and sends an Inngest event.
- Inngest function calls the render-worker HTTP endpoint.
- Render-worker navigates to the print-sign URL (includes query and canvas identifiers).
- The print-sign page fetches bulk action params from Redis to determine which plants to render.
- Page runs the plant query with those params and renders all signs into marked containers.
- Playwright screenshots each container, builds PDF or ZIP, uploads to blob storage.
- Inngest updates job status in Redis.
- Client polls the job status endpoint and downloads directly from the blob URL when complete.
Redis (Vercel KV) usage
Plantfolio uses Redis for two purposes in this flow:
- Bulk action queries: When a user does "Print selected" or "Select all in folio", a query ID is generated and the filter parameters are stored in Redis with a 30-day TTL. The print URL includes the query ID. When the print-sign page loads — including when Playwright navigates to it — it fetches the stored params to resolve which plants to render. This decouples the URL from the potentially large filter payload.
- Sign render jobs: Stores job status (pending, running, complete, error) and output URLs. TTL 1 hour. The client polls this for progress.
For smaller selections, plant IDs can be passed directly as a URL param instead of a query ID.
Dynamic per-sign logic
Each sign adapts to its content. Plants have different numbers of icon badges (e.g. deer-resistant, pollinator-friendly) — templates show 0–4 or 0–8 depending on layout. Descriptions vary in length; KV details use a character budget to fit available space. Font size adjusts by content length (e.g. longer titles get smaller type to prevent overflow). This per-sign logic is why HTML stays the source of truth: the same template renders each plant correctly without a fixed layout engine.
Outputs
- Single PDF: 1 or 2 signs per page, with Sharp for rotation and PDF compression.
- ZIP: One PDF per sign, named by canvas and plant. Built with archiver.
- Upload to Vercel Blob; URLs returned for download.
Flow diagram
Key components
- Render-worker — Express service, Playwright render logic
- Print-sign page — Batch sign page, resolves query from Redis
- Inngest function — Orchestrates the render flow
- KV operations — Job status and bulk action query storage
7. Results & Impact
- High-fidelity output — signs match the in-browser preview exactly. Customers can print hundreds of signs in one PDF; batch workflow is core to the product.
- Templates stay easy to modify — sign templates can be dynamically loaded and displayed in marketing pages without going out of date. The same HTML that renders in the dashboard renders in the worker.
- Latency trade-off accepted — render time is longer than real-time, but bulk printing does not require instant response. Users tolerate the wait for the quality gain.
- Service isolation — the render-worker can be deployed, scaled, and handed off independently. Clear ownership boundaries for future team growth.
8. Lessons Learned & Hindsight
- Trade-off: Render time vs instant response. For batch printing, longer render time is acceptable.
- HTML as source of truth: Multiple template options (not full design canvas control) simplifies maintenance. Per-sign logic — variable icon counts, description length, font size by content — would be far harder in a canvas model.
- Isolation pays off: The render-worker can be deployed, scaled, and owned independently.
- CSS and printing: Learned a lot about their limitations — missing effects, browser inconsistency, unpredictable output. The gap between screen and print drove the search for a better approach.
- Rendering techniques: Evaluated CSS print, tldraw/canvas, Playwright — each with pros and cons. Still a lot to learn; many companies keep rendering and conversion strategies proprietary.
- Playwright beyond testing: It has production use cases. Using it as a headless render engine for WYSIWYG output was the right fit here.
- Serverless vs dedicated service: Playwright needs a full browser — not viable in a serverless function. Understanding what belongs where shaped the isolated service design.
Discussion prompts
Topics I’d enjoy diving into: trade-offs between Playwright and alternatives (Puppeteer, jsPDF, server-side canvas), scaling the worker under load, or extending the system for new template types.