<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adrian</title>
    <description>The latest articles on DEV Community by Adrian (@adrianani-com).</description>
    <link>https://dev.to/adrianani-com</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023742%2Fbf9a94b6-d3bb-400a-a906-fb97add41025.png</url>
      <title>DEV Community: Adrian</title>
      <link>https://dev.to/adrianani-com</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adrianani-com"/>
    <language>en</language>
    <item>
      <title>The blueprint: an HTML-to-PDF API on Postgres, Chromium, and not much else</title>
      <dc:creator>Adrian</dc:creator>
      <pubDate>Mon, 13 Jul 2026 09:55:24 +0000</pubDate>
      <link>https://dev.to/adrianani-com/the-blueprint-an-html-to-pdf-api-on-postgres-chromium-and-not-much-else-3bgp</link>
      <guid>https://dev.to/adrianani-com/the-blueprint-an-html-to-pdf-api-on-postgres-chromium-and-not-much-else-3bgp</guid>
      <description>&lt;p&gt;PDF generation in 2026 is glued together from Puppeteer, LiquidJS, and S3. What's not solved by the glue is the production shape around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documents range from 50 KB to hundreds of megabytes. Memory can't care.&lt;/li&gt;
&lt;li&gt;Traffic arrives in bursts (10,000 documents at once). Rendering is CPU-bound.&lt;/li&gt;
&lt;li&gt;Documents contain names, amounts, addresses. Sensitive by default.&lt;/li&gt;
&lt;li&gt;For a solo operator, whatever you build has to be runnable by one person.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;a href="https://docpenny.com" rel="noopener noreferrer"&gt;DocPenny&lt;/a&gt; around those four constraints in about twelve weeks. This is the blueprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;SvelteKit, Hono, pg-boss, PostgreSQL, Chromium, S3-compatible storage. Nothing else. No Redis, no Kafka, no Kubernetes. No connection pooler — I ran one and deleted it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pg-boss&lt;/strong&gt; is the load-bearing choice: a job queue on the Postgres you already have. One database for app data and async jobs. One thing to back up, one thing to monitor. The alternative (Redis + BullMQ) adds a second stateful service whose failure modes (persistence config, eviction policies, split-brain) are exactly the kind you debug at 3 AM. For a solo product there is no on-call rotation; there's just you. Every dependency you don't have is one that can't bring the service down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory-bounded streaming
&lt;/h2&gt;

&lt;p&gt;A PDF can be 50 KB or 400 MB. If any stage holds the whole document in memory, RAM scales with the largest thing any customer generates. So no stage does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printToPDF&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transferMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ReturnAsStream&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;MB&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="nc"&gt;Readable &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backpressure&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;aware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;S3&lt;/span&gt; &lt;span class="nx"&gt;multipart&lt;/span&gt; &lt;span class="nf"&gt;upload &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="nx"&gt;MB&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome writes to a stream as it renders. Node pulls 1 MB chunks. If S3 slows down, reads from Chrome slow down — nothing accumulates. S3 starts receiving parts before Chrome finishes rendering the last page. Memory per document is a fixed ~20 MB, regardless of output size.&lt;/p&gt;

&lt;p&gt;The default (&lt;code&gt;transferMode&lt;/code&gt; omitted) buffers the entire PDF in Chrome's memory, then again in Node's. Works in every demo. Falls over the first time a real customer sends something big.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is the load balancer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → API (Hono) → pg-boss queue (Postgres)
                            ↓
              Workers (N replicas, CDP pool)
                            ↓
        Chromium renders → stream → S3 multipart
                            ↓
              Webhook (HMAC-signed) → client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ingestion: ~5,900 documents/second. Rendering: ~63/second. That 90× gap is the design. The queue absorbs spikes; workers drain at their natural rate. Nothing drops. Never couple accept rate to render rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security from day one
&lt;/h2&gt;

&lt;p&gt;Templates are structure, not data. Sensitive values come as JSON payloads at render time, encrypted with AES-256-GCM before touching storage. Each payload gets a fresh HKDF-derived key from the master secret and a random salt — used in memory, never persisted. No key table to leak.&lt;/p&gt;

&lt;p&gt;Webhooks are HMAC-SHA256 signed with a constant-time verification helper. Artifacts are short-lived: previews purge in 24 hours, documents in 4 days. The best breach mitigation is data that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;One slice of a shared OVH server, ~€45/mo for 9 of 24 threads. k6 saturation test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API ingestion: 8.47 jobs/s ≈ 5,938 documents/s enqueued, 100% success&lt;/li&gt;
&lt;li&gt;Render throughput: ~63 PDFs/s (CDP pool is the bottleneck, by design)&lt;/li&gt;
&lt;li&gt;p(95) latency under saturation: 4.7 s&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build or buy?
&lt;/h2&gt;

