<?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: Lucien Chemaly</title>
    <description>The latest articles on DEV Community by Lucien Chemaly (@luciench).</description>
    <link>https://dev.to/luciench</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3497693%2F8f1c42e1-ea56-4bec-a159-e3709e8e8155.png</url>
      <title>DEV Community: Lucien Chemaly</title>
      <link>https://dev.to/luciench</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luciench"/>
    <language>en</language>
    <item>
      <title>HTML to PDF API: Convert Web Content to PDF Programmatically with Foxit</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Thu, 16 Apr 2026 17:04:49 +0000</pubDate>
      <link>https://dev.to/luciench/html-to-pdf-api-convert-web-content-to-pdf-programmatically-with-foxit-1df0</link>
      <guid>https://dev.to/luciench/html-to-pdf-api-convert-web-content-to-pdf-programmatically-with-foxit-1df0</guid>
      <description>&lt;p&gt;Your &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; setup works fine at low volume. You launch a Chrome process, load the page, call &lt;code&gt;page.pdf()&lt;/code&gt;, and write the bytes to disk. Then your invoice generation hits 500 documents per night, your report export feature goes live across three time zones simultaneously, and the wheels come off. Chrome processes time out waiting for JavaScript hydration. Memory climbs until your container OOMs. The font that renders correctly on your MacBook looks wrong on the Linux build server. You spend a Friday afternoon tuning &lt;a href="https://pptr.dev/api/puppeteer.puppeteerlifecycleevent" rel="noopener noreferrer"&gt;&lt;code&gt;networkidle2&lt;/code&gt;&lt;/a&gt; timeouts per template instead of shipping features.&lt;/p&gt;

&lt;p&gt;That failure mode comes from treating a rendering engine as a conversion service. &lt;a href="https://developer.chrome.com/docs/chromium/new-headless" rel="noopener noreferrer"&gt;Headless Chrome&lt;/a&gt; is a browser, and running it at production document volume means operating a browser fleet: process pooling, memory isolation, crash recovery, rendering consistency across OS environments. All of that infrastructure overhead comes directly out of engineering time.&lt;/p&gt;

&lt;p&gt;A managed REST API sidesteps that entirely. You POST your HTML (or a URL), the service renders the PDF, and you download the result. The rendering infrastructure becomes the API provider's problem. This guide covers how to build that conversion pipeline end-to-end using &lt;a href="https://developer-api.foxit.com/" rel="noopener noreferrer"&gt;Foxit PDF Services API&lt;/a&gt;, from authentication through batch processing and production error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Production Problem with Headless Browser PDF Conversion
&lt;/h2&gt;

&lt;p&gt;A standard &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; setup looks like this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkidle2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;printBackground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At five documents a day, this is fine. At five hundred concurrent, each &lt;code&gt;puppeteer.launch()&lt;/code&gt; spins up a full Chromium process, roughly 100-200MB RSS on Linux. In a container with 2GB of memory and 20 concurrent requests, you're at the memory ceiling before accounting for the Node.js process or any other application memory.&lt;/p&gt;