&lt;p&gt;HTML-to-PDF is a crowded market with healthy competition. The pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buy when&lt;/strong&gt; document generation is not your product. If PDFs are an output of your actual business (invoices, reports), the twelve weeks I spent are twelve weeks of your roadmap rebuilding a solved problem, plus permanent ops burden (Chromium upgrades, storage lifecycle, security patching). Managed APIs charge fractions of a cent per document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build when&lt;/strong&gt; volumes make per-doc pricing material, data can't legally leave your infrastructure, or no vendor supports your rendering requirements. Then this blueprint is your starting point. The mistake most teams make: defaulting to the infrastructure they'd deploy at a company ten times their size. Teams don't fail by choosing the wrong queue. They fail by carrying five services when the problem needed two.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full article with the complete business side, unit economics, build-vs-buy framework, and a note on OSS: &lt;a href="https://adrianani.com/articles/html-to-pdf-api-blueprint" rel="noopener noreferrer"&gt;adrianani.com/articles/html-to-pdf-api-blueprint&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>postgres</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The hidden cost of "stepping up" (why it's a trap and what to do instead)</title>
      <dc:creator>Adrian</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:19:10 +0000</pubDate>
      <link>https://dev.to/adrianani-com/the-hidden-cost-of-stepping-up-why-its-a-trap-and-what-to-do-instead-h0i</link>
      <guid>https://dev.to/adrianani-com/the-hidden-cost-of-stepping-up-why-its-a-trap-and-what-to-do-instead-h0i</guid>
      <description>&lt;p&gt;Companies expect engineers to "step up" and take on architectural responsibility, without defining what that means, giving them time for it, or providing guidance. The engineers absorb the cost silently: sprint goals slip, tech debt piles up, burnout follows, and when things break there's no map to fix them.&lt;/p&gt;

&lt;p&gt;Deferring 2-3 sprints of foundation work to hit a deadline routinely costs months of development time later, once the system breaks and the team has no structure left to recover with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inherited mess
&lt;/h2&gt;

&lt;p&gt;At my last job before consultancy, we inherited a codebase from a contractor: an AI-generated application with no thought for DRY, security, or performance. It worked on the surface. Underneath: duplicated code with no component abstraction, circular dependencies, memory leaks, console errors, no documentation.&lt;/p&gt;

&lt;p&gt;We rewrote it. The stack was chosen for the business, not preference: Nuxt 4 with CSR (no SEO needed behind a login), reka-ui for accessible components, Tailwind for speed, TanStack Query for sane networking against a separate .NET API, and a &lt;code&gt;docs/&lt;/code&gt; folder that made the codebase navigable by humans and AI both.&lt;/p&gt;

&lt;p&gt;Result: 80% of what the client had planned for one year, delivered in four months. That took 120% capacity, which isn't sustainable long-term, but the structure is what made it possible to push hard without the codebase collapsing under the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Assessment.&lt;/strong&gt; Audit for circular dependencies, memory leaks, console errors, duplication. Quantify it: how many pages, how much duplication, how many dependencies. Frame it to product as cost (fragility, onboarding friction, delivery speed), not aesthetics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundation, 2-3 sprints of non-feature work.&lt;/strong&gt; Pick the stack from business constraints: rendering strategy from SEO needs, component library from accessibility requirements, state management from data complexity, styling from team size. Write the docs. Build the first features as examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling.&lt;/strong&gt; Onboard against the docs and examples. Deliver features on the clean foundation. Maintain it with continuous small investment, not big rewrites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnostic
&lt;/h2&gt;

&lt;p&gt;Recognize 3+ of these and your team is absorbing dysfunction instead of negotiating it.&lt;/p&gt;

&lt;p&gt;Code-level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Degrading quality: workarounds, deprecations, circular dependencies, memory leaks, console errors, duplication&lt;/li&gt;
&lt;li&gt;No documentation&lt;/li&gt;
&lt;li&gt;No tech stack strategy driven by business needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Process-level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent sprint delays, team running at 120%+ capacity&lt;/li&gt;
&lt;li&gt;Engineers afraid to voice opinions or ship to production&lt;/li&gt;
&lt;li&gt;No one owns the architecture or prioritizes tech debt&lt;/li&gt;
&lt;li&gt;Onboarding takes weeks because knowledge lives in people's heads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Organizational:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product doesn't see tech debt because the product still works, until it doesn't&lt;/li&gt;
&lt;li&gt;Engineers don't understand business priorities; product doesn't understand engineering constraints&lt;/li&gt;
&lt;li&gt;Every tradeoff conversation ends in frustration because there's no shared language for the tension&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real cost
&lt;/h2&gt;

&lt;p&gt;I've been on both sides: the team where "stepping up" was never defined, and the team where we built structure and delivered. The difference wasn't the engineers. It was the system around them.&lt;/p&gt;

&lt;p&gt;The hidden cost of "stepping up" isn't just burnout. It's the features that didn't ship, the onboarding that took weeks, the incidents that could've been prevented. It's invisible until you build the structure to see it.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>architecture</category>
      <category>career</category>
      <category>management</category>
    </item>
  </channel>
</rss>