&lt;p&gt;The standard fix is a Chrome process pool (libraries like &lt;a href="https://github.com/thomasdondorf/puppeteer-cluster" rel="noopener noreferrer"&gt;&lt;code&gt;puppeteer-cluster&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/coopernurse/node-pool" rel="noopener noreferrer"&gt;&lt;code&gt;generic-pool&lt;/code&gt;&lt;/a&gt;). Now you're managing pool size tuning, handling pool exhaustion under burst traffic, and writing cleanup logic for crashed Chrome instances. You've added significant operational complexity to what started as a one-liner.&lt;/p&gt;

&lt;p&gt;Font rendering is its own category of pain. Chrome on macOS uses CoreText. Chrome on Linux uses &lt;a href="https://freetype.org/" rel="noopener noreferrer"&gt;FreeType&lt;/a&gt; with &lt;a href="https://www.freedesktop.org/wiki/Software/fontconfig/" rel="noopener noreferrer"&gt;fontconfig&lt;/a&gt;. The same CSS &lt;code&gt;font-family: 'Inter'&lt;/code&gt; declaration produces visibly different output depending on whether Inter is installed as a system font or loaded via a &lt;code&gt;@font-face&lt;/code&gt; declaration, and whether the fallback stack resolves differently across environments. Teams that ship invoice PDFs to customers discover this in production.&lt;/p&gt;

&lt;p&gt;JavaScript execution adds another dimension. If your page renders a data table via a React component that fetches data on mount, &lt;code&gt;networkidle2&lt;/code&gt; is an unreliable wait condition. Network activity can go idle before the DOM finishes updating. You end up tuning &lt;a href="https://pptr.dev/api/puppeteer.page.waitforselector" rel="noopener noreferrer"&gt;&lt;code&gt;waitForSelector&lt;/code&gt;&lt;/a&gt; or adding arbitrary timeouts per template, and those timeouts become technical debt that breaks when the page changes.&lt;/p&gt;

&lt;p&gt;A managed REST API with consistent rendering environments and no infrastructure to maintain solves these problems at the architectural level.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Cloud HTML-to-PDF APIs Handle Rendering
&lt;/h2&gt;

&lt;p&gt;Cloud conversion APIs typically accept input in two modes: URL mode and file upload mode.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;URL mode&lt;/strong&gt;, you pass a public URL. The API fetches the page, renders it, and returns a PDF. This works when your page is publicly accessible and all assets (fonts, images, stylesheets) load from the same domain or CDN. The tradeoff is that the API's rendering environment must reach your server, which creates a dependency on network reachability and your server's response time. If you're generating PDFs from an internal dashboard behind a VPN, URL mode doesn't work without additional networking.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;file upload mode&lt;/strong&gt;, you construct the complete HTML file (with inlined CSS and assets where needed) and upload it to the API. The service processes the file and returns a PDF. This eliminates the external asset dependency and makes your conversion more deterministic. The same HTML file always produces the same PDF, regardless of what's deployed on your web server at the time.&lt;/p&gt;

&lt;p&gt;Beyond input mode, rendering fidelity depends on several factors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media" rel="noopener noreferrer"&gt;&lt;code&gt;@media print&lt;/code&gt;&lt;/a&gt; rules&lt;/strong&gt; control what renders into the PDF. Navigation bars, sidebars, and hover states should be hidden via print stylesheets so they don't appear in the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font loading strategy&lt;/strong&gt; determines rendering consistency. Relying on system fonts produces different output across environments. Embedding fonts via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face" rel="noopener noreferrer"&gt;&lt;code&gt;@font-face&lt;/code&gt;&lt;/a&gt; with a CDN URL or base64-inlined data guarantees consistent rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Page layout properties&lt;/strong&gt; (paper size, margins, orientation) can be controlled through CSS &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@page" rel="noopener noreferrer"&gt;&lt;code&gt;@page&lt;/code&gt;&lt;/a&gt; rules embedded in the HTML itself. This keeps layout configuration in the document rather than in API parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript execution&lt;/strong&gt; matters for pages that render content dynamically. Some APIs wait for the page to stabilize before capturing; others capture immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These factors are the same ones you'd manage with &lt;a href="https://pptr.dev/api/puppeteer.pdfoptions" rel="noopener noreferrer"&gt;Puppeteer's &lt;code&gt;page.pdf()&lt;/code&gt; options&lt;/a&gt;, but with a cloud API you handle them through your HTML and CSS rather than through in-process code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Foxit PDF Services API: Authentication and First Conversion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Foxit PDF Services API&lt;/a&gt; is a &lt;a href="https://developer-api.foxit.com/developer-blogs/use-cases-workflow-examples/automated-document-pipelines/introducing-pdf-apis-from-foxit/" rel="noopener noreferrer"&gt;cloud-hosted REST API&lt;/a&gt; built on Foxit's proprietary PDF engine, backed by over 20 years of PDF technology development. Create an account at &lt;a href="https://app.developer-api.foxit.com/pricing" rel="noopener noreferrer"&gt;the Foxit Developer Portal&lt;/a&gt; (the Developer plan is free, includes 500 credits/year, and requires no credit card). Generate your API credentials (&lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt;) from the Developer Dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the Async Workflow
&lt;/h3&gt;

&lt;p&gt;Foxit PDF Services uses an &lt;strong&gt;asynchronous task-based workflow&lt;/strong&gt;. Every operation follows the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Submit the job (upload a file, or POST a URL)&lt;/li&gt;
&lt;li&gt;Receive a &lt;code&gt;taskId&lt;/code&gt; in the response&lt;/li&gt;
&lt;li&gt;Poll the task status until it completes or fails&lt;/li&gt;
&lt;li&gt;Download the result using the &lt;code&gt;resultDocumentId&lt;/code&gt; from the completed task&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This design handles long-running operations cleanly. A complex HTML page might take several seconds to render, and the async pattern means your client never blocks on a single HTTP request waiting for rendering to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  URL-to-PDF Conversion
&lt;/h3&gt;

&lt;p&gt;For pages that are publicly accessible, URL-to-PDF is the simplest path. You POST the URL directly and the API fetches, renders, and converts it. The complete workflow in Python uses the &lt;a href="https://docs.python-requests.org/" rel="noopener noreferrer"&gt;&lt;code&gt;requests&lt;/code&gt;&lt;/a&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sleep&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_API_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# e.g., https://na1.fusion.foxit.com
&lt;/span&gt;&lt;span class="n"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_url_to_pdf_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Submit a URL for PDF conversion. Returns a taskId.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/create/pdf-from-url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Poll until the task completes or fails. Returns the task status object.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;download_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Download the resulting PDF by its document ID.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/download&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Full workflow: URL to PDF
&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_url_to_pdf_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/invoice/1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;download_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice_1042.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF generated successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three reusable functions map to the async workflow: &lt;code&gt;create_url_to_pdf_task()&lt;/code&gt; submits a public URL and returns a &lt;code&gt;taskId&lt;/code&gt;, &lt;code&gt;poll_task()&lt;/code&gt; checks task status in a loop until it reaches &lt;code&gt;COMPLETED&lt;/code&gt; or &lt;code&gt;FAILED&lt;/code&gt;, and &lt;code&gt;download_document()&lt;/code&gt; streams the resulting PDF to disk. The final three lines wire them together into the complete conversion pipeline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before running:&lt;/strong&gt; Set your &lt;code&gt;FOXIT_API_HOST&lt;/code&gt;, &lt;code&gt;FOXIT_CLIENT_ID&lt;/code&gt;, and &lt;code&gt;FOXIT_CLIENT_SECRET&lt;/code&gt; environment variables with the values from your &lt;a href="https://app.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Foxit Developer Dashboard&lt;/a&gt;. Never commit credentials to source control; use environment variables or a secrets manager.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  HTML File-to-PDF Conversion
&lt;/h3&gt;

&lt;p&gt;When your content isn't publicly accessible (internal dashboards, dynamically generated reports), you can upload an HTML file directly. This follows the same 4-step async pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Upload a file to Foxit. Returns a documentId.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_html_to_pdf_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create an HTML-to-PDF conversion task. Returns a taskId.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;AUTH_HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/create/pdf-from-html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Full workflow: HTML file to PDF
&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_html_to_pdf_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;download_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;report.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTML converted to PDF successfully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You first upload a local &lt;code&gt;.html&lt;/code&gt; file via &lt;code&gt;upload_document()&lt;/code&gt;, which returns a &lt;code&gt;documentId&lt;/code&gt; referencing the uploaded file on Foxit's servers. Then &lt;code&gt;create_html_to_pdf_task()&lt;/code&gt; submits that &lt;code&gt;documentId&lt;/code&gt; for conversion. The rest of the workflow is identical: poll for completion, then download the result.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Replace &lt;code&gt;"report.html"&lt;/code&gt; with the path to your own HTML file. This code reuses the &lt;code&gt;poll_task()&lt;/code&gt; and &lt;code&gt;download_document()&lt;/code&gt; functions from the URL-to-PDF example above, so make sure both are defined in the same script.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;URL-to-PDF skips the upload step because you POST the URL directly. HTML file conversion requires uploading the &lt;code&gt;.html&lt;/code&gt; file first via the &lt;code&gt;/documents/upload&lt;/code&gt; endpoint. Both use the same poll-and-download pattern after task creation.&lt;/p&gt;

&lt;p&gt;Refer to the &lt;a href="https://docs.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Foxit API documentation&lt;/a&gt; and the &lt;a href="https://developer-api.foxit.com/" rel="noopener noreferrer"&gt;Postman workspace&lt;/a&gt; for the complete parameter reference, including any additional rendering options supported by these endpoints. The &lt;a href="https://github.com/foxitsoftware/developerapidemos" rel="noopener noreferrer"&gt;GitHub demo repository&lt;/a&gt; contains working examples in Python, Node.js, and PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling CSS and JavaScript Rendering in HTML-to-PDF Conversion
&lt;/h2&gt;

&lt;p&gt;Regardless of which API you use for HTML-to-PDF conversion, output quality depends on how well you prepare the HTML. The rendering parameters live in your document, not in API request fields.&lt;/p&gt;

&lt;p&gt;The most common rendering problem between "looks right in a browser" and "looks wrong in a PDF" is the CSS media type. By default, browsers render with &lt;code&gt;screen&lt;/code&gt; styles, which means your navigation bar, sidebar, and hover states all appear in the output. For PDF output, you want your &lt;code&gt;@media print&lt;/code&gt; rules to take over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;nav&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nc"&gt;.sidebar&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="nc"&gt;.no-print&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;11pt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Arial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.invoice-table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;page-break-inside&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;avoid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.page-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;page-break-before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;@page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20mm&lt;/span&gt; &lt;span class="m"&gt;15mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stylesheet hides non-essential UI elements (navigation, sidebars) when printing, sets a clean body font, and uses &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-inside" rel="noopener noreferrer"&gt;&lt;code&gt;page-break-inside: avoid&lt;/code&gt;&lt;/a&gt; to prevent the renderer from splitting a table row across pages. The nested &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@page" rel="noopener noreferrer"&gt;&lt;code&gt;@page&lt;/code&gt;&lt;/a&gt; rule sets the paper size and margins at the CSS level, so layout configuration stays in the document rather than in API parameters.&lt;/p&gt;

&lt;p&gt;For font rendering consistency, don't rely on system fonts. Include a &lt;code&gt;@font-face&lt;/code&gt; declaration in your HTML that loads from a CDN, or inline the font as base64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ.woff2")&lt;/span&gt;
      &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;"woff2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This embeds the &lt;a href="https://fonts.google.com/specimen/Inter" rel="noopener noreferrer"&gt;Inter&lt;/a&gt; font directly in the HTML using a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face" rel="noopener noreferrer"&gt;&lt;code&gt;@font-face&lt;/code&gt;&lt;/a&gt; declaration pointing to Google Fonts. Inter renders in the PDF regardless of what fonts are installed in the API's container environment. The tradeoff is latency: the rendering engine fetches the font file during conversion. If you're running high-volume batch jobs, consider inlining the font as a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data" rel="noopener noreferrer"&gt;base64 data URI&lt;/a&gt; to eliminate that network round trip.&lt;/p&gt;

&lt;p&gt;For JavaScript-heavy pages, make sure the content has fully rendered before the API captures it. If you're using the URL-to-PDF endpoint, the API fetches and renders the live page, so your page's JavaScript will execute. For the HTML file upload path, keep your HTML self-contained with all data already rendered in the markup, rather than relying on client-side JavaScript to populate it after load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batch HTML-to-PDF Conversion at Scale
&lt;/h2&gt;

&lt;p&gt;Sequential conversion is the naive starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;html_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_html_to_pdf_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;download_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each invoice is processed one at a time, uploading, converting, polling, and downloading before moving to the next. Each iteration blocks on the poll loop before starting the next conversion. At a few seconds per document (upload, render, poll, download), 500 invoices could take over 30 minutes.&lt;/p&gt;

&lt;p&gt;Concurrent dispatch with a semaphore to cap parallelism fixes that. Check your plan's rate limits before setting the semaphore ceiling in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_API_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FOXIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;MAX_CONCURRENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="c1"&gt;# Adjust based on your plan's rate limits
&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;html_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="c1"&gt;# Step 1: Upload the HTML file
&lt;/span&gt;            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                    &lt;span class="n"&gt;upload_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;upload_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="c1"&gt;# Step 2: Create the conversion task
&lt;/span&gt;            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/create/pdf-from-html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="n"&gt;task_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="c1"&gt;# Step 3: Poll for completion
&lt;/span&gt;            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;result_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                        &lt;span class="k"&gt;break&lt;/span&gt;
                    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Step 4: Download the result
&lt;/span&gt;            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result_doc_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/download&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;pdf_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error converting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;invoice_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;batch_convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_CONCURRENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;connector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TCPConnector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MAX_CONCURRENT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;convert_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;succeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;succeeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;# Usage
&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inv_1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates/invoice_1042.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inv_1043&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates/invoice_1043.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;# ... up to thousands of entries
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;batch_convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Converted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;succeeded&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; PDFs. Failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.python.org/3/library/asyncio.html" rel="noopener noreferrer"&gt;&lt;code&gt;asyncio&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.aiohttp.org/en/stable/" rel="noopener noreferrer"&gt;&lt;code&gt;aiohttp&lt;/code&gt;&lt;/a&gt; let you process multiple conversions concurrently. The &lt;code&gt;convert_one()&lt;/code&gt; function runs the full 4-step workflow (upload, create task, poll, download) for a single invoice, while &lt;code&gt;batch_convert()&lt;/code&gt; dispatches all invoices in parallel, capped by a semaphore. Results are collected via &lt;code&gt;asyncio.gather()&lt;/code&gt; and split into succeeded and failed lists.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before running:&lt;/strong&gt; Set &lt;code&gt;FOXIT_API_HOST&lt;/code&gt;, &lt;code&gt;FOXIT_CLIENT_ID&lt;/code&gt;, and &lt;code&gt;FOXIT_CLIENT_SECRET&lt;/code&gt; as environment variables with your credentials from the &lt;a href="https://app.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Developer Dashboard&lt;/a&gt;. Adjust &lt;code&gt;MAX_CONCURRENT&lt;/code&gt; based on your &lt;a href="https://app.developer-api.foxit.com/pricing" rel="noopener noreferrer"&gt;plan's rate limits&lt;/a&gt;, and update the &lt;code&gt;invoices&lt;/code&gt; list with your actual file paths.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With &lt;code&gt;MAX_CONCURRENT = 10&lt;/code&gt; and several seconds per conversion (including polling), the batch processes 10 documents at a time. The semaphore prevents you from flooding the API with simultaneous requests and hitting the rate limit ceiling. &lt;code&gt;asyncio&lt;/code&gt; is part of Python's standard library, so no additional dependencies beyond &lt;code&gt;aiohttp&lt;/code&gt; are needed.&lt;/p&gt;

&lt;p&gt;Credit consumption matters at scale. The &lt;a href="https://app.developer-api.foxit.com/pricing" rel="noopener noreferrer"&gt;Developer plan&lt;/a&gt; includes 500 credits/year. The Startup plan ($1,750/year) provides 3,500 credits. Each conversion typically costs 1 credit. For higher volumes, the Business plan ($4,500/year) includes 150,000 credits. Check your remaining credit balance via the &lt;a href="https://app.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Developer Dashboard&lt;/a&gt; before launching a large batch job.&lt;/p&gt;

&lt;p&gt;For volumes beyond what a single process can handle efficiently, a queue-based architecture decouples submission from processing. Services like &lt;a href="https://aws.amazon.com/sqs/" rel="noopener noreferrer"&gt;Amazon SQS&lt;/a&gt; or &lt;a href="https://redis.io/docs/latest/develop/data-types/streams/" rel="noopener noreferrer"&gt;Redis Streams&lt;/a&gt; handle the message brokering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App Server -&amp;gt; Message Queue (SQS / Redis Streams) -&amp;gt; Worker Pool (N workers)
  Worker: upload HTML -&amp;gt; create task -&amp;gt; poll -&amp;gt; download PDF -&amp;gt; store in S3/GCS
  Worker: update job status in Postgres / Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worker picks a job from the queue, runs the 4-step conversion workflow, writes the resulting PDF to S3 or GCS, and updates the job status in a database. This pattern handles burst volume naturally: jobs queue up during spikes, workers drain at the rate the API allows, and your app server is never blocked waiting for conversions to complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Deployment Patterns for HTML-to-PDF Pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error Handling and Retry Logic
&lt;/h3&gt;

&lt;p&gt;Map HTTP status codes to decisions before writing any retry logic, because not all errors warrant a retry.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;400 Bad Request&lt;/code&gt; means your request body is malformed. Retrying the same payload returns another 400, so fix the payload. A &lt;code&gt;429 Too Many Requests&lt;/code&gt; and a &lt;code&gt;503 Service Unavailable&lt;/code&gt; are transient: back off and retry. A &lt;code&gt;FAILED&lt;/code&gt; task status means the conversion itself failed (possibly due to invalid HTML or unreachable URLs). Check the task response for diagnostic details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RequestException&lt;/span&gt;

&lt;span class="n"&gt;PERMANENT_ERRORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;TRANSIENT_ERRORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;POST with exponential backoff and jitter for transient errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PERMANENT_ERRORS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Permanent error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TRANSIENT_ERRORS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Max retries exceeded. Last status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transient error &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Retrying in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_delay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected: exhausted retries without returning or raising&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage with the URL-to-PDF endpoint
&lt;/span&gt;&lt;span class="n"&gt;auth_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;post_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/create/pdf-from-url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/invoice/1042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auth_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every POST request goes through a retry loop with &lt;a href="https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/" rel="noopener noreferrer"&gt;exponential backoff&lt;/a&gt;. The function distinguishes between permanent errors (like &lt;code&gt;400&lt;/code&gt; or &lt;code&gt;401&lt;/code&gt;, which you shouldn't retry) and transient errors (like &lt;code&gt;429&lt;/code&gt; or &lt;code&gt;503&lt;/code&gt;, which resolve on their own). Each retry doubles the wait time and adds random jitter to avoid synchronized retry waves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before running:&lt;/strong&gt; Replace &lt;code&gt;CLIENT_ID&lt;/code&gt;, &lt;code&gt;CLIENT_SECRET&lt;/code&gt;, and &lt;code&gt;HOST&lt;/code&gt; with your Foxit credentials and API host, or load them from environment variables as shown in the earlier examples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The jitter (&lt;code&gt;random.uniform(0, 0.5)&lt;/code&gt;) prevents a thundering herd where every worker wakes up and retries simultaneously after a 429 burst. Plain exponential backoff produces synchronized retry waves when all workers hit the rate limit at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Output Optimization: Compression and Linearization
&lt;/h3&gt;

&lt;p&gt;After conversion, you can chain additional PDF operations using the same async pattern. Upload the resulting PDF, call the compression or linearization endpoint, poll, and download the optimized version.&lt;/p&gt;

&lt;p&gt;For PDFs served directly in a browser, &lt;a href="https://kb.foxit.com/hc/en-us/articles/23450952035348-What-is-linearized-PDF" rel="noopener noreferrer"&gt;linearization&lt;/a&gt; enables Fast Web View, which lets the browser display page one while the rest of the file downloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compress_and_linearize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_pdf_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compress a PDF, then linearize it for fast web viewing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;json_headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Upload the PDF
&lt;/span&gt;    &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;upload_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Compress
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/modify/pdf-compress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compressionLevel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;compressed_doc_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Linearize the compressed result (no need to re-upload; use the resultDocumentId)
&lt;/span&gt;    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pdf-services/api/documents/optimize/pdf-linearize&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;compressed_doc_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Download the final optimized PDF
&lt;/span&gt;    &lt;span class="nf"&gt;download_document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultDocumentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You chain two PDF operations back-to-back here. First, you upload the PDF and compress it at &lt;code&gt;MEDIUM&lt;/code&gt; level (valid options are &lt;code&gt;LOW&lt;/code&gt;, &lt;code&gt;MEDIUM&lt;/code&gt;, and &lt;code&gt;HIGH&lt;/code&gt;). Once compression completes, you pass the &lt;code&gt;resultDocumentId&lt;/code&gt; directly into the linearization step, which avoids a second upload. The final download gives you a PDF that's both smaller and optimized for progressive loading in browsers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This function reuses &lt;code&gt;upload_document()&lt;/code&gt;, &lt;code&gt;poll_task()&lt;/code&gt;, and &lt;code&gt;download_document()&lt;/code&gt; from the earlier examples. Make sure those functions are defined in the same script with your credentials configured. The &lt;a href="https://dev.to/foxitdevelopers/how-to-chain-pdf-actions-with-foxit-p99"&gt;Foxit developer blog post on chaining PDF actions&lt;/a&gt; covers this pattern in detail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Monitoring and Secret Management
&lt;/h3&gt;

&lt;p&gt;Monitor three things per conversion job: how long each call takes (to spot API degradation early), credits consumed per job type (to forecast when you'll hit your plan ceiling), and failure rate broken down by error code (to catch template regressions before customers do). Set an alert when your remaining credits fall below 20% of the plan allocation. The &lt;a href="https://app.developer-api.foxit.com/" rel="noopener noreferrer"&gt;Foxit Developer Dashboard&lt;/a&gt; surfaces real-time usage data worth checking before kicking off large batch runs.&lt;/p&gt;

&lt;p&gt;Store API credentials in environment variables or a secrets manager (&lt;a href="https://aws.amazon.com/secrets-manager/" rel="noopener noreferrer"&gt;AWS Secrets Manager&lt;/a&gt;, &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt;, &lt;a href="https://cloud.google.com/secret-manager" rel="noopener noreferrer"&gt;GCP Secret Manager&lt;/a&gt;). When team members leave or you suspect a credential leak, rotate them from the Developer Dashboard. New credentials can be generated and old ones revoked without downtime, as long as you update your environment before revoking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Converting HTML to PDF
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://app.developer-api.foxit.com/pricing" rel="noopener noreferrer"&gt;Foxit Developer plan&lt;/a&gt; is free, requires no credit card, and gives you 500 credits to start with. Grab your &lt;code&gt;client_id&lt;/code&gt; and &lt;code&gt;client_secret&lt;/code&gt; from the Developer Dashboard, then either clone the &lt;a href="https://github.com/foxitsoftware/developerapidemos" rel="noopener noreferrer"&gt;demo repository&lt;/a&gt; for ready-made examples in Python, Node.js, and PHP, or drop the URL-to-PDF snippet from this guide into a script and run it against any public page.&lt;/p&gt;

&lt;p&gt;Once your first conversion comes back, check credit usage in the Dashboard to project costs at your production volume. If you need more throughput, the Startup plan ($1,750/year for 3,500 credits) is self-serve with no sales call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.developer-api.foxit.com/pricing" rel="noopener noreferrer"&gt;Get started for free on the Foxit Developer Portal&lt;/a&gt;&lt;/p&gt;

</description>
      <category>foxit</category>
      <category>pdf</category>
      <category>html</category>
    </item>
    <item>
      <title>The 10 Best Next.js Starter Kits for SaaS in 2026</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Wed, 15 Apr 2026 06:45:10 +0000</pubDate>
      <link>https://dev.to/luciench/the-10-best-nextjs-starter-kits-for-saas-in-2026-p7d</link>
      <guid>https://dev.to/luciench/the-10-best-nextjs-starter-kits-for-saas-in-2026-p7d</guid>
      <description>&lt;p&gt;You've decided to build a SaaS product with Next.js. The smart move is starting from a production-ready starter kit instead of spending three months wiring up auth, payments, and a dashboard from scratch. The not-so-smart move is picking the wrong one and realizing it six weeks in.&lt;/p&gt;

&lt;p&gt;There are dozens of Next.js boilerplates competing for your money right now. Some are genuinely excellent. Some are glorified &lt;code&gt;create-next-app&lt;/code&gt; templates with a Stripe webhook bolted on. This comparison cuts through the noise with real technical details: actual tech stacks, actual pricing, actual feature coverage, and honest trade-offs.&lt;/p&gt;

&lt;p&gt;I've built products on several of these and evaluated the rest against real-world SaaS requirements: authentication with RBAC, payment flexibility, database architecture, CMS capabilities, and how much code you'll actually need to write after checkout.&lt;/p&gt;

&lt;p&gt;Here are the 10 best Next.js starter kits for SaaS in 2026, ranked.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. RapidLaunch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyijzp1b6xvh9y6hkf65c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyijzp1b6xvh9y6hkf65c.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $99 (Starter) / $199 (Pro/Unlimited) - one-time, early bird pricing&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://rapidlaun.ch" rel="noopener noreferrer"&gt;rapidlaun.ch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16 with App Router&lt;/li&gt;
&lt;li&gt;TypeScript (strict mode)&lt;/li&gt;
&lt;li&gt;Supabase (PostgreSQL + Auth + Storage)&lt;/li&gt;
&lt;li&gt;Stripe + LemonSqueezy (choose via env variable)&lt;/li&gt;
&lt;li&gt;Tailwind CSS + DaisyUI&lt;/li&gt;
&lt;li&gt;TanStack Query&lt;/li&gt;
&lt;li&gt;TipTap rich text editor&lt;/li&gt;
&lt;li&gt;Zod validation&lt;/li&gt;
&lt;li&gt;Vitest + Playwright testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RapidLaunch is the most feature-complete starter kit on this list. Where most boilerplates give you auth, payments, and a landing page, RapidLaunch ships a full drag-and-drop CMS with 20+ section templates, an AI-powered chat widget with lead capture, real-time analytics with a 3D interactive visitor globe, and an AI template generator that builds complete websites from a business description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CMS alone justifies the price. You get 25+ templates, 35+ customizable components, a visual navigation editor, footer editor, and a media library. Your non-technical co-founder can update the marketing site without touching code. That's a feature most teams don't get until they integrate a headless CMS as a separate service.&lt;/p&gt;

&lt;p&gt;The AI chat widget runs on configurable OpenAI models (GPT-4, O3 Mini, and five others) with real-time streaming, conversation memory, and an intent detection system that automatically triggers a contact form when it detects purchase intent. That replaces tools like Intercom or Drift that run $50-$150/month.&lt;/p&gt;

&lt;p&gt;The analytics dashboard tracks visitors in real-time with UTM attribution, browser fingerprinting, individual visitor session histories, and a WebGL globe showing geographic distribution. That's the kind of feature you'd pay $200+/month for from a standalone analytics tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The full feature set:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication with RBAC (Owner + Admin roles)&lt;/li&gt;
&lt;li&gt;Stripe and LemonSqueezy billing (subscriptions, one-time, guest checkout)&lt;/li&gt;
&lt;li&gt;Automated webhook handling and two-way product sync&lt;/li&gt;
&lt;li&gt;Customer portal with account, billing, and subscription management&lt;/li&gt;
&lt;li&gt;Admin panel (CMS, users, billing, analytics, chat management)&lt;/li&gt;
&lt;li&gt;Maintenance mode and Coming Soon mode (one-click toggle)&lt;/li&gt;
&lt;li&gt;Cookie consent system&lt;/li&gt;
&lt;li&gt;SEO optimization&lt;/li&gt;
&lt;li&gt;15+ DaisyUI theme presets&lt;/li&gt;
&lt;li&gt;52+ unit tests, 15 database migrations&lt;/li&gt;
&lt;li&gt;CLAUDE.md file for AI-assisted development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Supabase is the only database option. If you need raw PostgreSQL with a different hosting provider or want MongoDB, you'll need to adapt the data layer. Auth is email/password only out of the box (no social OAuth yet, though Supabase supports adding it). The project is newer than established competitors like ShipFast or Makerkit, so the community is still growing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; If you want the most complete out-of-the-box experience (CMS + AI + analytics + auth + payments) at a price that undercuts most competitors, RapidLaunch is the strongest option available right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ShipFast
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1uutgvplkcrf1tl5t54s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1uutgvplkcrf1tl5t54s.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $199 (Starter) / $249 (All-in) / $299 (Bundle with CodeFast) - one-time&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://shipfa.st" rel="noopener noreferrer"&gt;shipfa.st&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15&lt;/li&gt;
&lt;li&gt;TypeScript or JavaScript&lt;/li&gt;
&lt;li&gt;MongoDB or Supabase&lt;/li&gt;
&lt;li&gt;Google OAuth + Magic Links&lt;/li&gt;
&lt;li&gt;Stripe + LemonSqueezy&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Mailgun or Resend for email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ShipFast is the most popular Next.js boilerplate by sheer community size. Built by Marc Lou, it has a 5,000+ member Discord with revenue leaderboards and a culture of shipping fast. The average user launches in 7 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Auth, payments, email with DNS setup, SEO, a blog, pre-built UI components, and $1,210 in partner discounts. It's laser-focused on getting an MVP out the door with minimal friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; No CMS, no admin dashboard beyond basic user management, no analytics, no AI features. The database choice is MongoDB or Supabase only. If your SaaS grows beyond MVP stage, you'll be building significant infrastructure on top of what ShipFast provides. The community and shipping culture are genuinely valuable, but the actual codebase is thinner than competitors at the same price point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for solo founders who prioritize speed over feature completeness and value the community aspect. If you want to ship an MVP in a week and figure out the rest later, ShipFast delivers on that promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Supastarter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1b01sf02utz7cg8jupmp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1b01sf02utz7cg8jupmp.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $349 (Solo) / $799 (Startup, 5 seats) / $1,499 (Agency, 10 seats, white-label) - one-time&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://supastarter.dev" rel="noopener noreferrer"&gt;supastarter.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js, Nuxt, or SvelteKit (same features across frameworks)&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Prisma or Drizzle ORM&lt;/li&gt;
&lt;li&gt;better-auth (2FA, passkeys, RBAC)&lt;/li&gt;
&lt;li&gt;Stripe, LemonSqueezy, Polar, Creem, Dodo Payments (5 providers)&lt;/li&gt;
&lt;li&gt;Hono.js with oRPC&lt;/li&gt;
&lt;li&gt;Tailwind CSS + Radix UI&lt;/li&gt;
&lt;li&gt;TanStack Query&lt;/li&gt;
&lt;li&gt;trigger.dev / QStash for background jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supastarter is the enterprise option. Five payment providers, framework-agnostic (the same feature set works on Next.js, Nuxt, or SvelteKit), i18n baked in, team/organization management, and a super admin dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; The payment flexibility is unmatched. Five providers means you can support customers in markets where Stripe isn't available without rebuilding your billing layer. The Agency tier at $1,499 includes white-label licensing, which is the right call if you're building SaaS products for clients. It's also optimized for AI coding agents (Cursor and Claude Code ready).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; No built-in CMS (blog and docs only), no AI features, no analytics dashboard. The starting price at $349 is higher than most alternatives, and the Agency tier at $1,499 is the most expensive kit on this list. The breadth of options (three frameworks, two ORMs, five payment providers) means more decisions upfront. If you're a solo developer building one product, Supastarter may be more kit than you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for teams and agencies that need multi-framework support, maximum payment flexibility, and white-label capabilities. Overkill for a solo founder building one product.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Makerkit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopgn0q549lw1kkwypudj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopgn0q549lw1kkwypudj.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $299 (Pro, individual) / $599 (Teams, 5 collaborators) - one-time, lifetime access&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://makerkit.dev" rel="noopener noreferrer"&gt;makerkit.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16, React 19&lt;/li&gt;
&lt;li&gt;TypeScript 5&lt;/li&gt;
&lt;li&gt;Supabase (with RLS), Drizzle, or Prisma (choose your stack)&lt;/li&gt;
&lt;li&gt;OAuth + MFA (Google, GitHub, Facebook, X, Discord, TOTP)&lt;/li&gt;
&lt;li&gt;Stripe Checkout + Customer Portal&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4 + Shadcn UI&lt;/li&gt;
&lt;li&gt;Playwright for E2E testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Makerkit has been around since 2022, making it one of the longest-running Next.js SaaS kits. It's focused on multi-tenant B2B SaaS with row-level security, RBAC, and team management built in from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; The database flexibility is strong. You choose between Supabase with row-level security, Drizzle ORM with Better Auth, or Prisma 7 with Better Auth. Each stack is fully implemented, not just a config toggle. The Figma UI kit and React.Email templates are nice touches that save design time. Auth is the most complete of any kit here: email/password, magic links, social OAuth (five providers), and TOTP MFA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; No CMS, no AI features, no built-in analytics. The $299 starting price is on the higher side for an individual license. The multiple stack options, while flexible, mean the documentation and codebase are more complex to navigate than a more opinionated kit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for B2B SaaS products that need multi-tenancy, strong auth, and row-level security from day one. The longest track record of any kit on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Nextbase
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9tgnen8mna7yr8dj1mwr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9tgnen8mna7yr8dj1mwr.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $99 (Essential) / $299 (Pro) / $399 (Ultimate) - one-time, lifetime access&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://usenextbase.com" rel="noopener noreferrer"&gt;usenextbase.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15+&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;Stripe&lt;/li&gt;
&lt;li&gt;Tailwind CSS + Shadcn&lt;/li&gt;
&lt;li&gt;Jest + Playwright&lt;/li&gt;
&lt;li&gt;Sentry error tracking&lt;/li&gt;
&lt;li&gt;PostHog + Google Analytics&lt;/li&gt;
&lt;li&gt;OpenAI GPT-4&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nextbase takes a tiered approach. The $99 Essential tier covers auth, payments, multi-tenancy, admin panel, and MDX docs. The $299 Pro tier adds a blog, user feedback collection, roadmap functionality, changelog, and user impersonation. The $399 Ultimate tier unlocks everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; AI integration with GPT-4 is built in, and the monitoring stack (Sentry + PostHog) means you start with observability from day one instead of bolting it on later. The $99 entry price for a solid set of core features is competitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Supabase only. Features are locked by tier, so you need to check exactly what you're getting at each price point. No CMS beyond the MDX blog. The community is smaller than ShipFast or Makerkit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Strong mid-range option with a genuinely useful tiered pricing model. The $99 Essential tier is one of the best value propositions for a production-ready starter.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. LaunchFast
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq321w0g5ljv6c6sy6kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmq321w0g5ljv6c6sy6kc.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $99 (single framework) / $149 (Astro + Next.js + SvelteKit bundle) - one-time&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://www.launchfa.st" rel="noopener noreferrer"&gt;launchfa.st&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (also Astro and SvelteKit versions)&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;5+ database options (MongoDB, Firebase, PostgreSQL via Neon/Supabase/Xata, Redis, SQLite)&lt;/li&gt;
&lt;li&gt;Flexible auth (email, magic links, OAuth)&lt;/li&gt;
&lt;li&gt;Stripe + LemonSqueezy&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Resend, Postmark, SendGrid, Mailgun, AutoSend&lt;/li&gt;
&lt;li&gt;S3, Cloudflare R2, Firebase, Supabase Storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LaunchFast is the "bring your own stack" option. It supports more database providers, email services, file storage backends, and deployment targets than any other kit on this list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; At $99 you get maximum flexibility. Swap databases, auth providers, payment processors, and deployment targets without rewriting your app. The $149 bundle gives you the same feature set across three frameworks. Deployment options include Vercel, Cloudflare Workers, Render, Fly.io, Netlify, and AWS Amplify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Less opinionated means more decisions and more wiring. No CMS, no AI features, no analytics dashboard. The documentation covers breadth over depth for each provider combination. If you want something that works out of the box with minimal configuration, this isn't it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for developers who have strong preferences about their stack and want a starter kit that adapts to them, not the other way around. The $99 price point is excellent for the flexibility offered.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Shipped.club
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfs0i3k0l06pqv0qe3ex.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfs0i3k0l06pqv0qe3ex.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $149 - one-time, lifetime access&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://shipped.club" rel="noopener noreferrer"&gt;shipped.club&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Supabase&lt;/li&gt;
&lt;li&gt;NextAuth + Supabase Auth&lt;/li&gt;
&lt;li&gt;LemonSqueezy&lt;/li&gt;
&lt;li&gt;ChakraUI + Tailwind CSS&lt;/li&gt;
&lt;li&gt;MailChimp + Loops&lt;/li&gt;
&lt;li&gt;MDX blog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shipped.club is the budget option that doesn't feel like one. At $149 you get a complete starter with auth, payments, a landing page, waitlist page, pre-order page, and an affiliate program built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; The affiliate program is a feature no other kit on this list includes out of the box. Dual UI framework support (Chakra UI and Tailwind) gives you styling flexibility. PPP (Purchasing Power Parity) pricing makes it accessible globally. There's also a Chrome Extension version if that's your product format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Stuck on Next.js 14 (not the latest). LemonSqueezy is the only payment option (no Stripe). Supabase only. Less documentation and a smaller community than the top-tier competitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best bang for your buck if you're on a tight budget and LemonSqueezy fits your payment needs. The affiliate program inclusion is a smart differentiator.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Indie Starter
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nxpqkehj5jmy9ssz5wx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9nxpqkehj5jmy9ssz5wx.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $69 (Tier 1) / $199 (Tier 2) / $299 (Tier 3) - one-time, unlimited projects&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://indie-starter.dev" rel="noopener noreferrer"&gt;indie-starter.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (latest)&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Supabase (PostgreSQL)&lt;/li&gt;
&lt;li&gt;Magic links + social OAuth (Google, GitHub)&lt;/li&gt;
&lt;li&gt;Stripe with webhooks&lt;/li&gt;
&lt;li&gt;Tailwind CSS + Shadcn/ui&lt;/li&gt;
&lt;li&gt;Sanity CMS&lt;/li&gt;
&lt;li&gt;Resend for email&lt;/li&gt;
&lt;li&gt;Umami + Google Analytics&lt;/li&gt;
&lt;li&gt;Zod validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Indie Starter stands out for its tiered pricing model starting at just $69. The Sanity CMS integration is a differentiator; most kits ship a basic MDX blog, but Indie Starter gives you a proper headless CMS for content management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; The $69 entry tier makes it the cheapest paid option on this list. SEO infrastructure (sitemaps, robots.txt, Schema.org markup) is built in, not an afterthought. Sanity CMS integration means you get a real content management workflow, not just markdown files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Feature availability varies by tier. Supabase is the only database option. The community is smaller. Limited auth options compared to Makerkit or Supastarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for indie hackers who want a solid foundation at minimal cost. The $69 tier is genuinely usable, not a stripped-down demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Nextless.js
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxi4pqq0qw540j64hnfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxi4pqq0qw540j64hnfs.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; $699 (Single, 1 project) / $2,099 (Unlimited) - one-time + optional $199-$599/year renewal&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://nextlessjs.com" rel="noopener noreferrer"&gt;nextlessjs.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;AWS Lambda (serverless)&lt;/li&gt;
&lt;li&gt;PostgreSQL, MySQL, MongoDB, or DynamoDB&lt;/li&gt;
&lt;li&gt;AWS Cognito (email, social login, MFA)&lt;/li&gt;
&lt;li&gt;Stripe&lt;/li&gt;
&lt;li&gt;AWS SES, SendGrid, Postmark, Mailgun, Mandrill&lt;/li&gt;
&lt;li&gt;AWS CDK (Infrastructure as Code)&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Jest (unit, integration, E2E)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nextless.js is the AWS-native option. If your company runs on AWS and needs serverless architecture with Infrastructure as Code, this is the only kit that delivers that out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; Full AWS CDK templates for infrastructure provisioning. Four database options in the Unlimited tier. Silo multi-tenancy model for proper data isolation. React Native boilerplate included for mobile support. This is the only kit on this list with a mobile-ready path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; The most expensive option by far ($699-$2,099). Annual renewal for updates adds ongoing cost. AWS vendor lock-in is real. The serverless architecture has a learning curve if you're not already AWS-comfortable. Stuck on Next.js 14. No CMS, no AI features, no analytics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; The right choice only if you're building on AWS and need serverless architecture with IaC. For everyone else, the price and complexity aren't justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Next.js SaaS Starter (by Vercel)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazr4h9a3lrap5160s5mt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazr4h9a3lrap5160s5mt.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Price:&lt;/strong&gt; Free (open source)&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;a href="https://github.com/nextjs/saas-starter" rel="noopener noreferrer"&gt;github.com/nextjs/saas-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (latest)&lt;/li&gt;
&lt;li&gt;TypeScript (93% of codebase)&lt;/li&gt;
&lt;li&gt;PostgreSQL + Drizzle ORM&lt;/li&gt;
&lt;li&gt;Email/password auth with JWTs in cookies&lt;/li&gt;
&lt;li&gt;Stripe&lt;/li&gt;
&lt;li&gt;Shadcn/ui&lt;/li&gt;
&lt;li&gt;Zod validation&lt;/li&gt;
&lt;li&gt;pnpm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official Next.js SaaS starter from Vercel. It's intentionally minimal: auth, Stripe checkout, subscription management via Customer Portal, Server Actions with Zod validation, an activity logging system, and protected routes with middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What sets it apart:&lt;/strong&gt; It's free, it's from Vercel, and it uses the latest Next.js patterns. The codebase is clean and readable, making it an excellent learning resource. No bloat, no unnecessary abstractions. If you understand what you're building and just need a solid starting point, this does the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt; Intentionally minimal means you'll add everything yourself: email service, landing pages, admin dashboard, CMS, AI integration, analytics, advanced auth (social OAuth, MFA), file storage. This is a starting point, not a finished product. If you factor in the hours to build what paid kits include, the "free" label is misleading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict:&lt;/strong&gt; Best for experienced developers who want maximum control and are comfortable building features from scratch. Also the best learning resource for understanding Next.js SaaS architecture patterns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;a href="https://rapidlaun.ch" rel="noopener noreferrer"&gt;RapidLaunch&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://shipfa.st" rel="noopener noreferrer"&gt;ShipFast&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://supastarter.dev" rel="noopener noreferrer"&gt;Supastarter&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://makerkit.dev" rel="noopener noreferrer"&gt;Makerkit&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://usenextbase.com" rel="noopener noreferrer"&gt;Nextbase&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://www.launchfa.st" rel="noopener noreferrer"&gt;LaunchFast&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://shipped.club" rel="noopener noreferrer"&gt;Shipped.club&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://indie-starter.dev" rel="noopener noreferrer"&gt;Indie Starter&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://nextlessjs.com" rel="noopener noreferrer"&gt;Nextless.js&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://github.com/nextjs/saas-starter" rel="noopener noreferrer"&gt;Vercel Starter&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starting Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$199&lt;/td&gt;
&lt;td&gt;$349&lt;/td&gt;
&lt;td&gt;$299&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;$149&lt;/td&gt;
&lt;td&gt;$69&lt;/td&gt;
&lt;td&gt;$699&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time&lt;/td&gt;
&lt;td&gt;One-time + renewal&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Next.js Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Latest&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;15+&lt;/td&gt;
&lt;td&gt;Latest&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Latest&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Latest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strict&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Strict&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;MongoDB/Supabase&lt;/td&gt;
&lt;td&gt;Prisma/Drizzle&lt;/td&gt;
&lt;td&gt;Supabase/Drizzle/Prisma&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;5+ options&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;4 options&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: Email/Password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (magic links)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: Social OAuth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (extensible)&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;5+ providers&lt;/td&gt;
&lt;td&gt;5 providers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;20+ providers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Google, GitHub&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: MFA/2FA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (passkeys)&lt;/td&gt;
&lt;td&gt;TOTP MFA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: RBAC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (RLS)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stripe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LemonSqueezy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Other Payments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Polar, Creem, Dodo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CMS/Page Builder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag-and-drop (20+ sections)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Sanity CMS&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Blog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via CMS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;MDX (Pro tier)&lt;/td&gt;
&lt;td&gt;MDX&lt;/td&gt;
&lt;td&gt;MDX&lt;/td&gt;
&lt;td&gt;Sanity&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Admin Dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (full)&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Chat Widget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPT-4 (7 models)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;GPT-4&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Site Generator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (3D globe, UTM, fingerprinting)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;PostHog + GA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Umami + GA&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Customer Portal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Stripe)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Mailgun/Resend&lt;/td&gt;
&lt;td&gt;Multiple&lt;/td&gt;
&lt;td&gt;React.Email&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;5 providers&lt;/td&gt;
&lt;td&gt;MailChimp/Loops&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;5 providers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;i18n&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-tenancy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (RLS)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (silo)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vitest + Playwright&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Jest + Playwright&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Jest&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Affiliate Program&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mobile Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;React Native&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Figma UI Kit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unlimited Projects&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pro tier&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Solo tier+&lt;/td&gt;
&lt;td&gt;Pro tier&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Unlimited tier&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Community Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Growing&lt;/td&gt;
&lt;td&gt;8,000+&lt;/td&gt;
&lt;td&gt;1,300+&lt;/td&gt;
&lt;td&gt;Established&lt;/td&gt;
&lt;td&gt;Growing&lt;/td&gt;
&lt;td&gt;110+&lt;/td&gt;
&lt;td&gt;330+&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;15.7K stars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The Next.js starter kit market has matured significantly. Every option on this list will save you time compared to starting from scratch. The difference comes down to how much time.&lt;/p&gt;

&lt;p&gt;Kits like the Vercel SaaS Starter and LaunchFast give you a foundation and expect you to build. Kits like ShipFast and Shipped.club get you to MVP fast with the essentials. And kits like RapidLaunch, Supastarter, and Makerkit aim to give you a near-complete product from day one.&lt;/p&gt;

&lt;p&gt;If I'm starting a new SaaS project today, my pick is &lt;a href="https://rapidlaun.ch" rel="noopener noreferrer"&gt;RapidLaunch&lt;/a&gt;. The combination of a full CMS with drag-and-drop builder, AI-powered chat with lead capture, enterprise-grade analytics, and dual payment provider support (Stripe + LemonSqueezy) at $99-$199 one-time is the best value proposition in this space. You'd spend more than that in a single month on the standalone tools it replaces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidlaun.ch" rel="noopener noreferrer"&gt;Get started with RapidLaunch&lt;/a&gt; and launch your SaaS in days, not months.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
    </item>
    <item>
      <title>Backstage SaaS &amp; Open Source Alternatives</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Thu, 29 Jan 2026 07:26:39 +0000</pubDate>
      <link>https://dev.to/luciench/backstage-saas-open-source-alternatives-2m08</link>
      <guid>https://dev.to/luciench/backstage-saas-open-source-alternatives-2m08</guid>
      <description>&lt;p&gt;Spotify's &lt;a href="https://roadie.io/backstage-spotify/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt; created the Internal Developer Portal category. It showed that a central hub for services, documentation, and tooling could actually improve how developers work. But here's what a lot of engineering leaders figure out the hard way: Backstage isn't right for every team.&lt;/p&gt;

&lt;p&gt;If you're reading this, you've probably realized that adopting Backstage means committing serious engineering resources. You need a dedicated team, TypeScript expertise, and months of setup time. For a lot of teams, the operational overhead just isn't worth it.&lt;/p&gt;

&lt;p&gt;But the problem Backstage solves doesn't go away. As your organization grows past about 150 people, you hit what's called the &lt;a href="https://en.wikipedia.org/wiki/Dunbar%27s_number" rel="noopener noreferrer"&gt;Dunbar Number Effect&lt;/a&gt;. Anthropologist Robin Dunbar found that humans can only maintain stable social networks of around 150 people. Go beyond that, and tribal knowledge disappears. Nobody knows who owns what. The informal Slack channels that worked when you were 30 people turn into complete chaos.&lt;/p&gt;

&lt;p&gt;You need a system to fix this chaos, a "system of record" for your engineering organization. But you don't need the complexity of self-hosted Backstage to get there.&lt;/p&gt;

&lt;p&gt;This guide covers the best Backstage alternatives in 2026. I'll break down three paths you can take, Build, Buy, or Hybrid, and help you figure out which one makes sense without drowning your platform team in maintenance work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Paths to an IDP: Build, Buy, or Hybrid
&lt;/h2&gt;

&lt;p&gt;Before we look at specific tools, you need to understand the three approaches you can take:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build (Self-Hosted Backstage):&lt;/strong&gt; You take the open-source Backstage project and dedicate engineers to build, customize, and maintain your own portal. Ultimate flexibility, but significant headcount and operational costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buy (Proprietary IDPs):&lt;/strong&gt; You purchase a SaaS solution from a vendor like Cortex or Port. Quick to set up and feature-rich, but you're locked into their proprietary data model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid (Managed Backstage):&lt;/strong&gt; You use a service like Roadie that handles the hosting and maintenance of Backstage for you. You get the open-source ecosystem without the operational burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backstage Alternatives: At a Glance
&lt;/h2&gt;

&lt;p&gt;Here's a quick comparison of your options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Core Technology&lt;/th&gt;
&lt;th&gt;Hosting Model&lt;/th&gt;
&lt;th&gt;Key Strength&lt;/th&gt;
&lt;th&gt;Ideal For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Roadie&lt;/td&gt;
&lt;td&gt;Backstage&lt;/td&gt;
&lt;td&gt;SaaS (Managed)&lt;/td&gt;
&lt;td&gt;Backstage ecosystem without the overhead&lt;/td&gt;
&lt;td&gt;Teams who want Backstage's power but need a managed solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cortex&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Engineering metrics and scorecards&lt;/td&gt;
&lt;td&gt;Organizations focused on measuring service quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Developer-friendly API and flexibility&lt;/td&gt;
&lt;td&gt;Teams building custom workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlassian Compass&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Deep Atlassian integration&lt;/td&gt;
&lt;td&gt;Companies invested in the Atlassian stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpsLevel&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Service maturity and reliability checks&lt;/td&gt;
&lt;td&gt;SRE teams enforcing production standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-Hosted Backstage&lt;/td&gt;
&lt;td&gt;Backstage (OSS)&lt;/td&gt;
&lt;td&gt;Self-Hosted&lt;/td&gt;
&lt;td&gt;Ultimate customization&lt;/td&gt;
&lt;td&gt;Large orgs with dedicated platform teams (5+ engineers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Hybrid Approach: Managed Backstage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Roadie
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk55dpfc1pbat14xasv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjk55dpfc1pbat14xasv1.png" alt=" " width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://roadie.io/" rel="noopener noreferrer"&gt;Roadie&lt;/a&gt; isn't an alternative to Backstage, it's a different way to adopt it. The core idea is that you shouldn't have to choose between the power of an open-source community and the convenience of a SaaS product.&lt;/p&gt;

&lt;p&gt;I've talked to a lot of teams who committed to self-hosting Backstage, only to realize it requires 3-12 engineers and 6-12 months to get production-ready. That's a significant investment. Roadie solves this by providing a secure, scalable, and fully managed Backstage experience out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams who've decided on Backstage but want to accelerate their timeline and reduce operational burden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get a production-ready Backstage instance running in minutes. Roadie handles upgrades, security, and maintenance.&lt;/li&gt;
&lt;li&gt;Enterprise features like Role-Based Access Control, enterprise-grade search, and scorecards come built-in.&lt;/li&gt;
&lt;li&gt;Install any open-source Backstage plugin without rebuilding your instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; Roadie uses Backstage as its foundation, so you get the same data model and core experience. If you want a completely different, highly opinionated UI, a proprietary vendor might fit better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Buy" Approach: Proprietary IDPs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cortex
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fciix318u8wtur2nrnm1n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fciix318u8wtur2nrnm1n.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cortex.io/" rel="noopener noreferrer"&gt;Cortex&lt;/a&gt; has established itself as a leader in the IDP space. They focus heavily on service quality, reliability, and engineering metrics. Their Scorecards feature is particularly strong for defining standards and tracking adoption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Organizations focused on establishing and measuring engineering standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt; A central inventory for microservices and APIs, scorecards to track service health, and a scaffolder for creating new services from templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; Cortex is proprietary. You're locked into their data model, and migrating away later could be painful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Port
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapz8ep2vaec20w81hvip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fapz8ep2vaec20w81hvip.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.getport.io/" rel="noopener noreferrer"&gt;Port&lt;/a&gt; is built around flexibility. Their developer-friendly API lets you ingest any data and build custom workflows. They position themselves as a platform for building a developer portal, not a rigid out-of-the-box solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Platform teams with strong dev skills who want to build highly custom experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt; A flexible "blueprint" model to define any asset, a self-service hub for custom actions, and scorecards for tracking quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; Port's flexibility is powerful but comes with a steeper learning curve. Expect more initial setup compared to more opinionated platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atlassian Compass
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejre6rjpe9w2rvhsi065.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fejre6rjpe9w2rvhsi065.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.atlassian.com/software/compass" rel="noopener noreferrer"&gt;Atlassian Compass&lt;/a&gt; is Atlassian's entry into developer experience. Its main advantage is seamless integration with Jira, Confluence, and Bitbucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Companies already standardized on Atlassian tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt; A component catalog for tracking ownership, health scorecards, and deep native integration with other Atlassian products.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; If you're not an Atlassian-centric organization, Compass may feel less compelling compared to other options.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpsLevel
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fde29ux2aoxvjhf4ou5h2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fde29ux2aoxvjhf4ou5h2.png" alt=" " width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.opslevel.com/" rel="noopener noreferrer"&gt;OpsLevel&lt;/a&gt; is a mature player focused on service ownership and reliability. SRE and platform teams like them because they help answer, "Is our software ready for production?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; SRE-driven organizations enforcing service maturity standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt; A complete service catalog, an extensive library of automated maturity checks, and integrations with on-call tools like PagerDuty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; OpsLevel's focus is more on reliability and standards than on developer self-service, which is stronger in other platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Build" Approach: Self-Hosted Backstage
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F444snlufnj8ic1yayn7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F444snlufnj8ic1yayn7w.png" alt=" " width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choosing to self-host Backstage is a significant commitment. You should treat it like building an internal product, not just deploying a tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Large organizations with a well-funded platform team (5+ engineers) that has a clear mandate to build and maintain a customized developer portal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt; Complete control over the code and data model. You can customize it to your exact specifications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Considerations:&lt;/strong&gt; This path has a &lt;a href="https://roadie.io/blog/backstage-how-much-does-it-really-cost/" rel="noopener noreferrer"&gt;high cost of ownership&lt;/a&gt;. You need to account for the fully-loaded salaries of a dedicated engineering team, 6-12 months of initial build time, and ongoing operational burden for maintenance and upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose the Right Path
&lt;/h2&gt;

&lt;p&gt;Your choice depends on your organization's priorities, resources, and philosophy. Here are the questions you should ask:&lt;/p&gt;

&lt;h3&gt;
  
  
  How important is the open-source ecosystem to us?
&lt;/h3&gt;

&lt;p&gt;If you want to avoid vendor lock-in and tap into community innovation, choose between self-hosting Backstage or using a managed service like Roadie. If you prefer an all-in-one vendor experience, a proprietary option like Cortex or Port makes more sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the size and skill set of our platform team?
&lt;/h3&gt;

&lt;p&gt;If you have 5-10 engineers with TypeScript experience and a mandate to build a custom portal, self-hosted Backstage is viable. If your platform team is smaller or focused on other priorities, Roadie or a proprietary vendor is more efficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's our most critical problem right now?
&lt;/h3&gt;

&lt;p&gt;If you need scorecards and don't mind vendor lock-in, tools like Cortex or OpsLevel offer polished solutions.&lt;/p&gt;

&lt;p&gt;If you want to build custom workflows from scratch and are comfortable in a closed-source ecosystem, Port gives you a flexible API.&lt;/p&gt;

&lt;p&gt;If your organization lives entirely in the Atlassian suite, Compass is a natural extension.&lt;/p&gt;

&lt;p&gt;If you want enterprise features combined with the freedom of the open-source ecosystem, Roadie gives you both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;An Internal Developer Portal is a long-term investment in your developer experience. The choice between Build, Buy, and Hybrid depends entirely on your team's size, skills, and priorities.&lt;/p&gt;

&lt;p&gt;I've seen teams succeed with all three approaches. The key is being honest about what you can realistically maintain and what problems you're actually trying to solve.&lt;/p&gt;

&lt;p&gt;I'm curious what path you're evaluating. Are you leaning toward self-hosting Backstage? Considering a proprietary vendor? Looking at the hybrid approach with something like Roadie? What's the biggest factor driving your decision, team size, budget, or something else? Drop a comment and share what's working (or not working) in your evaluation process.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>saas</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The "Glue Work" Trap: Why Your Best Engineer Looks Like Your Worst Performer</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Wed, 28 Jan 2026 07:46:49 +0000</pubDate>
      <link>https://dev.to/luciench/the-glue-work-trap-why-your-best-engineer-looks-like-your-worst-performer-58jf</link>
      <guid>https://dev.to/luciench/the-glue-work-trap-why-your-best-engineer-looks-like-your-worst-performer-58jf</guid>
      <description>&lt;p&gt;Your top engineer closed half the tickets the new grad did last quarter. The performance dashboard says they're underperforming. Your gut says the opposite.&lt;/p&gt;

&lt;p&gt;I ran into this problem after moving from data engineering into management. One of my engineers spent three hours debugging why the nightly ETL job was timing out, which was blocking the entire team from running integration tests. They pair-programmed with our new hire to help them understand the data architecture. They wrote detailed documentation for the ETL migration because no one else would. They jumped into a Microsoft Teams thread at 9 PM to unblock a production deployment that couldn't wait until morning.&lt;/p&gt;

&lt;p&gt;Then I pulled up the performance dashboard. They had closed half the tickets that our new grad closed that quarter.&lt;/p&gt;

&lt;p&gt;The metrics said they were underperforming. My experience working with them said the opposite. They were the glue holding our team together.&lt;/p&gt;

&lt;p&gt;This isn't a data engineering problem. It's an engineering management problem. Your best engineers spend their time on essential work that keeps the team functional, but none of it shows up in the systems that measure performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Your Metrics Are Lying to You
&lt;/h2&gt;

&lt;p&gt;Jira and similar project management tools measure intent, not reality. They track what you planned to do, not what actually happened.&lt;/p&gt;

&lt;p&gt;The problem compounds as engineers get more senior. Junior engineers can focus on tickets. Senior engineers spend most of their time on work that never gets a ticket: reviewing 40% of the team's PRs, helping others debug gnarly issues, fixing the CI pipeline that's been broken for weeks, sitting in architecture reviews to share context.&lt;/p&gt;

&lt;p&gt;Creating a Jira ticket for "helped someone debug their environment setup" feels absurd. It took 20 minutes. But do that five times a week, and you've spent nearly two hours on invisible work. Multiply that across code reviews, production firefighting, mentoring conversations, and architectural guidance, and engineers can easily spend 50-70% of their time on work that has no corresponding ticket.&lt;/p&gt;

&lt;p&gt;The result: visible output shrinks as actual value increases. The ticket dashboard shows your best engineer as underperforming while they're actually the team's MVP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://noidea.dog/glue" rel="noopener noreferrer"&gt;Tanya Reilly calls this "being glue"&lt;/a&gt;, the technical leadership work that holds teams together but doesn't map cleanly to traditional promotion criteria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Brag Documents Don't Work
&lt;/h2&gt;

&lt;p&gt;The standard advice is to have engineers maintain brag documents. &lt;a href="https://jvns.ca/blog/brag-documents/" rel="noopener noreferrer"&gt;Julia Evans popularized this approach&lt;/a&gt;: engineers keep a running log of everything they do, then compile it for performance reviews.&lt;/p&gt;

&lt;p&gt;This rarely works in practice.&lt;/p&gt;

&lt;p&gt;It requires constant discipline. Engineers need to remember to update it after every contribution. Most forget until the week before their review, then try to reconstruct six months of work from memory.&lt;/p&gt;

&lt;p&gt;It also feels self-aggrandizing. Engineers are trained to let their work speak for itself. Writing "I am great because I did X" feels uncomfortable, even when X is genuinely valuable.&lt;/p&gt;

&lt;p&gt;And here's the real problem: without data backing it up, a brag document is just the engineer's word against the metrics. When you're under pressure to justify ratings, you'll default to the numbers.&lt;/p&gt;

&lt;p&gt;Brag documents work for some people. But they put the burden on the wrong person. As a manager, you should have systems that capture your team's actual work, not force them to manually log it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Data Actually Shows
&lt;/h2&gt;

&lt;p&gt;Your engineers' work leaves digital traces everywhere. Every PR they review generates data in GitHub. Every commit they make has metadata. Every Teams conversation is timestamped. Every calendar event is logged.&lt;/p&gt;

&lt;p&gt;This digital exhaust contains proof of the glue work. The challenge is assembling it into a coherent picture.&lt;/p&gt;

&lt;p&gt;When an engineer reviews 127 PRs while their peers review 30, that's measurable. It explains why their ticket count is lower. They're multiplying everyone else's productivity.&lt;/p&gt;

&lt;p&gt;Those 50-line PRs fixing type definitions, updating the Dockerfile, or adding error handling don't map to epics. But they're real work that keeps the system running.&lt;/p&gt;

&lt;p&gt;When an engineer spends a week unblocking another team's API integration, that work might not even show up in your team's metrics. But it has real business impact.&lt;/p&gt;

&lt;p&gt;The data exists. You just need to surface it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Tools Like Span Solve This Problem
&lt;/h2&gt;

&lt;p&gt;I came across &lt;a href="https://span.app" rel="noopener noreferrer"&gt;Span&lt;/a&gt; and I wish I'd known about them when I was dealing with the invisible work issue on my team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl6jkl96xyk5bfjanly9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl6jkl96xyk5bfjanly9s.png" alt=" " width="544" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The approach is different from traditional productivity tools. Instead of trying to make engineers track their work better, Span analyzes the actual work in your codebase and other systems. It looks at your code, PRs, Jira tickets, incident management tools, and calendars to understand what's really happening.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfmo302ki64n1qbnksld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfmo302ki64n1qbnksld.png" alt=" " width="701" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The platform uses AI to automatically categorize work into different types. That random Tuesday afternoon an engineer spends fixing the ETL timeout? Span classifies it as infrastructure work without them needing to create a ticket. The three hours spent reviewing PRs? That shows up in the data.&lt;/p&gt;

&lt;p&gt;One feature that particularly addresses the glue work problem is called Investment Mix. According to one of their customers, Chad Bayer (VP of Technology at The Helper Bees), "Span's Investment Mix is the best on the market. It now powers our executive updates and board discussions, replacing what used to be a time-consuming manual process."&lt;/p&gt;

&lt;p&gt;Investment Mix shows how engineering time is distributed across different categories of work. Instead of just counting tickets, you can see the actual breakdown between feature development, code reviews, infrastructure improvements, and maintenance work. That paints a completely different picture than "closed 8 tickets this quarter."&lt;/p&gt;

&lt;p&gt;The platform also tracks metrics like PR cycle time and team velocity, helping you spot patterns in how work flows through your team. You can see if code reviews are a bottleneck or if infrastructure work is eating into feature development time.&lt;/p&gt;

&lt;p&gt;The key thing is that it uses the actual work as the source of truth. It doesn't matter if an engineer created a ticket. If they made substantial changes to the infrastructure, that work gets classified and measured automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbj20lltmz28370xbyqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbj20lltmz28370xbyqe.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Identify Glue Work in Your Team
&lt;/h2&gt;

&lt;p&gt;You need to actively look for this. Your best engineers won't complain that their glue work is invisible. They're too busy doing the work.&lt;/p&gt;

&lt;p&gt;Here are the patterns to watch for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Review Burden&lt;/strong&gt;: Pull up your team's PR activity. If one engineer is reviewing 2-3x more PRs than everyone else, that's glue work. They're acting as a quality gate for the entire team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Unblocking Pattern&lt;/strong&gt;: Watch for engineers who are constantly mentioned in other people's PRs or Teams/Slack threads. If someone's name keeps coming up when people need help, they're doing glue work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Infrastructure Tax&lt;/strong&gt;: Some engineers volunteer to fix the annoying things that everyone complains about but no one wants to own. The flaky tests. The slow CI pipeline. The missing documentation. This work is nearly invisible but high-impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Onboarding Load&lt;/strong&gt;: If you have an engineer who every new hire gets paired with for their first few weeks, that's glue work. They're transferring institutional knowledge that doesn't exist anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Cross-Team Coordination&lt;/strong&gt;: Watch for engineers who spend time in meetings with other teams or helping unblock dependencies. This work often doesn't even show up in your team's metrics, but it has real business impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Credit Glue Work in Performance Reviews
&lt;/h2&gt;

&lt;p&gt;Once you identify the glue work, you need to explicitly credit it in performance reviews and compensation decisions.&lt;/p&gt;

&lt;p&gt;Instead of apologizing for an engineer's ticket count, reframe the conversation around leverage and impact. Present the review with data, not gut feelings.&lt;/p&gt;

&lt;p&gt;Show that they spent 35% of their time on developer enablement. Then show how that translated into team velocity improvements. If the team shipped 23% more features in the second half of the quarter compared to the first half, and the engineer's increased review load preceded that improvement, connect those dots.&lt;/p&gt;

&lt;p&gt;Quantify specific contributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They reviewed 130 PRs that quarter, representing a significant portion of the team's total output. Their review feedback reduced the revision cycle time by an average of 8 hours per PR.&lt;/li&gt;
&lt;li&gt;They spent 15% of their time on infrastructure improvements that cut the CI pipeline time from 45 minutes to 12 minutes, saving the team approximately 180 hours in aggregate wait time.&lt;/li&gt;
&lt;li&gt;They pair-programmed with three junior engineers on complex features. All three shipped their work on schedule with minimal revision cycles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data doesn't just prove they were busy. It proves they were strategic about where they invested time.&lt;/p&gt;

&lt;p&gt;When you present this to your director, the response will be immediate. The engineer gets promoted instead of getting a "meets expectations" rating.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Change Your Team's Incentives
&lt;/h2&gt;

&lt;p&gt;Measuring glue work isn't enough. You need to actively reward it, or engineers will optimize for what gets measured.&lt;/p&gt;

&lt;p&gt;Change how you run performance reviews. Explicitly evaluate engineers on three dimensions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Individual Contribution&lt;/strong&gt;: Traditional feature development and bug fixes. This is what Jira measures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Team Multiplication&lt;/strong&gt;: Code reviews, mentoring, infrastructure work, and knowledge sharing. This is what tools like Span measure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-Team Impact&lt;/strong&gt;: Work that helps other teams or the company, even if it doesn't show up in your team's metrics.&lt;/p&gt;

&lt;p&gt;Weight all three dimensions equally. An engineer who excels at team multiplication but has lower individual contribution numbers can still get an "exceeds expectations" rating.&lt;/p&gt;

&lt;p&gt;Also change how you assign work. When planning sprints, explicitly allocate time for glue work. If an engineer is going to spend 30% of their time on code reviews, plan for them to close 30% fewer tickets. This prevents the trap where engineers get overloaded with both feature work and glue work, then get dinged for not completing enough tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long-Term Impact
&lt;/h2&gt;

&lt;p&gt;Six months after implementing these changes, the results are clear.&lt;/p&gt;

&lt;p&gt;Team velocity increases by 30%+. Not because people work more hours, but because the glue work gets distributed more evenly. Junior engineers close more tickets because they get unblocked faster. Mid-level engineers contribute to code reviews and documentation. Senior engineers still do the most glue work, but they're not penalized for it.&lt;/p&gt;

&lt;p&gt;Retention improves. Engineers who were considering leaving because they felt like their contributions weren't valued become your strongest advocates and refer excellent engineers to your team.&lt;/p&gt;

&lt;p&gt;Hiring gets easier. When candidates ask how you evaluate performance, you can show them the actual data on how you measure and reward different types of contributions. Engineers who care about doing high-quality work are attracted to teams that recognize all forms of contribution, not just ticket velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Should Do Tomorrow
&lt;/h2&gt;

&lt;p&gt;Start by identifying who on your team is doing the most glue work. Pull up your GitHub data and look at PR review patterns. Talk to your junior engineers about who they go to when they're stuck.&lt;/p&gt;

&lt;p&gt;Then look at how you're measuring performance. If you're only looking at ticket velocity, you're missing half the picture. Find ways to quantify the other half, whether that's through manual data gathering or tools like Span.&lt;/p&gt;

&lt;p&gt;Finally, have explicit conversations with your team about glue work. Tell them you value it. Show them you're measuring it. Make it clear that doing glue work won't hurt their career trajectory.&lt;/p&gt;

&lt;p&gt;Glue work is career-making if it's visible. It's career-ending if it stays invisible. As a manager, making it visible is your job, not your engineers'.&lt;/p&gt;

&lt;p&gt;The engineers who succeed at senior levels aren't the ones who close the most tickets. They're the ones who multiply their team's effectiveness. Code review, mentoring, infrastructure work, and technical guidance are how they create that leverage.&lt;/p&gt;

&lt;p&gt;But leverage without measurement looks like low productivity. You need systems that capture the full picture of contributions, not just the subset that maps cleanly to ticket boards.&lt;/p&gt;

&lt;p&gt;If you have engineers spending time on glue work, make sure it's being captured and credited. They shouldn't have to choose between doing high-impact work and having a successful performance review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you measure glue work on your team?&lt;/strong&gt; I'd love to hear what's worked (or hasn't worked) for other engineering managers. Drop a comment below.&lt;/p&gt;

</description>
      <category>career</category>
      <category>leadership</category>
      <category>management</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>10 Proven Ways to Improve Developer Productivity (Without Burning Out Your Team)</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Tue, 13 Jan 2026 14:49:12 +0000</pubDate>
      <link>https://dev.to/luciench/10-proven-ways-to-improve-developer-productivity-without-burning-out-your-team-8ak</link>
      <guid>https://dev.to/luciench/10-proven-ways-to-improve-developer-productivity-without-burning-out-your-team-8ak</guid>
      <description>&lt;p&gt;Engineering leaders face relentless pressure to ship faster. The tempting response is to push harder, add hours, and demand more. But this approach fails. The &lt;a href="https://dora.dev/publications/" rel="noopener noreferrer"&gt;2025 DORA State of AI-Assisted Software Development report&lt;/a&gt;, surveying nearly 5,000 technology professionals, reveals that &lt;a href="https://blog.google/technology/developers/dora-report-2025/" rel="noopener noreferrer"&gt;90% now use AI tools at work&lt;/a&gt;, spending a median of two hours daily with them. Yet only 24% trust the output. More telling: extended crunch periods create an almost perfect correlation between overtime and defect rates.&lt;/p&gt;

&lt;p&gt;The path to sustainable velocity isn't raw effort. It's removing friction, protecting focus, and building systems that let your engineers do their best work. Here are ten strategies backed by recent research that actually move the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Protect focus time by clustering interruptions
&lt;/h2&gt;

&lt;p&gt;The cost of context switching is staggering. Dr. Gloria Mark's research at UC Irvine established that workers need an &lt;a href="https://briq.com/blog/the-23-minute-problem#:~:text=The%2023%2DMinute%20Reality,psychologists%20call%20%22attention%20residue.%22" rel="noopener noreferrer"&gt;average of 23 minutes and 15 seconds to fully refocus&lt;/a&gt; after an interruption. Her 2023 book &lt;em&gt;Attention Span&lt;/em&gt; updated this to roughly 25 minutes and revealed something worse: the average time spent on any screen before switching has dropped to just 47 seconds.&lt;/p&gt;

&lt;p&gt;For developers, the damage compounds. GitHub's Good Day Project found that developers have an &lt;a href="https://github.blog/2021-05-25-octoverse-spotlight-good-day-project/" rel="noopener noreferrer"&gt;82% chance of having a good day&lt;/a&gt; with minimal interruptions, but only a 7% chance when interrupted frequently. Paul Graham captured this perfectly in his &lt;a href="https://paulgraham.com/makersschedule.html" rel="noopener noreferrer"&gt;2009 essay on the maker's schedule&lt;/a&gt;: "A single meeting can blow a whole afternoon, by breaking it into two pieces each too small to do anything hard in."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Implement no-meeting days or cluster all meetings into specific blocks. A 2022 MIT Sloan study of 76 companies found that one meeting-free day per week &lt;a href="https://www.superpowers.school/p/research-into-meeting-free-days" rel="noopener noreferrer"&gt;increased productivity by 35%&lt;/a&gt;. Two meeting-free days pushed that to 71%. The optimal balance was three no-meeting days per week, protecting 60% of the work week for focused work.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cut CI/CD wait times to eliminate forced context switches
&lt;/h2&gt;

&lt;p&gt;When a build takes 20 minutes, developers switch to something else. Then they need another 25 minutes to regain context when the build completes. That single build just cost 45 minutes of cognitive overhead.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://dora.dev/publications/" rel="noopener noreferrer"&gt;2025 DORA report&lt;/a&gt; shows that &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report" rel="noopener noreferrer"&gt;AI adoption increased throughput but also instability&lt;/a&gt;, with teams shipping faster but experiencing higher change failure rates when they lack robust control systems. The difference isn't just speed, it's that fast feedback loops let developers stay in flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Invest in incremental builds, parallelized test suites, and intelligent test impact analysis. &lt;a href="https://www.launchableinc.com/use-case/in-place-shortening/" rel="noopener noreferrer"&gt;AI-driven test selection can reduce CI run times by 40-80%&lt;/a&gt; by running only tests affected by recent changes. &lt;a href="https://agilealliance.org/glossary/xp/" rel="noopener noreferrer"&gt;Kent Beck's original 10-minute rule from &lt;em&gt;Extreme Programming Explained&lt;/em&gt;&lt;/a&gt;: "Automatically build the whole system and run all of the tests in ten minutes. A build that takes longer than ten minutes will be used much less often, missing the opportunity for feedback." Every minute beyond that threshold increases the likelihood of costly context switches.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat internal documentation as a product
&lt;/h2&gt;

&lt;p&gt;The 2024 Stack Overflow Developer Survey found that &lt;a href="https://survey.stackoverflow.co/2024/professional-developers" rel="noopener noreferrer"&gt;61% of developers spend more than 30 minutes daily&lt;/a&gt; searching for answers, with 26% spending over an hour. That's one entire workday per week burned on hunting for answers that should be documented.&lt;/p&gt;

&lt;p&gt;The impact on teams is severe. New hires take two to three months longer to become productive when documentation is poor. The 2024 Stack Overflow survey also found that while 70% of developers know where to find answers, only 56% can find them quickly, and 53% report that waiting on answers disrupts their workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Allocate dedicated time for documentation maintenance. Treat your internal wikis like a product with owners, roadmaps, and quality standards. Organizations with poor documentation waste hundreds of hours weekly for mid-sized engineering teams, equivalent to losing multiple full-time engineers to information hunting.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deploy AI coding tools, but measure what matters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://techcrunch.com/2025/07/30/github-copilot-crosses-20-million-all-time-users/" rel="noopener noreferrer"&gt;GitHub Copilot has crossed 20 million users&lt;/a&gt;, with 90% of Fortune 100 companies now using it. GitHub's controlled study showed developers &lt;a href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/" rel="noopener noreferrer"&gt;completed tasks 55% faster&lt;/a&gt; with Copilot, and &lt;a href="https://github.com/customer-stories/duolingo" rel="noopener noreferrer"&gt;Duolingo's engineering team estimated 25% speed increases&lt;/a&gt; for engineers new to a codebase.&lt;/p&gt;

&lt;p&gt;But the data isn't universally positive. A December 2025 CodeRabbit analysis of 470 open-source PRs found AI-generated code &lt;a href="https://www.coderabbit.ai/blog/state-of-ai-vs-human-code-generation-report" rel="noopener noreferrer"&gt;produces 1.7x more issues&lt;/a&gt; than human-written code, including 1.4x more critical bugs. The 2024 DORA report noted that while AI adoption increased &lt;a href="https://swimm.io/blog/heres-what-the-2024-dora-report-has-to-say-about-code-documentation" rel="noopener noreferrer"&gt;documentation quality by 7.5%&lt;/a&gt; and &lt;a href="https://www.hashicorp.com/en/blog/ai-is-making-developers-faster-but-at-a-cost" rel="noopener noreferrer"&gt;code review speed by 3.1%&lt;/a&gt;, it was also associated with higher instability. Meanwhile, &lt;a href="https://survey.stackoverflow.co/2025/ai" rel="noopener noreferrer"&gt;only about 33% of developers trust AI code accuracy&lt;/a&gt; according to the 2025 Stack Overflow survey, with 46% actively distrusting AI-generated code.&lt;/p&gt;

&lt;p&gt;The real risk is "AI sprawl," where teams adopt multiple tools without understanding impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Roll out AI assistants to reduce boilerplate and repetitive tasks, where they excel. But measure outcomes, not just adoption. Platforms like &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt; help leaders see if AI is reducing coding time or just increasing review time, allowing you to optimize the toolchain without guessing. &lt;a href="https://www.span.app/blog/introducing-span-detect-1" rel="noopener noreferrer"&gt;Span's AI code detector&lt;/a&gt; can identify AI-generated code with &lt;a href="https://www.span.app/blog/introducing-span-detect-1" rel="noopener noreferrer"&gt;95% accuracy&lt;/a&gt;, giving you ground truth on adoption and impact. The goal is reduced toil, not just more code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Standardize the inner development loop
&lt;/h2&gt;

&lt;p&gt;"It works on my machine" remains one of the most expensive phrases in engineering. Each developer maintaining their own unique environment creates configuration drift, onboarding delays, and debugging nightmares.&lt;/p&gt;

&lt;p&gt;Cloud development environments (CDEs) and dev containers eliminate this friction. Teams using mature internal developer platforms report significant reductions in cognitive load and faster onboarding. Spotify's platform engineering team found a &lt;a href="https://backstage.io/blog/2020/03/16/announcing-backstage/" rel="noopener noreferrer"&gt;55% improvement in time-to-tenth-pull-request&lt;/a&gt; for new developers using standardized environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Standardize development environments using containers, devcontainers, or cloud-based development tools. Define the inner loop (edit, build, test, debug) as a first-class product. When any developer can clone a repo and start contributing within hours instead of days, you've removed one of the most persistent friction points in engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Set explicit code review SLAs
&lt;/h2&gt;

&lt;p&gt;Code sitting in review is inventory on a shelf. It's not delivering value, it's accumulating merge conflicts, and the author has already context-switched away. Meta's engineering research found a &lt;a href="https://engineering.fb.com/2022/11/16/culture/meta-code-review-time-improving/" rel="noopener noreferrer"&gt;correlation between time-in-review and engineer dissatisfaction&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;High-performing teams treat review latency as a key metric. Google's engineering practices mandate &lt;a href="https://google.github.io/eng-practices/review/reviewer/speed.html" rel="noopener noreferrer"&gt;responding to code reviews within one business day maximum&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Establish team SLAs for review turnaround. A reasonable target: first review within four hours for internal team PRs, full cycle time under 24 hours. Keep PRs small (under 400 lines) for higher defect discovery rates. Research shows that &lt;a href="https://arxiv.org/abs/2203.05048" rel="noopener noreferrer"&gt;reducing the time between acceptance and merge can improve code velocity by up to 63%&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Create on-call rotations to protect maker time
&lt;/h2&gt;

&lt;p&gt;The "tap on the shoulder" through Slack is insidious. It feels harmless, but it fragments focus and distributes interruptions unpredictably across the team. Shadow work, the unplanned requests and ad-hoc support that never appears on a sprint board, silently drains capacity. When everyone is "available," no one is protected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Implement explicit on-call rotations where designated engineers handle interruptions while others focus. During on-call workdays, engineers do only on-call work with no feature development expected. If your on-call engineers regularly handle &lt;a href="https://sre.google/sre-book/being-on-call/" rel="noopener noreferrer"&gt;more than 2 incidents per 12-hour shift&lt;/a&gt;, Google SRE data suggests your system has reliability problems that need addressing - either through better automation, improved monitoring, or expanding the rotation. This protects the rest of the team's time while ensuring responsive support.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Build self-service infrastructure through platform engineering
&lt;/h2&gt;

&lt;p&gt;Filing tickets for AWS access, waiting for DevOps to configure a database, or requesting permission to deploy are all symptoms of underdeveloped platform capabilities. Each handoff adds days of latency and forces developers into waiting mode.&lt;/p&gt;

&lt;p&gt;Gartner predicts that by 2026, &lt;a href="https://www.beinformed.com/gartners-top-10-tech-trends-2024-embracing-platform-engineering/" rel="noopener noreferrer"&gt;80% of large software engineering organizations will have platform engineering teams&lt;/a&gt;, up from 45% in 2022. A 2024 survey of Kubernetes experts found &lt;a href="https://www.cncf.io/blog/2024/06/06/the-voice-of-kubernetes-experts-report-2024-the-data-trends-driving-the-future-of-the-enterprise/" rel="noopener noreferrer"&gt;96% of organizations&lt;/a&gt; already have a platform engineering function, and the &lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener noreferrer"&gt;2024 DORA report showed teams using internal developer platforms saw 10% increases in team performance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Build "golden paths" for common workflows: deploying a new service, provisioning a database, setting up monitoring. &lt;a href="https://engineering.atspotify.com/2020/08/how-we-use-golden-paths-to-solve-fragmentation-in-our-software-ecosystem" rel="noopener noreferrer"&gt;Spotify pioneered this approach&lt;/a&gt;, creating opinionated, well-documented pathways that reduce cognitive load while still allowing teams to deviate when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Measure systems and processes, not individual output
&lt;/h2&gt;

&lt;p&gt;Stack-ranking developers by commits or lines of code pushes them to game the numbers instead of doing good work, and it destroys psychological safety. The 2024 Stack Overflow survey found that &lt;a href="https://survey.stackoverflow.co/2024/professional-developers#3-satisfied-at-current-job" rel="noopener noreferrer"&gt;only 20% of developers report being happy at work&lt;/a&gt;, with &lt;a href="https://survey.stackoverflow.co/2024/professional-developers#2-most-common-frustrations" rel="noopener noreferrer"&gt;62% citing technical debt&lt;/a&gt; as their top frustration. The DORA research team explicitly warns against using their metrics for individual evaluation—these metrics exist to identify systemic bottlenecks, not to rank people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Use metrics to debug your engineering system, not your engineers. Look for process flaws: calendar fragmentation, review bottlenecks, deployment friction, documentation gaps. &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span's platform&lt;/a&gt; is valuable here because it highlights systemic issues, like calendars full of fragmented time, rather than simply counting lines of code. This preserves psychological safety while surfacing real obstacles.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Regularly survey developers and act on feedback
&lt;/h2&gt;

&lt;p&gt;Happy developers write better code. The DORA Accelerate research found that high-performing teams are &lt;a href="https://dustinewers.com/accelerate-book-summary/" rel="noopener noreferrer"&gt;2x as likely to exceed organizational performance goals&lt;/a&gt;. Google's 2022 research established that &lt;a href="https://research.google/pubs/what-improves-developer-productivity-at-google-code-quality/" rel="noopener noreferrer"&gt;perceived code quality causally increases productivity&lt;/a&gt;. Yet most organizations never systematically ask their engineers what's getting in the way.&lt;/p&gt;

&lt;p&gt;Productivity cannot be reduced to a single metric. You need satisfaction data alongside performance data. And you need to actually fix what developers say is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Run quarterly developer experience surveys. Ask what sucks. Then visibly address the top pain points. The investment pays compound returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with two changes this quarter
&lt;/h2&gt;

&lt;p&gt;You don't need to implement all ten strategies at once. Pick two that address your team's biggest friction points. If your calendar is a disaster, start with meeting-free days. If code rots in review, set explicit SLAs. If developers waste hours searching for answers, invest in documentation.&lt;/p&gt;

&lt;p&gt;The goal isn't maximum velocity for one sprint. It's sustainable pace that your team can maintain indefinitely. The &lt;a href="https://dora.dev/publications/" rel="noopener noreferrer"&gt;2025 DORA data&lt;/a&gt; is clear: &lt;a href="https://www.splunk.com/en_us/blog/learn/state-of-devops.html" rel="noopener noreferrer"&gt;AI functions as an amplifier&lt;/a&gt;, magnifying the strengths of high-performing teams and the dysfunctions of struggling ones. Teams that prioritize stability and developer well-being outperform those that sacrifice them for short-term speed. Build the system that lets your engineers do their best work, and the velocity will follow.&lt;/p&gt;

</description>
      <category>management</category>
      <category>mentalhealth</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The 7 Best Developer Portals for Enterprise Teams in 2025</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Thu, 08 Jan 2026 09:07:29 +0000</pubDate>
      <link>https://dev.to/luciench/the-7-best-developer-portals-for-enterprise-teams-in-2025-4kgj</link>
      <guid>https://dev.to/luciench/the-7-best-developer-portals-for-enterprise-teams-in-2025-4kgj</guid>
      <description>&lt;p&gt;Your platform team spent a year and $2 million building an internal developer portal. Or maybe you bought one off the shelf. Either way, you're probably wondering if you made the right choice.&lt;/p&gt;

&lt;p&gt;Here's what I've learned after working with dozens of companies implementing internal developer portals: the "build versus buy" decision isn't really the question anymore. The market has changed completely since 2020. Companies that went with self-hosted &lt;a href="https://roadie.io/backstage-spotify/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt; discovered that "free and open-source" actually means paying for 3-12 full-time engineers. Organizations that bought proprietary platforms found themselves locked into data models they can't escape. And the ones who picked wrong are now facing painful migrations.&lt;/p&gt;

&lt;p&gt;I've spent the last few years talking to engineering leaders about their IDP implementations, and I've noticed the landscape has evolved into three distinct approaches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build&lt;/strong&gt;: Self-hosted Backstage installations that give you maximum flexibility but cost over $1M per year to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Buy&lt;/strong&gt;: Proprietary SaaS platforms like Cortex and Port with polished interfaces but permanent vendor lock-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid&lt;/strong&gt;: Managed Backstage solutions that give you the open-source ecosystem without the engineering overhead.&lt;/p&gt;

&lt;p&gt;In this guide, I'll walk through the seven platforms that enterprise teams are actually using at scale. I'm focusing on the strategic trade-offs that matter three years after your initial decision, when you'll discover whether you made the right choice or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Matters in an Enterprise Developer Portal
&lt;/h2&gt;

&lt;p&gt;Before I dive into specific platforms, let me share what I've learned matters most when evaluating IDPs. A lot of these platforms look great in demos but fall apart in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ecosystem and Extensibility
&lt;/h3&gt;

&lt;p&gt;Can you integrate with your entire toolchain, or are you stuck with whatever the vendor decides to support? At enterprise scale, you're probably using 50+ different tools across CI/CD, monitoring, security, and cloud infrastructure. The difference between supporting 20 integrations versus 250+ becomes critical when half your value comes from centralizing everything in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vendor Lock-In Risk
&lt;/h3&gt;

&lt;p&gt;What happens to your data if you need to switch platforms in two years? Open-source-based solutions like &lt;a href="https://roadie.io/backstage-spotify/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt; use standardized YAML entity definitions you can export and migrate anywhere. Proprietary platforms often use custom data models that trap all your organizational knowledge inside their systems forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance Overhead
&lt;/h3&gt;

&lt;p&gt;Does running the platform require a dedicated team, or can your existing platform engineers manage it alongside their other work? I've seen self-hosted solutions consume 3-5 full-time engineers just for maintenance, upgrades, and troubleshooting. This is what I call the "TypeScript tax," the hidden cost of maintaining frontend infrastructure that most DevOps teams aren't equipped to handle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise Readiness
&lt;/h3&gt;

&lt;p&gt;Does the platform provide &lt;a href="https://roadie.io/product/access-control/" rel="noopener noreferrer"&gt;role-based access control&lt;/a&gt; (RBAC), single sign-on (SSO), and SOC2 compliance out of the box? For regulated industries or companies with strict security requirements, these aren't nice-to-haves. They're table stakes. Building them yourself in a self-hosted environment can take months.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day 2 Operations
&lt;/h3&gt;

&lt;p&gt;Look beyond the initial setup. How difficult is it to upgrade when breaking changes occur? How do you handle search infrastructure at scale? What happens when you need to migrate to a new backend system? The platforms that look easiest on day one often become maintenance nightmares by day 700.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Roadie: Managed Backstage for Teams Who Value Their Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Hybrid (Managed Backstage)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Teams that want Backstage's ecosystem without the operational burden&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxau0dqvpy9e7w1d7y53j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxau0dqvpy9e7w1d7y53j.png" alt=" " width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://roadie.io/" rel="noopener noreferrer"&gt;Roadie&lt;/a&gt; delivers the full power of Spotify's open-source &lt;a href="https://roadie.io/backstage-spotify/" rel="noopener noreferrer"&gt;Backstage platform&lt;/a&gt; as a managed SaaS service. This is what I call the hybrid approach. You get access to the entire Backstage ecosystem with 211 &lt;a href="https://roadie.io/backstage/plugins/" rel="noopener noreferrer"&gt;open-source plugins&lt;/a&gt;, standardized data models, and active community development. But you don't need to maintain any of the infrastructure.&lt;/p&gt;

&lt;p&gt;The platform handles everything you'd normally spend engineering time on: hosting, security patches, database management, enterprise-grade search, and those painful upgrades like the &lt;a href="https://roadie.io/blog/migrating-to-backstages-new-backend-a-step-by-step-guide/" rel="noopener noreferrer"&gt;New Backend System migration&lt;/a&gt; that challenged self-hosted teams throughout 2024. Your team focuses on configuring integrations and building workflows, not debugging TypeScript or updating React components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal-maintenance Backstage platform with automated upgrades&lt;/li&gt;
&lt;li&gt;Access to entire open-source plugin ecosystem (211 plugins, 82 supported out-of-the-box)&lt;/li&gt;
&lt;li&gt;Built-in &lt;a href="https://roadie.io/product/tech-insights/" rel="noopener noreferrer"&gt;Tech Insights&lt;/a&gt; for scorecards and engineering standards (paid add-on)&lt;/li&gt;
&lt;li&gt;Enterprise &lt;a href="https://roadie.io/product/access-control/" rel="noopener noreferrer"&gt;RBAC&lt;/a&gt; (basic RBAC in Teams plan, custom RBAC in Growth plan)&lt;/li&gt;
&lt;li&gt;No vendor lock-in since the data model is standard Backstage YAML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The biggest advantage is what I call avoiding the TypeScript tax. Platform engineering teams can focus on building platform capabilities instead of &lt;a href="https://roadie.io/blog/backstage-how-much-does-it-really-cost/" rel="noopener noreferrer"&gt;maintaining TypeScript and React frontends&lt;/a&gt;. Any &lt;a href="https://roadie.io/backstage/plugins/" rel="noopener noreferrer"&gt;Backstage plugin&lt;/a&gt; works, including community-developed integrations. Complex upgrades like the New Backend System transition happen automatically. Most customers &lt;a href="https://roadie.io/blog/from-day-0-to-day-2-a-guide-to-planning-and-implementing-backstage/" rel="noopener noreferrer"&gt;see value within weeks&lt;/a&gt;, not months. Your &lt;a href="https://roadie.io/docs/catalog/modeling-entities/" rel="noopener noreferrer"&gt;catalog definitions&lt;/a&gt; are portable YAML files, not proprietary formats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;You're working within Backstage's UI constraints, which means less drag-and-drop flexibility compared to tools like Port. It's more structured, which some teams see as a feature and others see as a limitation. While Roadie offers secure connectivity options like the &lt;a href="https://roadie.io/docs/integrations/broker/" rel="noopener noreferrer"&gt;Roadie Broker&lt;/a&gt; for on-premises resources, it's primarily a hosted service. If your security team absolutely requires on-premises deployment, self-hosted Backstage might be your only option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Teams plan starts at $24 per developer per month with a 50-seat minimum (50-150 developers). Growth plan pricing is custom with a 100-seat minimum (100+ developers). Only active contributors to your source control management incur costs. Non-coding team members like product managers and leadership can access for free. Tech Insights is an optional paid add-on. &lt;a href="https://roadie.io/pricing/" rel="noopener noreferrer"&gt;View pricing details&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cortex: When You're Obsessed with Scorecards
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Proprietary SaaS&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Organizations focused on service maturity scorecards and reliability metrics&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0rwp728njlishb7mono.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0rwp728njlishb7mono.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cortex.io/" rel="noopener noreferrer"&gt;Cortex&lt;/a&gt; built a polished, opinionated developer portal that's heavily focused on measuring and improving service quality through scorecards. The platform excels at gamifying service ownership with detailed maturity models, SLO tracking, and automated scoring based on engineering best practices using Bronze/Silver/Gold levels.&lt;/p&gt;

&lt;p&gt;The UI feels modern and intuitive, especially if your team is already familiar with SaaS tools like Datadog or PagerDuty. Cortex's scorecard system is more sophisticated than most alternatives, offering fine-grained control over scoring criteria with flexible rule definitions and excellent visualization of how your services stack up against engineering standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced scorecard system with customizable rubrics using Bronze/Silver/Gold levels&lt;/li&gt;
&lt;li&gt;Strong reliability engineering focus with SLOs, incidents, and on-call tracking&lt;/li&gt;
&lt;li&gt;Polished, modern UI optimized for service discovery&lt;/li&gt;
&lt;li&gt;AI-powered features including Ownership Prediction and Velocity Dashboard for DORA metrics&lt;/li&gt;
&lt;li&gt;60+ out-of-the-box integrations with major monitoring and development tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The scorecard sophistication is genuinely best-in-class. If you want to track service maturity with Bronze/Silver/Gold level visualization and detailed point-based scoring, nothing else comes close. The UI is beautiful and impresses stakeholders. The reliability focus is excellent for teams prioritizing SRE practices. You can get a basic catalog running quickly. New AI features for ownership prediction and metrics analysis are interesting additions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Cortex is known for being expensive at enterprise scale, especially compared to Backstage-based alternatives. The data model is Cortex-specific, making migration difficult if you ever want to leave. You're limited to whatever integrations Cortex builds. You can't leverage community plugins like you can with Backstage. The template and workflow capabilities lag behind &lt;a href="https://roadie.io/product/scaffolder/" rel="noopener noreferrer"&gt;Backstage's Software Templates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Not publicly disclosed. You need to sign up for a demo to get pricing information. However, the &lt;a href="https://tei.forrester.com/go/Cortex/IDP/?lang=en-us" rel="noopener noreferrer"&gt;Forrester Total Economic Impact study&lt;/a&gt; from July 2024 lists pricing at approximately $65 per user per month at scale. Multiple tiers available (Engineering Intelligence, Accelerate, Full IDP, Site License) with features scaling from basic catalog and scorecards to full platform capabilities. &lt;a href="https://www.cortex.io/pricing" rel="noopener noreferrer"&gt;Request pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Port: The Builder's Platform
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Proprietary SaaS&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Teams that need to model non-standard assets or want maximum UI customization&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxolocxfjdy3adgu2sfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxolocxfjdy3adgu2sfx.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.port.io/" rel="noopener noreferrer"&gt;Port&lt;/a&gt; takes a completely different approach. Instead of giving you an opinionated developer portal, it gives you building blocks to create your own. The platform's no-code interface lets you define custom data models (called Blueprints) for any asset type, not just services and APIs, but environments, IoT devices, or cloud resources.&lt;/p&gt;

&lt;p&gt;This flexibility makes Port uniquely suited for organizations with complex, non-standard infrastructure that doesn't fit typical service catalog patterns. You can build custom views, define relationships between any entity types, and create workflows that match your exact processes. Port recently rebranded as an "Agentic Internal Developer Portal" with enhanced AI capabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully customizable data models and UI views via Blueprints&lt;/li&gt;
&lt;li&gt;No-code interface for defining entities and relationships&lt;/li&gt;
&lt;li&gt;Strong visualization capabilities for complex systems&lt;/li&gt;
&lt;li&gt;Self-service actions using Cookiecutter templates&lt;/li&gt;
&lt;li&gt;50+ integrations including DORA metrics tracking&lt;/li&gt;
&lt;li&gt;AI agent capabilities and Engineering360 dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Port gives you ultimate flexibility to model anything your organization needs, not just traditional services. You can build exactly the interface your teams need. It's excellent for multi-cloud, hybrid environments with diverse asset types. The visual workflow builder lets you create automation without writing code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Maximum flexibility means you're building everything from scratch. There's less out-of-the-box value compared to opinionated platforms. You can't leverage the Backstage open-source plugin ecosystem, though the Ocean Framework provides extensibility through data integrations and workflow automation. While Port supports Markdown, it lacks the full &lt;a href="https://roadie.io/docs/getting-started/technical-documentation/" rel="noopener noreferrer"&gt;TechDocs&lt;/a&gt; build pipeline and search capabilities you get with Backstage. Teams need time to master the data modeling concepts, which means a steeper learning curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free tier available (up to 15 seats, 10,000 entities). Startup tier at $30 per developer per month. Enterprise tier available with premium features including SSO, advanced RBAC, ISO 27001 and SOC2 Type 2 certifications, and dedicated support. &lt;a href="https://www.port.io/pricing" rel="noopener noreferrer"&gt;View pricing details&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. OpsLevel: Fast Setup, Limited Growth
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Proprietary SaaS&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Teams focused primarily on service ownership and maturity tracking&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmizajyqu8ncvk49llzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmizajyqu8ncvk49llzl.png" alt=" " width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.opslevel.com/" rel="noopener noreferrer"&gt;OpsLevel&lt;/a&gt; started as a service maturity and ownership tool before evolving into a broader developer portal. This shows in its excellent service ownership features and straightforward approach to tracking engineering standards through its Rubric system with Bronze/Silver/Gold levels, plus separate Scorecards for team-specific standards.&lt;/p&gt;

&lt;p&gt;The platform offers the fastest time-to-initial-value for basic service cataloging. Typical deployments complete in 30-45 days. You can have a working catalog with ownership information and basic checks running within hours. But this simplicity comes at a cost. OpsLevel's feature set is more constrained than platforms built on extensible architectures. Recent additions include AI-powered features for check generation and catalog enrichment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast setup for basic service catalog (typical 30-45 day deployment)&lt;/li&gt;
&lt;li&gt;Strong focus on service ownership and maturity rubrics&lt;/li&gt;
&lt;li&gt;Good integration with CI/CD systems for automated checks (60+ integrations)&lt;/li&gt;
&lt;li&gt;AI-generated checks and AI-enriched catalog&lt;/li&gt;
&lt;li&gt;Package version inventories for SBOM visibility&lt;/li&gt;
&lt;li&gt;Clean, straightforward UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;You can get a basic catalog running faster than with any alternative. The service ownership focus is excellent for clarifying who owns what. Good built-in templates for measuring service quality. The interface is less complex than more feature-rich platforms, which some teams prefer. New AI features streamline catalog management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;You can't easily add community plugins or build custom integrations. The UI is less flexible than Port or Backstage for customization. The feature set is primarily focused on cataloging and checks, with less emphasis on documentation or scaffolding. The migration path is unclear if you outgrow the platform since it uses a proprietary data model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Not publicly disclosed. Pricing is based on team size with custom quotes. Per-developer pricing model with volume discounts available. Includes SOC2 Type 2 compliance and SAML-based SSO. Pricing customizable based on needs including self-hosted options and support levels. &lt;a href="https://www.opslevel.com/pricing" rel="noopener noreferrer"&gt;Request pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Atlassian Compass: The Jira Extension
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Proprietary SaaS&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Organizations deeply invested in the Atlassian ecosystem&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fec5g748ru6tp1dk2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fec5g748ru6tp1dk2t.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your company runs on Jira, Bitbucket, and Confluence, &lt;a href="https://www.atlassian.com/software/compass" rel="noopener noreferrer"&gt;Compass&lt;/a&gt; offers the most seamless native integration you'll find. The platform leverages Atlassian's identity system, pulls in data from other Atlassian products automatically, and feels like a natural extension of your existing toolchain.&lt;/p&gt;

&lt;p&gt;Compass provides automated service health monitoring, tracking metrics from integrated tools and surfacing problems before they escalate. For teams already paying for Atlassian products, Compass represents an incremental cost with minimal integration effort. The platform has scorecards with a new Maturity Levels feature added in 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important Note&lt;/strong&gt;: Atlassian deprecated the Templates and scaffolding feature on December 1, 2025. This is a significant capability reduction for teams requiring self-service service creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native integration with Jira, Bitbucket, Confluence, and Opsgenie&lt;/li&gt;
&lt;li&gt;Automated service health monitoring&lt;/li&gt;
&lt;li&gt;Component tracking with Atlassian-native data models&lt;/li&gt;
&lt;li&gt;Integrated incident management through Opsgenie&lt;/li&gt;
&lt;li&gt;Scorecards with Maturity Levels feature&lt;/li&gt;
&lt;li&gt;Built on Atlassian Forge with GraphQL APIs for extensibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The Atlassian integration is unbeatable if you use Jira for everything. The UX feels consistent with other Atlassian products, so there's minimal learning curve. Automated health monitoring works well. You can get it as a standalone product or bundled with some enterprise packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;This is what I call the Atlassian trap. The platform struggles with non-Atlassian tools like &lt;a href="https://roadie.io/docs/integrations/github-discovery/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://roadie.io/docs/integrations/gitlab/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;, or CircleCI. The scaffolding feature was removed on December 1, 2025, so it's no longer available for service creation. You can't extend it with community plugins since it uses a proprietary ecosystem. It's more of a service catalog with add-ons than a complete platform interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free tier available (3 full users, unlimited basic users). Standard tier at $8 per user per month includes basic features. Premium tier at $25 per user per month includes IP Allowlisting, advanced integrations, 99.9% uptime SLA, and premium support. Discounted rates available for teams above 101 users. Compass is a standalone product with separate billing from other Atlassian tools. &lt;a href="https://www.atlassian.com/software/compass/pricing" rel="noopener noreferrer"&gt;View pricing details&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Backstage: The Industry Standard (Self-Hosted)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Open Source (Self-Hosted)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Large enterprises with dedicated platform engineering teams and specific compliance requirements&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxnfo0m06hmp5mh6086y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxnfo0m06hmp5mh6086y.png" alt=" " width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Spotify's &lt;a href="https://backstage.io/" rel="noopener noreferrer"&gt;Backstage&lt;/a&gt; is the industry-standard open-source developer portal framework. It powers IDPs at Spotify, American Airlines, Pinterest, and thousands of other organizations. You own the code, control the infrastructure, and can customize anything.&lt;/p&gt;

&lt;p&gt;But this flexibility comes with significant operational costs. Self-hosting Backstage requires 3-5 dedicated engineers to manage infrastructure, handle upgrades, maintain search systems, and keep up with the rapidly evolving codebase. Roadie's survey of the Backstage community found that successful self-hosted deployments had at least three engineers dedicated full-time. Some companies have teams of 12 people just for Backstage.&lt;/p&gt;

&lt;p&gt;Breaking changes occur regularly with monthly releases. Major migrations like the &lt;a href="https://roadie.io/blog/migrating-to-backstages-new-backend-a-step-by-step-guide/" rel="noopener noreferrer"&gt;New Backend System&lt;/a&gt; transition that completed in 2024 consumed months of engineering time for self-hosted teams. For perspective on the true cost, &lt;a href="https://roadie.io/blog/backstage-how-much-does-it-really-cost/" rel="noopener noreferrer"&gt;Zalando invested over $4 million&lt;/a&gt; across four years developing their internal platform before open-sourcing their work as part of Backstage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully open-source with Apache 2.0 license&lt;/li&gt;
&lt;li&gt;250+ &lt;a href="https://roadie.io/backstage/plugins/" rel="noopener noreferrer"&gt;community plugins&lt;/a&gt; covering every major tool&lt;/li&gt;
&lt;li&gt;Extensible architecture for &lt;a href="https://roadie.io/docs/custom-plugins/overview/" rel="noopener noreferrer"&gt;custom plugins&lt;/a&gt; and integrations&lt;/li&gt;
&lt;li&gt;Active community and regular monthly releases&lt;/li&gt;
&lt;li&gt;CNCF Incubating project with strong enterprise adoption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;You have ultimate control and can customize without limits. There are no license costs since it's free to download and use. The ecosystem is massive with the largest community and plugin library (250+ plugins). You have no vendor dependency and can run it anywhere, modify anything. It's the industry standard backed by CNCF and major enterprises.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The operational overhead is substantial. Successful deployments require 3-5 full-time engineers minimum. Some teams reach 12 full-time employees just maintaining Backstage. Most DevOps teams lack frontend skills for React and TypeScript customization, which is what I call the TypeScript tax. Monthly breaking changes and major migrations like the New Backend System require significant engineering investment.&lt;/p&gt;

&lt;p&gt;You manage databases, search infrastructure with Elasticsearch, monitoring, and security patches. The hidden costs are enormous. At typical senior platform engineer compensation of $250K per year fully loaded, 3-5 engineers cost $750K to $1.25M annually, plus infrastructure costs of $12K to $24K per year. Total cost of ownership typically exceeds $2M+ over three years when you factor in engineering time, opportunity cost, and infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (open source under Apache 2.0 license). However, total cost of ownership includes 3-5 engineer salaries ($750K to $1.25M+ annually) plus infrastructure costs ($12K to $24K+ annually). TCO typically exceeds $2M+ over three years.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Configure8: The Discovery Platform
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Category&lt;/strong&gt;: Proprietary SaaS&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best For&lt;/strong&gt;: Organizations prioritizing discovery and cost analytics&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4q06so63k3qz1jpfm3l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy4q06so63k3qz1jpfm3l.png" alt=" " width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://configure8-io.webflow.io/" rel="noopener noreferrer"&gt;Configure8&lt;/a&gt; positions itself as a universal catalog that can ingest and relate data from virtually any source. The platform emphasizes discovery features, helping teams understand what they have and how it's interconnected. It also offers strong cloud cost integration, surfacing spending data alongside technical resources.&lt;/p&gt;

&lt;p&gt;While Configure8 has solid core features including 30+ integrations and workflow-based Self-Serve Actions, its smaller market presence and proprietary nature make it a riskier choice than platforms with larger ecosystems or open-source foundations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Universal catalog supporting diverse asset types&lt;/li&gt;
&lt;li&gt;Strong discovery and search capabilities&lt;/li&gt;
&lt;li&gt;Cloud cost analytics integration&lt;/li&gt;
&lt;li&gt;Relationship mapping across resources&lt;/li&gt;
&lt;li&gt;Workflow-based Self-Serve Actions&lt;/li&gt;
&lt;li&gt;Available as SaaS or on-premises deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Like&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;It's good at helping teams understand what exists in their infrastructure. The cost integration is unique, combining cloud spending data with technical resources. It can pull data from many systems. You get deployment flexibility with both SaaS and on-premises options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;The ecosystem is smaller with less community support and fewer integrations (30+) than larger platforms. The data model is Configure8-specific, which means proprietary lock-in. There are fewer public case studies and enterprise deployments than alternatives. As a smaller player in a competitive market, there's some uncertainty about long-term viability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free tier available (up to 10 users for scorecards). Paid tiers available with SOC2 certification and RBAC features. Enterprise pricing available with additional features and volume discounts. Available as both SaaS and on-premises deployment. Contact Configure8 for detailed pricing. &lt;a href="https://configure8-io.webflow.io/pricing" rel="noopener noreferrer"&gt;View pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Foundation&lt;/th&gt;
&lt;th&gt;Maintenance&lt;/th&gt;
&lt;th&gt;Ecosystem Size&lt;/th&gt;
&lt;th&gt;Lock-In Risk&lt;/th&gt;
&lt;th&gt;Enterprise Features&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roadie&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open Source (Backstage)&lt;/td&gt;
&lt;td&gt;Minimal (Managed)&lt;/td&gt;
&lt;td&gt;211 plugins&lt;/td&gt;
&lt;td&gt;Low (standard YAML)&lt;/td&gt;
&lt;td&gt;RBAC, SSO, SOC2 Day 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cortex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Minimal (SaaS)&lt;/td&gt;
&lt;td&gt;60+ integrations&lt;/td&gt;
&lt;td&gt;High (proprietary)&lt;/td&gt;
&lt;td&gt;Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Minimal (SaaS)&lt;/td&gt;
&lt;td&gt;50+ integrations&lt;/td&gt;
&lt;td&gt;High (proprietary)&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpsLevel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Minimal (SaaS)&lt;/td&gt;
&lt;td&gt;60+ integrations&lt;/td&gt;
&lt;td&gt;High (proprietary)&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Minimal (SaaS)&lt;/td&gt;
&lt;td&gt;Atlassian-centric&lt;/td&gt;
&lt;td&gt;High (proprietary)&lt;/td&gt;
&lt;td&gt;Good (if Atlassian)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backstage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open Source&lt;/td&gt;
&lt;td&gt;High (3-12 engineers)&lt;/td&gt;
&lt;td&gt;250+ plugins&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configure8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Minimal (SaaS)&lt;/td&gt;
&lt;td&gt;30+ integrations&lt;/td&gt;
&lt;td&gt;High (proprietary)&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Making Your Choice
&lt;/h2&gt;

&lt;p&gt;The right IDP depends on your organization's constraints, technical culture, and platform engineering maturity. Here's how I'd think about it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Self-Hosted Backstage if&lt;/strong&gt; you have a team of 10+ platform engineers, unlimited budget, and specific requirements that absolutely can't be met by managed solutions. You're willing to invest significant engineering time in maintenance and customization for maximum control. Be prepared for 3-12 dedicated engineers and $2M+ total cost of ownership over three years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Cortex or Port if&lt;/strong&gt; you value polished UI above everything else, don't mind proprietary lock-in, and want specific workflow capabilities their platforms emphasize. Budget for potentially higher costs at scale (around $65 per user per month for Cortex, around $30+ per user per month for Port enterprise tiers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose OpsLevel if&lt;/strong&gt; you need a basic service catalog immediately and your primary use case is &lt;a href="https://roadie.io/docs/catalog/ownership/" rel="noopener noreferrer"&gt;tracking ownership&lt;/a&gt; and maturity, not building complex workflows or maintaining extensive documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Compass if&lt;/strong&gt; you live entirely in the Atlassian ecosystem and can accept its limitations with non-Atlassian tools. Note that scaffolding and templates were deprecated on December 1, 2025. The integration efficiency may outweigh the platform's constraints if you're already deep in the Atlassian world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Roadie if&lt;/strong&gt; you want the industry standard (Backstage) with its entire ecosystem and community support, but you value your engineers' time too much to spend it on infrastructure maintenance. It's what I consider the golden path for enterprises that want modern platform capabilities without the platform tax or the $2M+ cost of building from scratch.&lt;/p&gt;

&lt;p&gt;The key question isn't which platform has the most features. It's which platform lets your engineers focus on building platform capabilities instead of maintaining platform infrastructure. At the 150+ engineer scale where IDPs become critical, that distinction determines whether your portal becomes a force multiplier or just another thing to maintain, potentially at a cost exceeding $2M over three years if you go the self-hosted route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;After evaluating these platforms for the past few years, I keep coming back to a simple principle: your platform team should build platform capabilities, not maintain platform infrastructure. The $2M+ you'd spend on self-hosting Backstage could fund a lot of actual platform work.&lt;/p&gt;

&lt;p&gt;If you're serious about Backstage but want to skip the TypeScript tax, &lt;a href="https://roadie.io/request-demo/" rel="noopener noreferrer"&gt;request a personalized demo of Roadie&lt;/a&gt; that'll show you what managed Backstage looks like in practice. Worth checking out before you commit to building everything yourself.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Developer Productivity vs Developer Experience: Why You Can't Fix One Without the Other</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Mon, 05 Jan 2026 07:32:43 +0000</pubDate>
      <link>https://dev.to/luciench/developer-productivity-vs-developer-experience-why-you-cant-fix-one-without-the-other-56j0</link>
      <guid>https://dev.to/luciench/developer-productivity-vs-developer-experience-why-you-cant-fix-one-without-the-other-56j0</guid>
      <description>&lt;p&gt;Your engineering team lost 8+ hours per developer this week to broken CI pipelines, unclear requirements, and context switching between tools. That's 20% of your engineering capacity. Your manager brings sprint velocity reports. You bring developer complaints about the flaky test suite. Nobody wins because you're treating these as separate problems.&lt;/p&gt;

&lt;p&gt;They're not. Developer Experience is the leading indicator of Productivity. Fix one without the other and you're building either a burnout factory or an expensive hobby club.&lt;/p&gt;

&lt;p&gt;Here's what these terms mean:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Productivity&lt;/strong&gt; is the rate of value delivery to users. Features shipped, bugs fixed, deployments completed. The measurable output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Experience (DevEx)&lt;/strong&gt; is the friction encountered while delivering that value. Build failures, context switches, review delays, broken tooling, ambiguous requirements. The daily obstacles that slow everything down.&lt;/p&gt;

&lt;p&gt;The relationship is causal. Bad DevEx degrades Productivity over time. High Productivity with poor DevEx is temporary. You can't optimize one metric while ignoring the human cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1qsyvyynth0ypm4pd59.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1qsyvyynth0ypm4pd59.jpg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Burnout Factory: High Productivity, Low DevEx
&lt;/h2&gt;

&lt;p&gt;You can force short-term productivity gains. Extend sprints, skip code reviews, ignore tooling problems, accumulate technical debt. The velocity metrics look great for six months.&lt;/p&gt;

&lt;p&gt;Then the system breaks.&lt;/p&gt;

&lt;p&gt;Your best engineers start quiet quitting. Code quality drops. Hero culture kicks in where 20% of the team carries 80% of the work. On call becomes unbearable because nobody understands the hastily shipped features. Six months later, you lose three senior engineers in a single month.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.opslevel.com/resources/tl-dr-key-takeaways-from-the-2024-google-cloud-dora-report" rel="noopener noreferrer"&gt;2024 DORA State of DevOps Report&lt;/a&gt; found that teams with unstable organizational priorities experience 40% higher risk of burnout. Even strong leadership and good documentation can't compensate for constantly shifting goals. The human cost isn't visible in your sprint reports until it's too late.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI Amplification Effect
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://research.google/pubs/dora-2025-state-of-ai-assisted-software-development-report/" rel="noopener noreferrer"&gt;2025 DORA report&lt;/a&gt; introduces a critical insight: AI acts as an amplifier. It magnifies whatever foundation you give it. High-performing teams with solid practices see acceleration. Teams with dysfunction see amplified problems.&lt;/p&gt;

&lt;p&gt;The data shows AI adoption now &lt;a href="https://www.splunk.com/en_us/blog/learn/state-of-devops.html" rel="noopener noreferrer"&gt;positively correlates with throughput&lt;/a&gt;. Teams ship code faster and recover from failures more quickly. But here's the catch: AI adoption still correlates with higher instability. More change failures, increased rework, longer cycle times to resolve issues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://adtmag.com/articles/2025/09/24/what-2025-dora-report-means-for-developers.aspx" rel="noopener noreferrer"&gt;90% of technology professionals use AI at work&lt;/a&gt;, up 14% from 2024. Developers spend a median of two hours per day with AI tools. Yet only 24% trust AI outputs strongly. The trust paradox: nearly everyone uses it, but few fully trust it.&lt;/p&gt;

&lt;p&gt;Your senior engineers now spend 60% of their time reviewing AI-generated code. The throughput metric goes up. The experience reality is code review fatigue, debugging syntactically correct but architecturally wrong solutions, and fixing subtle bugs that pass basic checks. Six months later, your tech leads quit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Country Club: Low Productivity, High DevEx
&lt;/h2&gt;

&lt;p&gt;The opposite failure is equally dangerous. Some organizations optimize purely for developer happiness. Perfect tooling, unlimited refactoring time, zero pressure to ship. Engineers love it. The business can't sustain it.&lt;/p&gt;

&lt;p&gt;Tech companies have been cutting significant portions of their workforce. &lt;a href="https://news.crunchbase.com/startups/tech-layoffs/" rel="noopener noreferrer"&gt;Crunchbase tracked over 95,000 tech layoffs in 2024&lt;/a&gt;, down from 200,000 in 2023 but still substantial. Companies cutting 20-30% of headcount are common: Stack Overflow cut 28%, Bumble 30%, Mozilla 30%. When organizations can't demonstrate value from engineering investments, the cuts are severe.&lt;/p&gt;

&lt;p&gt;The trap is vibes-based management. Running surveys, collecting feedback, investing in tooling but never connecting improvements to business outcomes. When the CFO asks "what did we get for the \$2M tooling budget?" you have no answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SPACE Framework: Measuring Both Dimensions
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://queue.acm.org/detail.cfm?id=3454124" rel="noopener noreferrer"&gt;SPACE framework&lt;/a&gt; from Microsoft Research, GitHub, and the University of Victoria recognizes that developer productivity is multi-dimensional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Satisfaction and Well-being&lt;/strong&gt;: How developers feel about their work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: The outcome of developer work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Activity&lt;/strong&gt;: Developer actions and outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communication and Collaboration&lt;/strong&gt;: How teams interact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency and Flow&lt;/strong&gt;: Ability to complete work with minimal interruptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need both objective telemetry (GitHub activity, deployment frequency, cycle time) and subjective data (developer surveys, sentiment analysis). One without the other gives you an incomplete picture.&lt;/p&gt;

&lt;p&gt;Example: Your deployment telemetry says builds complete in 5 minutes (fast). Your survey data says builds fail randomly 50% of the time, forcing developers to restart and wait another 5 minutes (frustrating). You need both signals to understand the real problem.&lt;/p&gt;

&lt;p&gt;Most tools measure half the picture. Traditional metrics platforms like &lt;a href="https://linearb.io/" rel="noopener noreferrer"&gt;LinearB&lt;/a&gt; focus on quantitative signals (DORA metrics, cycle time). Survey platforms like &lt;a href="https://www.cultureamp.com/" rel="noopener noreferrer"&gt;Culture Amp&lt;/a&gt; capture sentiment across organizations but aren't developer-specific. &lt;a href="https://getdx.com/" rel="noopener noreferrer"&gt;DX&lt;/a&gt; (founded by DORA/SPACE research creators) combines developer surveys with SDLC analytics. These approaches require deliberate implementation and buy-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Variable: Speed Gains with Quality Costs
&lt;/h2&gt;

&lt;p&gt;AI coding tools have shifted the productivity-experience equation. Teams are getting faster but the stability costs are real.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Throughput Story
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://devops.com/dora-2025-faster-but-are-we-any-better/" rel="noopener noreferrer"&gt;2025 DORA report&lt;/a&gt; confirms what earlier studies suggested: AI improves throughput. Teams using AI move work through the system faster than those who don't. The speed gains are measurable and consistent across organizations.&lt;/p&gt;

&lt;p&gt;But the report asks the critical question: "Faster, but are we any better?"&lt;/p&gt;

&lt;p&gt;Large-scale field studies show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft&lt;/strong&gt;: &lt;a href="https://mit-genai.pubpub.org/pub/v5iixksv" rel="noopener noreferrer"&gt;12.92% to 21.83% more pull requests&lt;/a&gt; per week in production environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accenture&lt;/strong&gt;: 7.51% to 8.69% more pull requests per week&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world tasks&lt;/strong&gt;: Studies using proprietary codebases report &lt;a href="https://arxiv.org/abs/2406.17910" rel="noopener noreferrer"&gt;30-40% time savings&lt;/a&gt; on repetitive tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are meaningful gains. Not the 55% from controlled lab studies, but significant improvements in real work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Quality Tradeoff
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.veracode.com/blog/ai-generated-code-security-risks/" rel="noopener noreferrer"&gt;Veracode's 2025 GenAI Code Security Report&lt;/a&gt; tested over 100 large language models across 80 coding tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;45% of AI-generated code&lt;/strong&gt; introduced OWASP Top 10 vulnerabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java had a 72% failure rate&lt;/strong&gt; (highest risk)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-site scripting tasks failed 86%&lt;/strong&gt; of the time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer, larger models performed no better&lt;/strong&gt; than smaller ones on security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security performance hasn't improved despite massive gains in code generation capabilities.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://linearb.io/blog/is-github-copilot-worth-it" rel="noopener noreferrer"&gt;GitClear's analysis&lt;/a&gt; found code churn (lines reverted or updated within two weeks) projected to double in 2024 compared to pre-AI baseline. AI-generated code shows 41% higher churn rate.&lt;/p&gt;

&lt;p&gt;The 2025 DORA research confirms this at scale: AI adoption correlates with higher instability. More change failures. Increased rework. Longer recovery times. Teams are shipping faster but breaking more.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Amplifier Effect
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.splunk.com/en_us/blog/learn/state-of-devops.html" rel="noopener noreferrer"&gt;DORA's central thesis&lt;/a&gt; is that AI doesn't create elite organizations, it anoints them. Organizations with solid foundations (robust platforms, clear processes, strong culture) see AI accelerate everything. Organizations with dysfunction see AI magnify the problems.&lt;/p&gt;

&lt;p&gt;High-quality platforms, data ecosystems, and governance are the difference. Without them, AI just helps you hit the wall faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Measurement Problem: Time Loss at Scale
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.atlassian.com/blog/developer/developer-experience-report-2024" rel="noopener noreferrer"&gt;2024 State of Developer Experience report&lt;/a&gt; from Atlassian and DX surveyed 2,100+ developers and leaders:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;69% of developers lose 8+ hours per week to inefficiencies&lt;/strong&gt;. That's 20% of their time. The top causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical debt&lt;/li&gt;
&lt;li&gt;Insufficient documentation&lt;/li&gt;
&lt;li&gt;Flawed build processes&lt;/li&gt;
&lt;li&gt;Context switching between tools&lt;/li&gt;
&lt;li&gt;Unclear requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The disconnect: &lt;strong&gt;Only 44% of developers believe their leaders are aware&lt;/strong&gt; of these issues. Meanwhile, &lt;strong&gt;86% of leaders&lt;/strong&gt; recognize they can't attract and retain talent without improving developer experience.&lt;/p&gt;

&lt;p&gt;This gap is expensive. A Bay Area developer making \$180,000 annually who loses 8 hours per week to inefficiencies represents \$36,000 in wasted productivity per year. For a team of 50 developers, that's \$1.8M annually lost to friction that leadership doesn't know exists.&lt;/p&gt;

&lt;p&gt;The 2025 DORA report found &lt;a href="https://www.faros.ai/blog/key-takeaways-from-the-dora-report-2025" rel="noopener noreferrer"&gt;no correlation between AI adoption and increased burnout&lt;/a&gt;. Developers are adapting to AI-enhanced workflows despite handling more concurrent workstreams. This is surprising given the increased context switching (9% more task contexts, 47% more pull requests daily). Teams are absorbing the complexity without burning out, at least not yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure Both (Without Creating Overhead)
&lt;/h2&gt;

&lt;p&gt;The failure mode is measurement theater. Weekly surveys, 50 different metrics, reports nobody reads, bureaucracy that creates more friction than insight.&lt;/p&gt;

&lt;p&gt;Do this instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't spam surveys.&lt;/strong&gt; Run comprehensive DevEx surveys quarterly using research-backed frameworks like &lt;a href="https://linearb.io/blog/space-framework" rel="noopener noreferrer"&gt;SPACE metrics&lt;/a&gt;. Use targeted pulse checks triggered by specific events: after major incidents, when cycle time spikes, during oncall rotations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't weaponize metrics.&lt;/strong&gt; Metrics should debug the system, not judge individual developers. If your team thinks productivity tracking is surveillance, you've lost. Make the data visible to everyone. Explain what you're measuring and why. Focus on team-level trends, not individual performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlate, don't report in silos.&lt;/strong&gt; The real insight comes from connecting the dots. When rework rate spikes, does developer sentiment drop? When you improve build times, does deployment frequency increase? When AI adoption hits 80%, what happens to code review cycle time and change failure rate?&lt;/p&gt;

&lt;p&gt;Microsoft Research studied this directly. Their &lt;a href="https://www.microsoft.com/en-us/research/wp-content/uploads/2024/11/Time-Warp-Developer-Productivity-Study.pdf" rel="noopener noreferrer"&gt;2024 "Time Warp" study&lt;/a&gt; found that developers who felt "very productive" had the highest correlation (0.52) between their actual and ideal workweeks. For unproductive developers, that correlation dropped to 0.18. The gap between how developers want to spend their time and how they actually spend it predicts productivity and satisfaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Using to Solve This
&lt;/h2&gt;

&lt;p&gt;I've been evaluating tools that connect productivity metrics with developer experience data. The problem is that most platforms do one or the other, not both.&lt;/p&gt;

&lt;p&gt;After testing several options, I'm using &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt;. What sold me: it &lt;a href="https://www.businesswire.com/news/home/20251105703479/en/Span-Raises-$25M-to-Bring-Clarity-to-Engineering-Teams-Navigating-AI-Transformation" rel="noopener noreferrer"&gt;combines metrics, team surveys, and behavioral context&lt;/a&gt; to show the complete picture of productivity and team health. Not just isolated numbers.&lt;/p&gt;

&lt;p&gt;You can correlate rework rate (productivity) with developer sentiment scores (experience) to understand if technical debt is creating team burnout. You can compare deployment frequency (productivity) with build satisfaction (experience) to measure if your CI/CD improvements actually helped. You can track AI code adoption (activity) alongside code review time and change failure rate (efficiency and stability) to see if your Copilot investment is shifting bottlenecks instead of eliminating them.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.span.app/blog/introducing-span-detect-1" rel="noopener noreferrer"&gt;span-detect-1 model&lt;/a&gt; provides 95% accuracy in detecting AI-assisted code across any tool: Copilot, Cursor, Claude, or custom solutions. This matters because you need to measure actual impact, not just adoption rates. Is AI code getting merged faster? Does it require more review cycles? How does the change failure rate compare between AI-assisted and human-written code?&lt;/p&gt;

&lt;p&gt;The platform doesn't require perfect Jira hygiene or process changes. &lt;a href="https://www.upstartsmedia.com/p/span-raises-25m-ai-code-tracker" rel="noopener noreferrer"&gt;AI-native classification&lt;/a&gt; automatically categorizes work, identifies patterns, and surfaces insights without adding overhead to your team. That was the dealbreaker for me. I'm not asking my team to change their workflow to feed a metrics tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;The organizations winning in 2025 stopped treating productivity and experience as competing priorities. Here's what they do differently:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They measure time loss, not just time spent.&lt;/strong&gt; Understanding where 8 hours per week disappears matters more than tracking story points completed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They connect DevEx investments to business outcomes.&lt;/strong&gt; When you improve build reliability, track deployment frequency and change failure rate before and after. When you fix documentation gaps, measure onboarding time and time-to-first-commit for new engineers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They treat AI tools as transformations, not features.&lt;/strong&gt; The &lt;a href="https://platformengineering.com/features/dora-2025-ai-wont-save-you-without-a-solid-platform/" rel="noopener noreferrer"&gt;2025 DORA AI Capabilities Model&lt;/a&gt; identifies seven organizational practices that determine AI success: clear governance, high-quality data ecosystems, robust version control, small-batch delivery, user-centric feedback, and strong platforms. Organizations that invest in these capabilities see AI accelerate their work. Organizations that skip them see AI magnify dysfunction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They ask developers what's broken.&lt;/strong&gt; Less than half of developers think their leaders understand the obstacles they face. The simplest fix is also the most effective: ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They prioritize stable goals over constant pivots.&lt;/strong&gt; The "move fast and constantly pivot" mentality increases burnout by 40% even with strong leadership and good documentation. Stability matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They understand AI is an amplifier.&lt;/strong&gt; If your foundation is shaky (technical debt, unclear requirements, flaky builds, poor documentation), AI will make it worse. Get your platform house in order first. Then AI becomes a force multiplier instead of a chaos accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;You can't fix Developer Productivity without improving Developer Experience. You can't improve Developer Experience without measuring Productivity outcomes. The organizations winning in 2025 stopped treating these as competing priorities.&lt;/p&gt;

&lt;p&gt;AI tools are forcing this reckoning. The 2025 DORA report confirms teams are getting faster with AI. But the instability costs are real and persistent. You need visibility into both sides to make informed decisions about where to invest.&lt;/p&gt;

&lt;p&gt;The teams that figure this out first will have a sustainable competitive advantage. The teams that don't will keep losing their best engineers while wondering why the velocity charts look so good.&lt;/p&gt;

&lt;p&gt;If you're ready to stop choosing between speed and happiness, check out &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt;. Their &lt;a href="https://www.span.app/detector" rel="noopener noreferrer"&gt;AI Code Detector&lt;/a&gt; gives you ground truth on how much AI code is being written and merged. You can &lt;a href="https://www.span.app/get-demo/" rel="noopener noreferrer"&gt;request a demo&lt;/a&gt; to see how it correlates metrics with sentiment and surfaces where your investments are actually paying off.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>leadership</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Measuring Noise: The Productivity Metrics That Really Matter in Software Engineering</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Tue, 23 Dec 2025 11:55:48 +0000</pubDate>
      <link>https://dev.to/luciench/stop-measuring-noise-the-productivity-metrics-that-really-matter-in-software-engineering-49p8</link>
      <guid>https://dev.to/luciench/stop-measuring-noise-the-productivity-metrics-that-really-matter-in-software-engineering-49p8</guid>
      <description>&lt;p&gt;"Productivity" has become a dirty word in engineering.&lt;/p&gt;

&lt;p&gt;Mention it in a Slack channel, and the immediate assumption is that management is looking for a reason to fire the bottom 10%, or that McKinsey is back with another controversial report. (For what it's worth, &lt;a href="https://www.mckinsey.com/industries/technology-media-and-telecommunications/our-insights/yes-you-can-measure-software-developer-productivity" rel="noopener noreferrer"&gt;their 2023 report&lt;/a&gt; actually warns against using "overly simple measurements, such as lines of code produced, or number of code commits" rather than recommending them. The backlash came from other aspects of their approach.)&lt;/p&gt;

&lt;p&gt;The skepticism is earned. For decades, productivity metrics in software engineering have been weaponized to micromanage individual contributors rather than optimize systems.&lt;/p&gt;

&lt;p&gt;But ignoring metrics entirely is just as dangerous. When you run an engineering organization on vibes and anecdotal evidence, you end up playing a game of Telephone. The reality of what's happening on the ground gets distorted as it passes through layers of management. You lose the ground truth.&lt;/p&gt;

&lt;p&gt;The question isn't &lt;em&gt;if&lt;/em&gt; we should measure productivity. The question is &lt;em&gt;what&lt;/em&gt; we should measure.&lt;/p&gt;

&lt;p&gt;Most standard dashboards are filled with noise. In the era of &lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;AI coding tools&lt;/a&gt;, &lt;a href="https://dora.dev/" rel="noopener noreferrer"&gt;DORA metrics&lt;/a&gt; are no longer enough. Here's what actually matters in 2025.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Velocity at All Costs
&lt;/h2&gt;

&lt;p&gt;For the last few years, the industry converged on DORA metrics (Deployment Frequency, Change Lead Time, Change Fail Percentage, Failed Deployment Recovery Time) as the gold standard.&lt;/p&gt;

&lt;p&gt;DORA is excellent, but it's a smoke alarm. It tells you when the house is burning down (Change Fail Percentage spikes) or when you're moving painfully slow (Deployment Frequency drops). But it doesn't tell you &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The rise of AI tools like &lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt; and Cursor has broken velocity as a standalone metric. A developer can now generate a 500-line Pull Request in 30 seconds using an LLM. The cycle time for the coding phase looks incredible. But if that code is a hallucinated mess that clogs up the review process for three days, that personal velocity came at the expense of the team's throughput.&lt;/p&gt;

&lt;p&gt;If you only measure speed, you'll incentivize behaviors that destroy quality. &lt;a href="https://arxiv.org/abs/2510.10165" rel="noopener noreferrer"&gt;Research from Tilburg University&lt;/a&gt; analyzing GitHub activity found that while less-experienced developers gain productivity from AI tools, core developers now review 6.5% more code and show a 19% drop in their own original code productivity. The time shifted to reviewing AI-generated submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Framework: Inputs, Outputs, and Internals
&lt;/h2&gt;

&lt;p&gt;To get a real signal, stop treating engineering like a black box. Start treating it like a system. You need to measure three distinct areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inputs:&lt;/strong&gt; What are we investing? (Headcount, Tooling Costs, Cloud Spend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internals:&lt;/strong&gt; How is the work actually happening? (PR workflow, Rework, Focus Time, Context Switching)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outputs:&lt;/strong&gt; What's the result? (Reliability, Feature Adoption, Customer Value)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here are the four specific metrics that are proving most valuable for modern engineering leaders.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Rework Rate (The AI Counter-Balance)
&lt;/h2&gt;

&lt;p&gt;This is the most underrated metric in software development right now. &lt;a href="https://www.faros.ai/blog/5th-dora-metric-rework-rate-track-it-now" rel="noopener noreferrer"&gt;Rework Rate measures the percentage of code that is rewritten or reverted shortly after being merged&lt;/a&gt;. The &lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener noreferrer"&gt;2024 DORA Report&lt;/a&gt; now includes rework rate as part of their evolved framework, categorizing it alongside Change Fail Percentage as an "instability metric."&lt;/p&gt;

&lt;p&gt;In an AI-augmented world, it's very easy to ship bad code fast. Data from platforms analyzing hundreds of engineering teams reveals a fascinating U-shaped curve regarding AI adoption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low AI usage:&lt;/strong&gt; Standard rework rates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High AI usage (Boilerplate):&lt;/strong&gt; Low rework rates (AI excels at unit tests and scaffolding)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid usage (25-50% AI):&lt;/strong&gt; Highest rework rates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When developers use AI in a "mixed context" (half human logic, half AI autocomplete), it creates code that looks correct at a glance but fails in edge cases. This hybrid approach causes cognitive whiplash for reviewers who must switch between evaluating human and AI logic. PRs that are clearly all-human OR all-AI are easier to review than mixed-ratio PRs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The red flag:&lt;/strong&gt; If you see your cycle time improving but your rework rate creeping up, you're not moving faster. You're just building technical debt faster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" rel="noopener noreferrer"&gt;GitClear's analysis of 211 million lines of code&lt;/a&gt; found that code churn is projected to double in 2024 versus their 2021 baseline, with 7.9% of newly added code being revised within two weeks (compared to 5.5% in 2020). Copy-pasted code rose from 8.3% to 12.3%.&lt;/p&gt;

&lt;p&gt;The challenge: you can't fix what you can't see. Most teams have no idea what percentage of their code is AI-generated, let alone how that AI code correlates with rework. Tools like &lt;a href="https://www.span.app/detector" rel="noopener noreferrer"&gt;Span's AI Code Detector&lt;/a&gt; now measure AI-authored code with 95% accuracy (for Python, TypeScript, and JavaScript), giving you ground truth on adoption patterns and quality impact.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Investment Distribution (The Truth vs. Your Task Tracker)
&lt;/h2&gt;

&lt;p&gt;Ask a VP of Engineering what their team is working on, and they'll show you a roadmap: "40% New Features, 20% Tech Debt, 40% KTLO."&lt;/p&gt;

&lt;p&gt;Ask the engineers what they're working on, and you'll hear about the "shadow work": "I'm supposed to be on the Platform team, but I'm spending 20 hours a week helping the Checkout team fix bugs because I'm the only one who knows the legacy codebase."&lt;/p&gt;

&lt;p&gt;Project management tools are often a lagging indicator of &lt;em&gt;intent&lt;/em&gt;, not &lt;em&gt;reality&lt;/em&gt;. Tickets get rolled over, scope creeps, and quick fixes go untracked. &lt;a href="https://www.infoworld.com/article/3831759/developers-spend-most-of-their-time-not-coding-idc-report.html" rel="noopener noreferrer"&gt;An IDC report analyzing developer time allocation found that application development accounts for just 16% of developers' time&lt;/a&gt;, with the rest consumed by meetings, context switching, and what engineering manager Anton Zaides calls &lt;a href="https://newsletter.manager.dev/p/the-shadow-work-in-engineering-teams" rel="noopener noreferrer"&gt;"shadow work"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Zaides identifies three main types of invisible work stealing team capacity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Invisible production support:&lt;/strong&gt; Investigating alerts, answering questions, ad-hoc requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical glue work:&lt;/strong&gt; Code reviews, planning, mentoring, documenting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow backlog:&lt;/strong&gt; Off-the-record PM requests, engineers doing things "right" without approval&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In one case study, a senior engineer had more than 40% of their time as invisible work. An internal team performed roughly 65% shadow work, all without cost codes or billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The red flag:&lt;/strong&gt; You need a metric that looks at actual code activity (commits, PRs, and reviews) to classify where effort is going. This automated analysis often reveals that the "Innovation" team is actually spending 70% of their time on maintenance. You can't fix that allocation if you rely solely on project management tools.&lt;/p&gt;

&lt;p&gt;Platforms like &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt; automatically categorize engineering work by analyzing git activity, creating what they call an "automated P&amp;amp;L of engineering time." You can finally answer questions like "How much time did we actually spend on that platform migration?" with data instead of guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Review Burden Ratio
&lt;/h2&gt;

&lt;p&gt;This metric tracks the relationship between the time spent &lt;em&gt;writing&lt;/em&gt; code and the time spent &lt;em&gt;reviewing&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;As AI drives the marginal cost of writing code to zero, the bottleneck shifts to the reviewer. &lt;a href="https://github.com/resources/articles/what-is-ai-code-generation" rel="noopener noreferrer"&gt;AI generates code instantly&lt;/a&gt;, humans review code linearly. If your review burden is skyrocketing, your senior engineers are becoming human spell-checkers for LLMs.&lt;/p&gt;

&lt;p&gt;The Tilburg University research quantified this shift: each core contributor now reviews approximately 10 additional PRs annually. Meanwhile, &lt;a href="https://www.faros.ai/blog/5th-dora-metric-rework-rate-track-it-now" rel="noopener noreferrer"&gt;Faros AI's analysis&lt;/a&gt; found that code review time increases 91% as PR volume outpaces reviewer capacity, with PR size growing 154% and bug rates climbing 9%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The red flag:&lt;/strong&gt; Watch for "LGTM" culture versus "Nitpick" culture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too fast:&lt;/strong&gt; If massive AI-generated PRs are getting approved in minutes, your Review Burden is suspiciously low, and you likely have a quality issue looming&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too slow:&lt;/strong&gt; If review burden is high, your senior engineers are trapped in Review Hell, leading to burnout and a halt in architectural innovation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Fragmented Time (The Anti-Flow Metric)
&lt;/h2&gt;

&lt;p&gt;Engineering requires flow. Yet, most organizations schedule their way out of productivity. Fragmented Time measures the blocks of available deep work time (2+ hours) versus time fractured by meetings and interruptions.&lt;/p&gt;

&lt;p&gt;You can have the best AI tools in the world, but if an engineer has 30 minutes between standup and a planning meeting, they're not shipping complex features. They're answering emails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://news.gallup.com/businessjournal/23146/too-many-interruptions-work.aspx" rel="noopener noreferrer"&gt;Research from UC Irvine professor Gloria Mark&lt;/a&gt; found that it takes an average of 23 minutes and 15 seconds to fully return to a task after an interruption. The nuance: this measures returning to your original "working sphere" (project-level work), not recovering from every small distraction. Her updated 2023 research in "Attention Span" found this has increased to approximately 25 minutes, while average attention on screen dropped from 2.5 minutes (2004) to just 47 seconds (2021).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The red flag:&lt;/strong&gt; If you look at calendar data and see that 40% of your engineering capacity is lost to context switching costs (the 30-minute dead zones between meetings), you've found the cheapest way to improve productivity: cancel meetings. No tool can fix a broken calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure Without Being Big Brother
&lt;/h2&gt;

&lt;p&gt;The biggest risk with productivity metrics is cultural, not technical. If you roll these out as a way to rank engineers, you've already lost. You'll incentivize gaming the system (splitting one PR into ten tiny ones to boost "throughput").&lt;/p&gt;

&lt;p&gt;Here's the golden rule: &lt;strong&gt;Metrics are for debugging systems, not people.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't&lt;/strong&gt; use data to ask: "Why is Alice slower than Bob?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do&lt;/strong&gt; use data to ask: "Why is the Checkout Team stuck in code review twice as long as the Platform Team? Do they need better tooling? Is their tech debt unmanageable?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leaders shouldn't want a dashboard that tells them who to fire. They should want a dashboard that acts as a neutral third party, objective data to validate what engineers are already saying in 1:1s. When an engineer says, "I'm swamped with maintenance work," you want the data to prove it so you can secure the budget and resources the team actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;So how do you actually implement these metrics?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Rework Rate:&lt;/strong&gt;&lt;br&gt;
Use your version control system to track code that gets reverted or significantly modified within 14-21 days of merging. Most engineering intelligence platforms can calculate this automatically. If you're rolling your own, start with a git analysis script that flags commits that touch the same files within your chosen time window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Investment Distribution:&lt;/strong&gt;&lt;br&gt;
Engineering intelligence platforms can automatically categorize work based on code patterns, repo structure, and commit messages. The key is classification: New Features vs. Bug Fixes vs. Tech Debt vs. Operations. If you don't have budget for tools, start manually by sampling one week per month and having engineers log their actual time allocation, then compare it to what your task tracker says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Review Burden:&lt;/strong&gt;&lt;br&gt;
Calculate the ratio of time PRs spend in review versus time spent in active development. GitHub, GitLab, and Bitbucket all expose this data through their APIs. A healthy ratio varies by team, but if you see review time consistently exceeding development time, or if senior engineers are spending more than 50% of their time reviewing, you've got a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Fragmented Time:&lt;/strong&gt;&lt;br&gt;
Use calendar analysis tools or simple calendar exports. Count the number of 2+ hour blocks available per engineer per week. Anything less than 10 hours of uninterrupted time per week per engineer is a red flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Quality Question
&lt;/h2&gt;

&lt;p&gt;The elephant in the room: how do you actually measure AI code quality?&lt;/p&gt;

&lt;p&gt;You can't manage what you can't measure. The first step is knowing how much AI-generated code you're actually shipping. Most teams are flying blind here, relying on surveys that are notoriously unreliable (&lt;a href="https://www.span.app/blog/why-surveys-fall-short" rel="noopener noreferrer"&gt;self-reported data has serious limitations&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Platforms like &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt; use code-level detection to identify AI-authored code with high accuracy, then correlate that with downstream metrics like rework rate, review cycles, and bug density. This gives you a complete picture: where AI is helping, where it's hurting, and how to coach your team to use it more effectively.&lt;/p&gt;

&lt;p&gt;The counterintuitive finding: it's not about reducing AI usage. It's about using AI more deliberately. Teams that coach engineers to use AI for complete, well-scoped tasks (not mixed human-AI work) see better outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;We're entering a new era of engineering intelligence. The old proxies (lines of code and commit counts) are dead. Even the modern standards like cycle time are insufficient on their own.&lt;/p&gt;

&lt;p&gt;To navigate the next few years, you need to understand the interplay between human creativity and AI leverage. You need to measure the quality of AI code, not just the volume.&lt;/p&gt;

&lt;p&gt;If you want to build a high-performing team in 2025, stop measuring how busy everyone looks. Start measuring the friction that slows them down.&lt;/p&gt;

&lt;p&gt;The metrics discussed here won't give you a single number to optimize. Instead, they'll give you a system view: where time actually goes, where quality breaks down, where your senior engineers are drowning in review work, and where your calendar is stealing focus time.&lt;/p&gt;

&lt;p&gt;Use them to debug your system, not to rank your people.&lt;/p&gt;

&lt;p&gt;If you're ready to move beyond vanity metrics and get real insights into your engineering organization, &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;check out Span&lt;/a&gt; to see how engineering intelligence platforms are helping teams measure what actually matters.&lt;/p&gt;

</description>
      <category>management</category>
      <category>softwareengineering</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>AI Tools for Developer Productivity: Hype vs. Reality in 2025</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Wed, 17 Dec 2025 10:18:48 +0000</pubDate>
      <link>https://dev.to/luciench/ai-tools-for-developer-productivity-hype-vs-reality-in-2025-480d</link>
      <guid>https://dev.to/luciench/ai-tools-for-developer-productivity-hype-vs-reality-in-2025-480d</guid>
      <description>&lt;p&gt;If you believe the marketing, AI "Agents" are about to replace us all, and "Autonomous Software Engineering" has arrived. If you ask actual developers, we're mostly just fixing hallucinations in boilerplate code.&lt;/p&gt;

&lt;p&gt;Every week brings another breathless announcement. Another demo video showing an AI building a complete app in 47 seconds. Another VC-funded startup claiming to have cracked AGI for code. Meanwhile, you're sitting in a PR review, staring at AI-generated code that confidently imports a library that doesn't exist.&lt;/p&gt;

&lt;p&gt;The noise is exhausting. But here's the thing: AI coding tools &lt;em&gt;are&lt;/em&gt; useful. Just not in the way the hype machine suggests.&lt;/p&gt;

&lt;p&gt;The real value isn't "autonomy," where AI does the job for you. It's "flow," where AI keeps you in the zone. That distinction matters more than any feature comparison, and understanding it will save you from chasing the wrong tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl3gl2dng3g7268qtr55.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl3gl2dng3g7268qtr55.jpg" alt="AI Coding Tools" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Category 1: The Sidekicks (IDE Extensions)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Hype: "It reads your mind and writes the function."&lt;/p&gt;

&lt;p&gt;The Reality: It's a super-powered &lt;a href="https://stackoverflow.com/" rel="noopener noreferrer"&gt;StackOverflow&lt;/a&gt; copy-paste.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt;, &lt;a href="https://sourcegraph.com/cody" rel="noopener noreferrer"&gt;Cody&lt;/a&gt;, and &lt;a href="https://supermaven.com/" rel="noopener noreferrer"&gt;Supermaven&lt;/a&gt; fall into this category. They live inside your existing IDE as extensions, watching what you type and suggesting completions. The marketing implies they understand your codebase, your intentions, and your architectural decisions.&lt;/p&gt;

&lt;p&gt;They don't. What they actually do is pattern-match against billions of lines of training data and make educated guesses about what comes next.&lt;/p&gt;

&lt;p&gt;That sounds dismissive, but it shouldn't be. For certain tasks, this is genuinely transformative. Writing &lt;a href="https://www.regular-expressions.info/" rel="noopener noreferrer"&gt;regex patterns&lt;/a&gt; that would otherwise require fifteen minutes of trial and error becomes a two-second autocomplete. Generating boilerplate for unit tests goes from tedious to trivial. Converting between data formats, writing SQL queries, scaffolding API endpoints, all the repetitive work that burns hours across a sprint becomes nearly instant.&lt;/p&gt;

&lt;p&gt;The problem emerges when developers start trusting these suggestions for anything beyond boilerplate. Ask Copilot to implement business logic and you'll get something that looks correct, compiles successfully, and silently breaks edge cases. Ask it for security-sensitive code and you might ship a vulnerability that passed all your reviews because the code &lt;em&gt;looked&lt;/em&gt; reasonable.&lt;/p&gt;

&lt;p&gt;The Verdict: Essential for reducing toil, but requires heavy verification. Use them for the boring stuff. Don't use them for the important stuff. And definitely don't measure productivity by how many suggestions you accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Category 2: The Native Editors (AI-First IDEs)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Hype: "The IDE that codes for you."&lt;/p&gt;

&lt;p&gt;The Reality: This is the biggest actual leap in 2025.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cursor.sh/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; and &lt;a href="https://codeium.com/windsurf" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt; represent a fundamentally different approach. Instead of bolting AI onto an existing editor, they build the entire IDE around AI-assisted workflows. This architectural decision has practical consequences that matter.&lt;/p&gt;

&lt;p&gt;Because these tools own the runtime, the terminal, and the diff view, they reduce context switching in ways plugins can't. When Cursor's "Composer" mode suggests a multi-file refactor, you see the changes across your entire project in a unified interface. You can accept, modify, or reject at the file level without jumping between tabs.&lt;/p&gt;

&lt;p&gt;This might sound like a minor UX improvement. It's not. The friction cost of context switching compounds throughout a coding session. Every time you break focus to check another file, run a test, or verify a change, you're burning cognitive resources. Tools that reduce those interruptions keep you in flow state longer.&lt;/p&gt;

&lt;p&gt;The multi-file editing capabilities are particularly strong for refactoring work. Renaming a function, updating its call sites, adjusting tests, and modifying documentation can happen in a single coordinated operation. For migration tasks, dependency updates, or architectural changes that touch dozens of files, this is genuinely faster than doing it manually.&lt;/p&gt;

&lt;p&gt;The Verdict: Superior to plugins for anyone doing regular refactoring or multi-file changes. The investment in learning a new editor pays off for teams with complex, interconnected codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Category 3: The Autonomous Agents (AI Software Engineers)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Hype: "The First AI Software Engineer."&lt;/p&gt;

&lt;p&gt;The Reality: Still experimental. High potential, but high babysitting cost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.cognition.ai/" rel="noopener noreferrer"&gt;Devin&lt;/a&gt; and &lt;a href="https://swe-agent.com/latest/" rel="noopener noreferrer"&gt;SWE-agent&lt;/a&gt; represent the bleeding edge: AI systems that theoretically work independently, browsing documentation, writing code, running tests, and iterating until the task is complete. The demo videos are impressive. An AI figures out an unfamiliar codebase, implements a feature, and opens a PR, all without human intervention.&lt;/p&gt;

&lt;p&gt;In practice, the experience differs from the demos.&lt;/p&gt;

&lt;p&gt;These agents work best for isolated "janitorial" tasks. Version bumps across multiple packages. Dependency migrations where the changes are mechanical. Adding logging to existing functions. Tasks with clear success criteria and minimal interaction with core business logic.&lt;/p&gt;

&lt;p&gt;Where they struggle is anywhere that requires understanding context beyond the immediate code. Implementing a feature that needs to match existing patterns in your codebase. Making architectural decisions that align with your team's conventions. Anything touching authentication, authorization, or data integrity.&lt;/p&gt;

&lt;p&gt;The oversight cost is the killer. You can't just fire off a task and forget about it. You need to review the agent's approach, check its intermediate steps, and validate the final output. For complex tasks, the time spent supervising often exceeds the time you would have spent just doing the work yourself.&lt;/p&gt;

&lt;p&gt;The Verdict: Useful for sandbox tasks and mechanical migrations. Not ready to touch core production logic without significant human oversight. Worth experimenting with, but don't bet your roadmap on them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjv0a7tr3lg8anwrgl1t.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjv0a7tr3lg8anwrgl1t.jpg" alt="AI Coding Tool Categories" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Hidden Cost of AI: Rework and Review&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's what nobody talks about in the productivity demos: the &lt;a href="https://en.wikipedia.org/wiki/Jevons_paradox" rel="noopener noreferrer"&gt;Jevons Paradox&lt;/a&gt; applied to code.&lt;/p&gt;

&lt;p&gt;When writing code becomes cheap and fast, we write more of it. But &lt;em&gt;reading&lt;/em&gt; code is still expensive. Someone has to review every AI-generated PR. Someone has to debug the subtle issues that passed initial review. Someone has to maintain the extra abstraction layers that seemed like a good idea when they took three seconds to generate.&lt;/p&gt;

&lt;p&gt;The nastiest problem is the "Hybrid PR." Code that's 50% AI-generated and 50% human-written often takes the longest to review. The context switches between AI patterns and human patterns. The reviewer can't develop a mental model of the author's intent because there are effectively two authors with different styles.&lt;/p&gt;

&lt;p&gt;This isn't theoretical. A &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;rigorous study from METR in July 2025&lt;/a&gt; found that experienced developers took 19% longer to complete tasks when using AI tools, despite believing they were faster. Teams report that review times have increased even as coding times have decreased. The net effect on cycle time is smaller than the productivity demos suggest, sometimes negligible.&lt;/p&gt;

&lt;p&gt;This doesn't mean AI tools aren't worth using. It means you need to measure the entire pipeline, not just the "lines written per hour" number that looks good in executive reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to Separate Hype from Reality in Your Own Team&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Don't trust demo videos. Don't trust vendor benchmarks. Trust your own repository data.&lt;/p&gt;

&lt;p&gt;The questions that actually matter are: Are developers accepting AI suggestions, or ignoring them? Is AI-generated code causing more rework in PRs? Which tools are your power users actually using daily versus which ones got installed and forgotten?&lt;/p&gt;

&lt;p&gt;This is where measurement becomes critical. &lt;a href="https://www.span.app/blog/why-surveys-fall-short" rel="noopener noreferrer"&gt;Self-reported usage surveys are unreliable&lt;/a&gt; because developers often overestimate their AI adoption. Vendor telemetry only shows you data for their specific tool, not how it fits into your broader workflow.&lt;/p&gt;

&lt;p&gt;What you need is code-level visibility: the ability to see which PRs contain AI-assisted code, how that code performs in review, and whether it requires more or less revision than human-written code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Span&lt;/a&gt; provides this visibility. The platform can &lt;a href="https://www.span.app/blog/introducing-span-detect-1" rel="noopener noreferrer"&gt;detect AI-generated code with over 95% accuracy&lt;/a&gt; across all major AI coding tools, giving you ground truth on adoption rather than self-reported estimates. More importantly, it correlates AI usage with downstream metrics like review cycles and rework rates, so you can see whether your AI investments are actually paying off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy64swnvazgnev1u4fq91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy64swnvazgnev1u4fq91.png" alt="PR Lifecycle" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal isn't to discourage AI tool usage. It's to ensure you're investing in tools that genuinely improve your team's output rather than just creating the appearance of productivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The best AI coding tool is the one that fits your workflow, not the one with the coolest Twitter demo.&lt;/p&gt;

&lt;p&gt;Sidekicks like Copilot reduce toil on repetitive tasks but require constant verification. Native editors like Cursor and Windsurf genuinely improve multi-file workflows and refactoring. Autonomous agents like Devin show promise for isolated tasks but aren't ready for unsupervised production work.&lt;/p&gt;

&lt;p&gt;None of them are magic. All of them require thoughtful integration into your development process. And measuring their actual impact requires looking beyond the metrics that vendors want you to track.&lt;/p&gt;

&lt;p&gt;Experiment aggressively. Try the new tools, push their limits, and see what works for your specific codebase and team structure. But measure the results with something more rigorous than vibes.&lt;/p&gt;

&lt;p&gt;The AI revolution in software development is real. It's just more mundane than the hype suggests, and that's okay. Mundane improvements that compound across thousands of developer-hours are worth more than revolutionary demos that don't survive contact with production code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Unsure if your team's AI tools are hype or reality? &lt;a href="https://www.span.app/" rel="noopener noreferrer"&gt;Connect Span to your repo&lt;/a&gt; to see exactly how much AI code is actually sticking in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>developer</category>
      <category>discuss</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Cheat Sheet for Basic Python for Postgres</title>
      <dc:creator>Lucien Chemaly</dc:creator>
      <pubDate>Mon, 22 Sep 2025 06:34:17 +0000</pubDate>
      <link>https://dev.to/luciench/cheat-sheet-for-basic-python-for-postgres-52pf</link>
      <guid>https://dev.to/luciench/cheat-sheet-for-basic-python-for-postgres-52pf</guid>
      <description>&lt;p&gt;Combining &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; with &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt; is an effective solution for complex data-driven tasks thanks to Python's straightforward programming experience and extensive libraries.&lt;/p&gt;

&lt;p&gt;In real-world applications, effective data management is essential. Whether you're working with user profiles, financial transactions, product inventories, or any other structured data, you'll often need to create, read, update, or delete records. These four operations—collectively known as CRUD—are the foundation of all database interactions. Mastering CRUD with Python and Postgres lays the foundation for building web applications, automating data workflows, and developing robust backend services for use cases like e-commerce platforms, automation scripts, and mobile apps.&lt;/p&gt;

&lt;p&gt;This tutorial shows how I set up a Postgres database using &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt; and connected a Python application to it. I show how to perform create, update, and delete operations, and how to make sure your database operations are safe, efficient, and maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Environment and Project
&lt;/h2&gt;

&lt;p&gt;If you don't have Python installed on your machine, go to the &lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;official Python website&lt;/a&gt; and download Python 3.10 or later.&lt;/p&gt;

&lt;p&gt;Create a new directory for your project by executing the following commands from your terminal or shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;python-postgres-crud
&lt;span class="nb"&gt;cd &lt;/span&gt;python-postgres-crud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep your project isolated, create a virtual environment by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now activate the virtual environment. If you're on Windows, run the command below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\venv\Scripts\activate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS/Linux, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need a dedicated adapter to interact with Postgres using Python. Run the following command to install &lt;a href="https://pypi.org/project/psycopg2/" rel="noopener noreferrer"&gt;psycopg2&lt;/a&gt;, the most commonly used library for this purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;psycopg2-binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With your project created, you're ready to start setting up the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up a Postgres Database
&lt;/h3&gt;

&lt;p&gt;Instead of setting up a local Postgres instance, I decided to use Supabase because it provides a fully managed, hosted Postgres database out of the box, simplifying the setup time and database management. This allows us to focus entirely on writing and deploying the application code without worrying about infrastructure details. First, visit &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt; and create a free account if you don't have one yet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you prefer to work with a local database, you can &lt;a href="https://www.postgresql.org/download/" rel="noopener noreferrer"&gt;install Postgres locally&lt;/a&gt; instead. However, this tutorial assumes you're using Supabase.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you're logged in, go to your dashboard and click &lt;strong&gt;New Project&lt;/strong&gt;. Fill out the form by providing a project name (&lt;em&gt;eg&lt;/em&gt; &lt;code&gt;python-tutorial&lt;/code&gt;), setting a strong database password (make sure to save it), and selecting a region close to your location. After you submit the form, Supabase will take a minute or two to provision your project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7mt0kfjpastrk4a6aqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7mt0kfjpastrk4a6aqx.png" alt="Create Supabase project" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After your project is ready, click &lt;strong&gt;Connect&lt;/strong&gt; on the top bar. Then scroll down to &lt;strong&gt;Session pooler&lt;/strong&gt; and click &lt;strong&gt;View parameters&lt;/strong&gt;. Here you'll find your connection details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlc6oq2iv6tji5md2e6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftlc6oq2iv6tji5md2e6u.png" alt="Connection details" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy your connection details (host, database, and user) and keep them somewhere safe because you'll need them later to connect your Python application to your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Python to Your Supabase Database
&lt;/h2&gt;

&lt;p&gt;Inside your project directory, create a file named &lt;code&gt;db_connect.py&lt;/code&gt; and add the following code to establish a connection to the database (make sure to replace &lt;code&gt;YOUR_HOST&lt;/code&gt;, &lt;code&gt;YOUR_USER&lt;/code&gt;, and &lt;code&gt;YOUR_PASSWORD&lt;/code&gt; with your credentials):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection to the database successful!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to connect to database: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the connection is successful, the code will generate a success message. If the connection fails, an error message will display.&lt;/p&gt;

&lt;p&gt;To test the connection, create a new file named &lt;code&gt;test_connection.py&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db_connect&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_connection&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this code from your terminal or shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python test_connection.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test verifies your database credentials, connection parameters, and network accessibility, confirming that your application can successfully communicate with the Supabase database. If everything is set up correctly, you'll see &lt;code&gt;Connection to the database successful!&lt;/code&gt; printed in the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing Your Database
&lt;/h2&gt;

&lt;p&gt;Before your Python code can interact with any data, you'll need to set up a database table. In this example, you'll create a &lt;code&gt;users&lt;/code&gt; table to store user information, including names and email addresses.&lt;/p&gt;

&lt;p&gt;From the left navigation in the Supabase dashboard, navigate to &lt;strong&gt;SQL Editor&lt;/strong&gt; and paste the following SQL command, then click &lt;strong&gt;Run&lt;/strong&gt; to create a &lt;code&gt;users&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command defines a table where each user will have a unique ID, a name, a unique email address, and an automatically set creation timestamp. After you run the query, open &lt;strong&gt;Table Editor&lt;/strong&gt; from the left navigation to ensure your table has been created successfully.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furha40rcp9755hptnw6k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Furha40rcp9755hptnw6k.png" alt="Users table" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inserting Records
&lt;/h2&gt;

&lt;p&gt;To insert records, create a new file named &lt;code&gt;insert_user.py&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db_connect&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_connection&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        INSERT INTO users (name, email)
        VALUES (%s, %s)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alice@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User inserted successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to insert user: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code uses the &lt;code&gt;create_connection()&lt;/code&gt; function to create the &lt;a href="https://www.psycopg.org/docs/connection.html" rel="noopener noreferrer"&gt;connection&lt;/a&gt;, which is the communication channel between your Python code and the database. The &lt;code&gt;conn.cursor()&lt;/code&gt; function creates a &lt;a href="https://www.psycopg.org/docs/cursor.html" rel="noopener noreferrer"&gt;cursor&lt;/a&gt; that is used to execute SQL commands. The &lt;code&gt;cur.execute()&lt;/code&gt; method executes the SQL insert, and the &lt;code&gt;conn.commit()&lt;/code&gt; function saves the change to the database. If the operation is successful, you will get a success message, and a new user with the name &lt;code&gt;Alice&lt;/code&gt; and email &lt;code&gt;alice@example.com&lt;/code&gt; will be created.&lt;/p&gt;

&lt;p&gt;Run the code by executing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python insert_user.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Connection to the database successful!
User inserted successfully!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you check your database from &lt;strong&gt;Table Editor&lt;/strong&gt;, you should see that the user named &lt;code&gt;Alice&lt;/code&gt; has been added to your table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpdhuf1sbst2u89g6qij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnpdhuf1sbst2u89g6qij.png" alt="New record created" width="800" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Multiple Records
&lt;/h3&gt;

&lt;p&gt;When you need to insert many records at once—such as during initial setup or data imports—you can do a bulk insert. To do so, create a new file named &lt;code&gt;insert_many_users.py&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db_connect&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_connection&lt;/span&gt;

&lt;span class="c1"&gt;# Number of users to insert
&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

&lt;span class="c1"&gt;# Generate dummy users
&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Connect to the database
&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Bulk insert using executemany
&lt;/span&gt;    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users (name, email) VALUES (%s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;users&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ Inserted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rowcount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; users successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;❌ Failed to insert users: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code specifies the number of users (twenty) through the variable &lt;code&gt;x&lt;/code&gt; before it uses a &lt;code&gt;for&lt;/code&gt; loop to fill the dummy users array. The code establishes a database connection before running an SQL query to insert all users simultaneously through the &lt;code&gt;executemany&lt;/code&gt; method. If the operation succeeds, it prints a success message after committing changes. If something goes wrong, it rolls back the transaction and prints an error message.&lt;/p&gt;

&lt;p&gt;Run the code by executing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python insert_many_users.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Connection to the database successful!
✅ Inserted 20 &lt;span class="nb"&gt;users &lt;/span&gt;successfully!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you check your database from &lt;strong&gt;Table Editor&lt;/strong&gt;, you should see that twenty new user records have been added to the table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbdtags2atdxb1x7g74m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftbdtags2atdxb1x7g74m.png" alt="Bulk records created" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating Records
&lt;/h2&gt;

&lt;p&gt;Most applications experience data changes through time because users perform updates to their email addresses, name changes, and preference modifications. Updating operations enable you to modify existing database records without needing to delete or recreate them. To update the records in the example, create a file named &lt;code&gt;update_user.py&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db_connect&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_connection&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        UPDATE users
        SET email = %s
        WHERE id = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;newalice@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User updated successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to update user: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's important that you use a &lt;code&gt;WHERE&lt;/code&gt; clause when performing updates so you don't accidentally modify all your records. You can use the primary key (&lt;code&gt;id&lt;/code&gt;) or a unique field (&lt;em&gt;eg&lt;/em&gt; &lt;code&gt;email&lt;/code&gt;) to precisely identify the record you want to update. Avoid using other fields that are not unique (&lt;em&gt;eg&lt;/em&gt; &lt;code&gt;name&lt;/code&gt;) as they can contain duplicate values and lead to unintended changes.&lt;/p&gt;

&lt;p&gt;The code is similar to the previous one for creating a connection and handling errors, but this time you're running an update SQL query to update the email of the record that has the &lt;code&gt;id&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the code by executing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python update_user.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Connection to the database successful!
User updated successfully!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you check your database from &lt;strong&gt;Table Editor&lt;/strong&gt;, you should see that the record has been updated with a new email: &lt;code&gt;newalice@example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsebsltlkud4p5dv230sj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsebsltlkud4p5dv230sj.png" alt="Record updated" width="800" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting Records
&lt;/h2&gt;

&lt;p&gt;In real-world applications, you may need to remove records, for example, when you delete a user account or clean up test data. While deleting data permanently is sometimes necessary (&lt;em&gt;eg&lt;/em&gt; complying with privacy regulations or removing irrelevant test entries), many applications use "soft deletes" instead. Soft deletes mark records as deleted (&lt;em&gt;eg&lt;/em&gt; with an &lt;code&gt;is_deleted&lt;/code&gt; flag) rather than actually removing them, allowing for later recovery and preserving historical data. That said, to perform a permanent delete using SQL, create a new file named &lt;code&gt;delete_user.py&lt;/code&gt; and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;db_connect&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_connection&lt;/span&gt;

&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        DELETE FROM users
        WHERE id = %s
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User deleted successfully!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to delete user: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this is similar to the previous code for creating a connection and handling errors, but you're running an update SQL query to delete a record that has the &lt;code&gt;id&lt;/code&gt; &lt;code&gt;1&lt;/code&gt;, the email &lt;code&gt;newalice@example.com&lt;/code&gt;, and the name &lt;code&gt;Alice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the code by executing the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python delete_user.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an output similar to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Connection to the database successful!
User deleted successfully!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, if you check your database from &lt;strong&gt;Table Editor&lt;/strong&gt;, you should see that the record with the email &lt;code&gt;newalice@example.com&lt;/code&gt; has been deleted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhviz5ve4609zgqqecq2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhviz5ve4609zgqqecq2r.png" alt="Record deleted" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practices and Error Handling
&lt;/h2&gt;

&lt;p&gt;When you're building a production-grade application, it's not just about making your code work—it's about making sure it works reliably, securely, and efficiently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameterized Queries
&lt;/h3&gt;

&lt;p&gt;To begin with, always use parameterized queries. These help defend your database from SQL injection, which can happen if user inputs are directly interpolated into SQL commands. Consider this vulnerable code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM users WHERE email = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the variable &lt;code&gt;email&lt;/code&gt; contains malicious SQL code, your entire database could be compromised. For example, an attacker can put something like &lt;code&gt;'; DROP TABLE users; –&lt;/code&gt;, and this will turn the SQL command into &lt;code&gt;DELETE FROM users WHERE email = ''; DROP TABLE users; --'&lt;/code&gt;. This query will first delete users with empty email addresses, then completely remove your users table, which will delete all your user data.&lt;/p&gt;

&lt;p&gt;Instead, use this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM users WHERE email = %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the input is safely handled by the database driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Context Managers
&lt;/h3&gt;

&lt;p&gt;You should also use context managers with the &lt;code&gt;with&lt;/code&gt; statement when dealing with connections and cursors. This approach ensures proper resource cleanup, even if your code crashes midway. This prevents potential resource leaks, such as leaving database connections open unintentionally. Here's how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;closing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;closing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(...))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Transactions
&lt;/h3&gt;

&lt;p&gt;If you perform multiple operations that should succeed or fail as a group, wrap them in a transaction. Transactions help preserve data integrity. An example is a user registration where you want to create a new user and update their preferences at the same time. Here's how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users (username, email) VALUES (%s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO user_preferences (user_id, preference) VALUES (%s, %s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preference&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User registration failed:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exception Handling
&lt;/h3&gt;

&lt;p&gt;You also need to consider how you'll handle exceptions specifically. Use targeted exception blocks like &lt;code&gt;psycopg2.IntegrityError&lt;/code&gt; to give more accurate feedback to users. If you try to insert a duplicate email, catching this specific error allows you to tell the user exactly what went wrong rather than presenting a vague or generic message. Here's how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users(email) VALUES(%s)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The email &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; already exists.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fetching Data Efficiently
&lt;/h3&gt;

&lt;p&gt;There are also a few ways you can improve performance. Avoid fetching more data than you need. If you're only displaying a user's name and email in your app, don't query every column from the database.&lt;/p&gt;

&lt;p&gt;Instead of writing a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should write a more specific query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pagination
&lt;/h3&gt;

&lt;p&gt;When your data set grows large, pagination becomes essential. Loading thousands of records at once can crash your app or slow it down. Fetch a limited number of rows at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This retrieves fifty records starting from the 101st entry. It's great for building paginated UIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing
&lt;/h3&gt;

&lt;p&gt;Indexing also helps speed up searches and filtering. For example, if you frequently look up users by email, create an index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_email&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The use of indexes speeds up read operations but results in slower insert and update operations. The selection of your indexed columns depends on the actual behavior of your application. You should never run &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; without a &lt;code&gt;WHERE&lt;/code&gt; clause because this can unintentionally affect all rows in your table. You can even enforce safety nets in development environments by limiting update/delete operations with application-level checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Foreign Key Constraints and Cascading Deletes
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-FK" rel="noopener noreferrer"&gt;foreign key&lt;/a&gt; establishes a link between tables by ensuring that each value in one table points to an existing record in another table, such as linking each post to its corresponding user. The &lt;code&gt;ON DELETE CASCADE&lt;/code&gt; feature provides a safe deletion method by automatically deleting related records in child tables whenever a parent record gets deleted.&lt;/p&gt;

&lt;p&gt;For example, you can create a &lt;code&gt;posts&lt;/code&gt; table that references the user's &lt;code&gt;id&lt;/code&gt; as a foreign key, and on delete, you do cascade deletes. Here's what it will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;

&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use cascading deletes with caution, as they can unintentionally remove large amounts of related data. Always design your deletion logic with a clear understanding of the relationships in your schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The foundational skills explained in this tutorial are your "cheat sheet" that will allow you to interact with a Postgres database directly from Python. I explained how to do basic CRUD operations using Python and Postgres. I also covered how to implement transactions, handle exceptions properly, avoid SQL injection, and write optimized queries to ensure that your application not only works but remains fast and secure as it scales.&lt;/p&gt;

&lt;p&gt;You can find the code used in this tutorial in the following &lt;a href="https://github.com/See4Devs/python-postgres-crud" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
