<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ timothy ogbemudia - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ timothy ogbemudia - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 25 Jul 2026 22:27:01 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/glamboyosa/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Make a Static HTML Page Editable in the Browser with Vanilla JavaScript ]]>
                </title>
                <description>
                    <![CDATA[ When you maintain a document for someone else, such as a résumé, a one-page portfolio, or a printable menu, the bottleneck is rarely the layout. It's the edit loop. Every small change ("move this bull ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-make-a-static-html-page-editable-in-the-browser-with-vanilla-javascript/</link>
                <guid isPermaLink="false">6a61266b62f25d8178b980e0</guid>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ DOM ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ timothy ogbemudia ]]>
                </dc:creator>
                <pubDate>Wed, 22 Jul 2026 20:22:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/08400ed2-ef6a-404f-a135-b6cefe9d6919.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you maintain a document for someone else, such as a résumé, a one-page portfolio, or a printable menu, the bottleneck is rarely the layout. It's the edit loop.</p>
<p>Every small change ("move this bullet up", "delete that line", "this link is dead") goes through you, the person with the code editor, even though the person requesting the change knows exactly what they want.</p>
<p>I ran into this maintaining a family member's résumé as a single static HTML file. The design was done. The content was theirs. But every revision, whether it was reordering a role, adding a certification, fixing a link, or nudging a print page break, meant another round of "send me the change, I'll edit the file." After the tenth round, the fix became obvious: make the page edit itself.</p>
<p>In this article, you'll build an in-browser editing layer for a static HTML page using <code>contenteditable</code>, about a hundred lines of vanilla JavaScript, and no build step. The person editing can change any text, reorder or delete any block, add new content, edit links, control print pagination, and print to PDF. A refresh restores the original file, untouched.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-what-is-contenteditable">What Iscontenteditable?</a></p>
</li>
<li><p><a href="#heading-why-not-a-react-app">Why Not a React App?</a></p>
</li>
<li><p><a href="#heading-how-to-prepare-the-markup-for-reordering">How to Prepare the Markup for Reordering</a></p>
</li>
<li><p><a href="#heading-how-to-attach-controls-to-every-block">How to Attach Controls to Every Block</a></p>
</li>
<li><p><a href="#heading-how-to-show-controls-only-on-the-innermost-block">How to Show Controls Only on the Innermost Block</a></p>
</li>
<li><p><a href="#heading-how-to-move-and-delete-blocks">How to Move and Delete Blocks</a></p>
</li>
<li><p><a href="#heading-how-to-edit-links-inside-contenteditable">How to Edit Links Insidecontenteditable</a></p>
</li>
<li><p><a href="#heading-how-to-let-users-control-print-pagination">How to Let Users Control Print Pagination</a></p>
</li>
<li><p><a href="#heading-how-to-add-new-content-from-templates">How to Add New Content from Templates</a></p>
</li>
<li><p><a href="#heading-why-nothing-persists">Why Nothing Persists</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have:</p>
<ul>
<li><p>Working knowledge of HTML and CSS, including CSS Grid and <code>@media print</code></p>
</li>
<li><p>Basic understanding of JavaScript DOM APIs (<code>querySelector</code>, event listeners, creating elements)</p>
</li>
<li><p>No frameworks, libraries, or build tools. That's the point.</p>
</li>
</ul>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<ul>
<li><p>What <code>contenteditable</code> gives you for free, and where it stops</p>
</li>
<li><p>How to attach move/delete controls to repeatable blocks with one reusable function</p>
</li>
<li><p>How to show controls only on the innermost hovered block using <code>:has()</code></p>
</li>
<li><p>How to reorder DOM nodes without a framework, and the markup prep that makes it safe</p>
</li>
<li><p>How to edit link URLs inside an editable region</p>
</li>
<li><p>How to let users place print page breaks themselves</p>
</li>
<li><p>How to add new content from templates, with placeholder text pre-selected</p>
</li>
<li><p>Why "nothing persists" can be a feature, not a limitation</p>
</li>
</ul>
<h2 id="heading-what-is-contenteditable">What Is <code>contenteditable</code>?</h2>
<p><code>contenteditable</code> is an HTML attribute that turns any element into an editable region. The browser handles the hard parts: caret placement, text selection, typing, deletion, clipboard, and undo history.</p>
<pre><code class="language-html">&lt;div class="page" contenteditable="true" spellcheck="false"&gt;
  &lt;!-- the entire document --&gt;
&lt;/div&gt;
</code></pre>
<p>That one attribute gets you further than you might expect. Clicking any paragraph places a caret. Cmd+Z undoes typing. Pressing Enter inside a <code>&lt;ul&gt;</code> creates a new <code>&lt;li&gt;</code>. The browser understands list semantics natively, so "add a bullet by pressing Enter" works with zero code.</p>
<p>But <code>contenteditable</code> alone isn't an editor. It has no concept of <em>blocks</em>. It won't move a job entry above another one, delete a card cleanly, or change an <code>href</code>. Clicking a link inside an editable region just places the caret in its text. Everything structural is on you. The rest of this article is about filling that gap.</p>
<h2 id="heading-why-not-a-react-app">Why Not a React App?</h2>
<p>The obvious alternative is to rebuild the page as a "real" app: components, state, a form per section, and an export button. I decided against it, and the tradeoff is important.</p>
<p>The file lives in a <code>public/</code> folder and is served as a static asset. It works from a URL, from disk, or from an email attachment. It has no dependencies to install, no build to run, and no way to rot when a toolchain updates.</p>
<p>The editing needs are small and bounded: change text, move blocks, delete blocks, add blocks, and print. That's DOM manipulation, the thing the DOM API is already good at.</p>
<p>A framework earns its complexity when state outlives the DOM: persistence, collaboration, validation, and syncing. This page has none of those requirements. When your state <em>is</em> the DOM and the lifetime is one session, a framework is an extra layer that buys you nothing.</p>
<h2 id="heading-how-to-prepare-the-markup-for-reordering">How to Prepare the Markup for Reordering</h2>
<p>Before writing any JavaScript, look at your markup for anything positional, meaning elements that only make sense <em>between</em> other elements. In my case, jobs were separated by <code>&lt;hr&gt;</code> dividers:</p>
<pre><code class="language-html">&lt;div class="job"&gt;...&lt;/div&gt;
&lt;hr class="divider"&gt;
&lt;div class="job"&gt;...&lt;/div&gt;
</code></pre>
<p>The moment blocks can move or be deleted, separators like this become landmines. Delete a job and its divider survives as an orphaned line. Move a job and the divider stays behind.</p>
<p>The fix is to delete the <code>&lt;hr&gt;</code> elements entirely and derive the separator from adjacency:</p>
<pre><code class="language-css">.section .job + .job {
  border-top: 0.5px solid var(--rule);
  padding-top: 18px;
}
</code></pre>
<p>The sibling combinator draws a rule above every job that follows another job. Reorder them, delete them, add new ones, and the separators are always exactly where they should be, because they're computed from structure rather than stored in it. This is the same principle as deriving state instead of duplicating it, applied to CSS.</p>
<h2 id="heading-how-to-attach-controls-to-every-block">How to Attach Controls to Every Block</h2>
<p>Each movable block gets a small control cluster with move up, move down, and delete actions, injected by one reusable function:</p>
<pre><code class="language-js">const SELECTORS = ['.section', '.job', '.bullets li', '.cert-card', '.skills-row', '.edu-row'];
const BREAKABLE = new Set(['.section', '.job']);

function makeBlock(el, sel) {
  el.classList.add('blk');
  el.dataset.sel = sel;
  const ctl = document.createElement('span');
  ctl.className = 'ctl';
  ctl.setAttribute('contenteditable', 'false');
  ctl.innerHTML =
    '&lt;button data-act="up" data-tip="Move this up"&gt;↑&lt;/button&gt;' +
    '&lt;button data-act="down" data-tip="Move this down"&gt;↓&lt;/button&gt;' +
    (BREAKABLE.has(sel) ? '&lt;button data-act="brk" data-tip="Page break: start a new printed page here"&gt;⇟&lt;/button&gt;' : '') +
    '&lt;button data-act="del" data-tip="Remove this. Refresh to bring it back"&gt;×&lt;/button&gt;';
  el.appendChild(ctl);
}

SELECTORS.forEach(sel =&gt; {
  page.querySelectorAll(sel).forEach(el =&gt; makeBlock(el, sel));
});
</code></pre>
<p>A few design decisions are worth calling out.</p>
<p>First, <code>contenteditable="false"</code> on the control cluster. Editable regions inherit. Everything inside the page is editable unless you opt out. Without this, the user could place a caret inside your buttons and delete them like text.</p>
<p>Second, <code>el.dataset.sel</code> records <em>which selector matched</em>. This matters later: when a block moves, it should only swap with siblings of its own kind. A bullet moves among bullets, a job among jobs. Storing the selector on the element makes that check trivial.</p>
<p>Third, the controls live <em>inside</em> the block they control. That gives you positioning for free, with <code>position: absolute</code> against the block's own <code>position: relative</code>, and means a block carries its controls with it wherever it moves.</p>
<h2 id="heading-how-to-show-controls-only-on-the-innermost-block">How to Show Controls Only on the Innermost Block</h2>
<p>Blocks nest: a bullet sits inside a job, which sits inside a section. Hovering a bullet technically hovers all three, and naïve CSS shows three control clusters at once. The result is visual noise exactly where the user is trying to focus.</p>
<p>Modern CSS solves this in one line:</p>
<pre><code class="language-css">.blk:hover:not(:has(.blk:hover)) &gt; .ctl { display: inline-flex; }
</code></pre>
<p>Read it inside out: show a block's controls when it's hovered, <em>unless</em> some descendant block is also hovered, in which case that deeper block wins. Hover a bullet, you get bullet controls. Hover the job's title (outside any bullet), you get job controls. One rule, no JavaScript.</p>
<p><code>:has()</code> is supported in every current browser, but a fallback costs one more rule:</p>
<pre><code class="language-css">@supports not selector(:has(*)) {
  .blk:hover &gt; .ctl { display: inline-flex; }
}
</code></pre>
<p>Older browsers get the noisier all-ancestors behavior instead of no controls at all. Degrade loudly, not silently.</p>
<h2 id="heading-how-to-move-and-delete-blocks">How to Move and Delete Blocks</h2>
<p>With controls attached, the actual reordering is short. One delegated listener handles every button on the page:</p>
<pre><code class="language-js">function siblings(el) {
  return [...el.parentElement.children].filter(c =&gt;
    c.classList.contains('blk') &amp;&amp; c.dataset.sel === el.dataset.sel);
}

page.addEventListener('click', e =&gt; {
  const btn = e.target.closest('.ctl button');
  if (!btn) return;
  e.preventDefault();
  const el = btn.closest('.blk');
  const sibs = siblings(el);
  const i = sibs.indexOf(el);
  const act = btn.dataset.act;
  if (act === 'up' &amp;&amp; i &gt; 0) sibs[i - 1].before(el);
  else if (act === 'down' &amp;&amp; i &lt; sibs.length - 1) sibs[i + 1].after(el);
  else if (act === 'del') el.remove();
  else if (act === 'brk') el.classList.toggle('page-break');
});
</code></pre>
<p><code>siblings()</code> is where <code>dataset.sel</code> pays off: it filters the parent's children down to blocks <em>of the same kind</em>, so a job can never swap into the middle of a bullet list. <code>before()</code> and <code>after()</code> move the live node with no cloning or re-rendering, and the block's own controls travel with it.</p>
<p>There's one subtle bug to prevent. Clicking a button inside an editable region moves the text caret first, which can scroll the page or collapse a selection. Suppress it at <code>mousedown</code>, before the browser acts:</p>
<pre><code class="language-js">page.addEventListener('mousedown', e =&gt; {
  if (e.target.closest('.ctl, .add-btn')) e.preventDefault();
});
</code></pre>
<p>Forgetting this is the kind of thing you only notice as a vague feeling that clicking buttons "jumps." It's worth ruling out before it ships.</p>
<h2 id="heading-how-to-edit-links-inside-contenteditable">How to Edit Links Inside <code>contenteditable</code></h2>
<p>Inside an editable region, single-clicking a link places the caret instead of navigating. That's correct for text editing but leaves no way to change the URL itself. The <code>href</code> isn't text, it's an attribute.</p>
<p>Double-click is unclaimed real estate, so hang URL editing off it:</p>
<pre><code class="language-js">page.addEventListener('dblclick', e =&gt; {
  const a = e.target.closest('a');
  if (!a) return;
  e.preventDefault();
  const url = prompt('Link URL (leave empty to remove the link):', a.getAttribute('href'));
  if (url === null) return;
  if (!url.trim()) a.replaceWith(document.createTextNode(a.textContent));
  else a.setAttribute('href', url.trim());
});
</code></pre>
<p>Yes, <code>prompt()</code>. It's unfashionable, but consider what a custom modal would cost: markup, styles, focus management, and an escape handler, all for a dialog that asks one question. <code>prompt()</code> is native, keyboard-accessible, and can't break.</p>
<p>The empty-string branch is a nice touch: it unwraps the link entirely, replacing it with its own text, so "remove this link" doesn't require knowing any HTML.</p>
<p>Since none of this is discoverable, tell the user. A <code>title</code> attribute on every link ("Double-click to change this link") surfaces the affordance exactly where it's needed.</p>
<h2 id="heading-how-to-let-users-control-print-pagination">How to Let Users Control Print Pagination</h2>
<p>If the document's destination is a printed PDF, page breaks are content decisions, like "start my Skills section on page three," and the user should own them. CSS makes the break itself easy:</p>
<pre><code class="language-css">@media print {
  .page-break { break-before: page; page-break-before: always; }
  .job { break-inside: avoid; page-break-inside: avoid; }
}
</code></pre>
<p>The interesting part is the interface. The <code>⇟</code> button you saw in <code>makeBlock</code> just toggles the <code>page-break</code> class on a job or section. On screen, the class renders as a dashed accent line above the block, a visible seam showing where the printed page will end:</p>
<pre><code class="language-css">.page .page-break {
  border-top: 1.5px dashed var(--accent) !important;
  padding-top: 14px !important;
}

@media print {
  .page .page-break { border-top: none !important; padding-top: 0 !important; }
}
</code></pre>
<p>The dashed line exists only on screen. In print it vanishes and the actual break takes its place. The user toggles, glances at the seam, and prints. Nobody edits CSS to re-paginate a document, and just as importantly, nobody asks me to.</p>
<p>The same <code>@media print</code> block hides every piece of editing chrome (<code>.toolbar, .ctl, .add-btn { display: none !important; }</code>), so the printed output is indistinguishable from the original static page.</p>
<h2 id="heading-how-to-add-new-content-from-templates">How to Add New Content from Templates</h2>
<p>Editing and deleting only go so far. Eventually someone needs a new bullet, a new role, or a new certification. Each repeatable container gets a dashed "+ Add" button that builds a blank block from a template:</p>
<pre><code class="language-js">skillsSection.appendChild(newAddBtn('+ Add skill row', 'Adds a blank row. Type over the placeholder.', btn =&gt; {
  const row = document.createElement('div');
  row.className = 'skills-row';
  row.innerHTML =
    '&lt;span class="skill-label"&gt;Label&lt;/span&gt;' +
    '&lt;span class="skill-items"&gt;Skill one, skill two, skill three&lt;/span&gt;';
  skillsSection.insertBefore(row, btn);
  makeBlock(row, '.skills-row');
  selectText(row.querySelector('.skill-label'));
}));
</code></pre>
<p>Two details here do most of the work.</p>
<p>New blocks go through the same <code>makeBlock</code> as everything parsed at load. There is exactly one code path for "this is a block now", so added content is immediately movable and deletable. For roles it gets its own nested "+ Add bullet" button.</p>
<p>If you find yourself writing a second registration path for dynamic content, stop. You're about to fork behavior that must stay identical.</p>
<p>And <code>selectText</code> pre-selects the placeholder:</p>
<pre><code class="language-js">function selectText(node) {
  const range = document.createRange();
  range.selectNodeContents(node);
  const sel = getSelection();
  sel.removeAllRanges();
  sel.addRange(range);
}
</code></pre>
<p>Click "+ Add skill row" and the word <code>Label</code> is already highlighted, so typing replaces it. No clicking into the field, no manually deleting placeholder text, and no placeholders accidentally left in the printed document.</p>
<p>One caveat: select the <em>text node</em>, not the block. The block contains your <code>contenteditable="false"</code> control cluster, and a selection spanning it will delete your buttons along with the placeholder on the first keystroke.</p>
<h2 id="heading-why-nothing-persists">Why Nothing Persists</h2>
<p>Every edit lives in the DOM and dies on refresh. That sounds like the missing feature, but it's the design.</p>
<p>The workflow this page serves is: open, adjust, print to PDF, close. The PDF is the artifact. The page is a template you stamp from. Ephemerality gives you a free, bulletproof undo-everything (refresh), zero risk of a half-finished edit becoming the new baseline, and a canonical version that always matches source control.</p>
<p>The toolbar says it plainly: <em>"Nothing is saved. Refresh resets everything."</em> Stated upfront, it reads as a guarantee rather than a gotcha.</p>
<p>Persistence would also be the complexity cliff. The moment edits survive refresh you inherit serialization, versioning, merge conflicts with the source file, and "which copy is real?" Those are the exact problems this design exists to avoid.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You now have a static HTML page that edits itself: <code>contenteditable</code> for text, one <code>makeBlock</code> function for structure, <code>:has()</code> for focused hover controls, a class toggle for print pagination, and templates with pre-selected placeholders for new content. Around a hundred lines of JavaScript, with no dependencies and no build.</p>
<p>Just as important is knowing when this approach stops being right. If edits must persist, if multiple people edit concurrently, or if the content needs validation and workflow, you've outgrown the DOM-as-state model. In those cases, reach for a real application and a database.</p>
<p>But for the wide middle ground of documents that one person adjusts and prints, such as résumés, invoices, certificates, and programmes, the browser already ships the editor. You just have to turn it on.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Reliable SSE Client in TypeScript ]]>
                </title>
                <description>
                    <![CDATA[ When you build a feature that streams data, like an AI chat response or a live notification feed, the network is rarely as cooperative as fetch makes it look. Connections drop, proxies buffer response ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-reliable-sse-client-in-typescript/</link>
                <guid isPermaLink="false">6a3db0651016f6a6b4bd2a89</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streaming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SSE ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ timothy ogbemudia ]]>
                </dc:creator>
                <pubDate>Thu, 25 Jun 2026 22:49:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3c13d795-15e8-452a-b490-89528d58efd2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you build a feature that streams data, like an AI chat response or a live notification feed, the network is rarely as cooperative as <code>fetch</code> makes it look.</p>
<p>Connections drop, proxies buffer responses, and mobile networks switch from WiFi to cellular mid-stream. If your streaming code doesn't plan for this, the user sees a response that just stops, with no error and no recovery.</p>
<p>In this article, you'll use an open source TypeScript library called <a href="https://github.com/glamboyosa/ore">Ore</a> as a practical example of how to build a streaming client that handles real-world network conditions: automatic retries, the official Server-Sent Events (SSE) parsing spec, and clean integration with React and React Server Components.</p>
<p>By the end, you'll understand how async generators, the Fetch API, and the SSE spec fit together to build something far more reliable than a basic <code>fetch</code> and <code>response.body.getReader()</code> loop.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-what-is-server-sent-events">What Is Server-Sent Events?</a></p>
</li>
<li><p><a href="#heading-why-build-a-custom-streaming-client">Why Build a Custom Streaming Client?</a></p>
</li>
<li><p><a href="#heading-how-to-stream-raw-chunks-with-an-async-generator">How to Stream Raw Chunks with an Async Generator</a></p>
</li>
<li><p><a href="#heading-how-to-parse-server-sent-events-by-hand">How to Parse Server-Sent Events by Hand</a></p>
</li>
<li><p><a href="#heading-how-to-implement-reconnection-with-last-event-id">How to Implement Reconnection with Last-Event-ID</a></p>
</li>
<li><p><a href="#heading-how-to-handle-retries-with-backoff">How to Handle Retries with Backoff</a></p>
</li>
<li><p><a href="#heading-how-to-use-this-with-react">How to Use This with React</a></p>
</li>
<li><p><a href="#heading-how-to-use-this-with-react-server-components">How to Use This with React Server Components</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have:</p>
<ul>
<li><p>A working understanding of TypeScript</p>
</li>
<li><p>Familiarity with <code>fetch</code>, <code>ReadableStream</code>, and <code>async</code>/<code>await</code></p>
</li>
<li><p>Basic knowledge of React (for the React-specific sections)</p>
</li>
</ul>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<ul>
<li><p>How to stream raw text or bytes from a <code>fetch</code> response using async generators</p>
</li>
<li><p>How to parse the Server-Sent Events spec by hand, field by field</p>
</li>
<li><p>How to implement automatic reconnection with <code>Last-Event-ID</code> so you don't lose events</p>
</li>
<li><p>How to handle retries with exponential backoff</p>
</li>
<li><p>How to integrate a streaming client with React state and React Server Components</p>
</li>
</ul>
<h2 id="heading-what-is-server-sent-events">What Is Server-Sent Events?</h2>
<p>Server-Sent Events (SSE) is a web standard for one-way streaming from server to client over a single HTTP connection. Unlike WebSockets, it's plain HTTP, which means it works through existing infrastructure like load balancers and proxies without special configuration.</p>
<p>An SSE response looks like this on the wire:</p>
<pre><code class="language-plaintext">event: update
id: 42
data: {"status": "processing"}

event: update
id: 43
data: {"status": "complete"}
</code></pre>
<p>Each event is separated by a blank line. The <code>data</code> field carries the payload, <code>event</code> names the event type, and <code>id</code> lets the client track its position in the stream for reconnection.</p>
<p>The browser has a built-in <code>EventSource</code> API for this, but it has real limitations: no custom headers, no POST requests, and inconsistent reconnection behavior across browsers. For anything beyond the simplest case, you often need to parse the stream yourself.</p>
<h2 id="heading-why-build-a-custom-streaming-client">Why Build a Custom Streaming Client?</h2>
<p>Many streaming use cases, like AI chat responses, don't use the SSE spec at all. They're just raw chunks of text arriving over time. Other cases, like live notifications, genuinely benefit from the structure SSE provides: named events, IDs for resumption, and a server-controlled retry interval.</p>
<p>Ore handles both with two separate functions:</p>
<ul>
<li><p><code>stream()</code> for raw text or byte streaming, with no assumptions about format</p>
</li>
<li><p><code>streamSSE()</code> for spec-compliant SSE parsing</p>
</li>
</ul>
<p>Both are async generators, so consuming either looks the same from the call site:</p>
<pre><code class="language-typescript">for await (const chunk of stream("https://api.example.com/chat")) {
  console.log(chunk);
}
</code></pre>
<h2 id="heading-how-to-stream-raw-chunks-with-an-async-generator">How to Stream Raw Chunks with an Async Generator</h2>
<p>The simplest case is streaming raw text. This is useful for AI responses or log tails where there's no event structure, just a sequence of bytes arriving over time.</p>
<p>Here's the core of <code>stream()</code>:</p>
<pre><code class="language-typescript">export async function* stream(
  url: string,
  options?: StreamOptions
): AsyncGenerator&lt;string | Uint8Array, void, unknown&gt; {
  const { headers, retries = 3, signal, decode = true } = options || {};

  let retryCount = 0;

  while (retryCount &lt;= retries) {
    try {
      const response = await fetch(url, { method: "GET", headers, signal });

      if (!response.body) {
        throw new Error("Response body is null");
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();

      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          yield decode ? decoder.decode(value, { stream: true }) : value;
        }
      } finally {
        reader.releaseLock();
      }

      return;
    } catch (error: any) {
      if (signal?.aborted) throw error;
      retryCount++;
      if (retryCount &gt; retries) {
        throw new Error(`Max retries exceeded. Last error: ${error.message}`);
      }
      await new Promise((r) =&gt; setTimeout(r, 1000 * retryCount));
    }
  }
}
</code></pre>
<p>A few design decisions are worth calling out.</p>
<p>The function is an async generator (<code>async function*</code>), so the caller can use <code>for await...of</code> instead of managing a reader and a loop manually. That's the difference between exposing a raw <code>ReadableStream</code> and exposing something pleasant to consume.</p>
<p>The <code>finally</code> block always releases the reader lock, even if the loop exits early through a <code>break</code> or an exception. Forgetting this is a common source of stream leaks.</p>
<p>The retry loop only catches errors from the <code>fetch</code> call and the read loop. If the <code>AbortSignal</code> was the cause of the failure, it rethrows immediately rather than retrying, since retrying a deliberate cancellation makes no sense.</p>
<h2 id="heading-how-to-parse-server-sent-events-by-hand">How to Parse Server-Sent Events by Hand</h2>
<p>The SSE spec is a simple text format, but parsing it correctly means handling several edge cases: events split across multiple data lines, comment lines starting with a colon, fields with no value, and incomplete lines at the end of a chunk.</p>
<p>Here's the core state machine inside <code>streamSSE()</code>:</p>
<pre><code class="language-typescript">let buffer = "";
let currentEvent: Partial&lt;SSEEvent&gt; = { data: "", event: null, id: null };
let hasData = false;

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split(/\r\n|\r|\n/);
  buffer = lines.pop() || ""; // keep the last incomplete line for the next chunk

  for (const line of lines) {
    if (line === "") {
      if (hasData) {
        const event: SSEEvent = {
          id: currentEvent.id ?? lastEventId,
          event: currentEvent.event ?? null,
          data: currentEvent.data!.endsWith("\n")
            ? currentEvent.data!.slice(0, -1)
            : currentEvent.data!,
          retry: currentEvent.retry,
        };
        if (event.id) lastEventId = event.id;
        yield event;
        currentEvent = { data: "", event: null, id: null };
        hasData = false;
      }
      continue;
    }

    if (line.startsWith(":")) continue; // comment line, ignore

    const colonIndex = line.indexOf(":");
    const field = colonIndex === -1 ? line : line.slice(0, colonIndex);
    let valueStr = colonIndex === -1 ? "" : line.slice(colonIndex + 1);
    if (valueStr.startsWith(" ")) valueStr = valueStr.slice(1);

    switch (field) {
      case "data":
        currentEvent.data += valueStr + "\n";
        hasData = true;
        break;
      case "event":
        currentEvent.event = valueStr;
        break;
      case "id":
        if (valueStr.indexOf("\0") === -1) currentEvent.id = valueStr;
        break;
      case "retry":
        const retry = parseInt(valueStr, 10);
        if (!isNaN(retry)) retryInterval = retry;
        break;
    }
  }
}
</code></pre>
<p>A network chunk doesn't respect line boundaries. A single <code>read()</code> call might end mid-line, so the last, possibly incomplete line is held back in <code>buffer</code> and prepended to the next chunk rather than processed early. This is the part of SSE parsing that's easy to get wrong if you reach for a naïve <code>response.text()</code> and a string split.</p>
<p>The blank line is what ends an event. SSE events don't have a fixed-length header. The spec says a blank line marks the boundary, so the parser only yields an event once it has seen one.</p>
<p>The <code>id</code> field is rejected outright if it contains a null byte, per the spec. That's a small detail that almost no hand-rolled implementation gets right on the first try.</p>
<h2 id="heading-how-to-implement-reconnection-with-last-event-id">How to Implement Reconnection with Last-Event-ID</h2>
<p>This is the part of SSE that gives it a real advantage over a plain <code>fetch</code> stream: built-in support for resuming after a disconnect without losing your place.</p>
<pre><code class="language-typescript">let lastEventId: string | null = null;

while (retryCount &lt;= retries) {
  const headers = { ...customHeaders };
  if (lastEventId) {
    (headers as any)["Last-Event-ID"] = lastEventId;
  }

  const response = await fetch(url, { method: "GET", headers, signal });
  // ... read and parse events, updating lastEventId as they arrive
}
</code></pre>
<p>Every time an event with an <code>id</code> field arrives, <code>lastEventId</code> is updated. If the connection drops and the client reconnects, it sends <code>Last-Event-ID</code> in the request headers. A well-behaved server can use that header to resume the stream from the right point instead of replaying everything or skipping ahead.</p>
<p>This only works if the server actually honors the header, so it's a contract between client and server, not something the client can guarantee alone. But having the client track and send it correctly is the necessary half of that contract.</p>
<h2 id="heading-how-to-handle-retries-with-backoff">How to Handle Retries with Backoff</h2>
<p>Both <code>stream()</code> and <code>streamSSE()</code> retry on failure, but they do it slightly differently based on what failed.</p>
<p><code>stream()</code> uses a simple linear backoff tied to the retry count:</p>
<pre><code class="language-typescript">await new Promise((resolve) =&gt; setTimeout(resolve, 1000 * retryCount));
</code></pre>
<p><code>streamSSE()</code> respects the server-specified <code>retry</code> field from the SSE spec when one is provided, falling back to a default otherwise:</p>
<pre><code class="language-typescript">let retryInterval = 1000;
// ... updated from the "retry" field if the server sends one
await new Promise((r) =&gt; setTimeout(r, retryInterval));
</code></pre>
<p>Letting the server influence the retry interval matters in practice. A server under load can tell clients to back off longer, which is exactly the kind of cooperative behavior the SSE spec was designed to support.</p>
<p>In both functions, an aborted <code>AbortSignal</code> always short-circuits the retry loop. Treating a deliberate cancellation as a retryable failure is a common bug, and the fix is just checking <code>signal?.aborted</code> before deciding to retry.</p>
<h2 id="heading-how-to-use-this-with-react">How to Use This with React</h2>
<p>Because both functions are async generators, integrating with React state is a matter of looping and calling <code>setState</code> per chunk:</p>
<pre><code class="language-typescript">function ChatComponent() {
  const [messages, setMessages] = useState("");

  useEffect(() =&gt; {
    const controller = new AbortController();

    (async () =&gt; {
      try {
        for await (const chunk of stream("/api/chat", { signal: controller.signal })) {
          setMessages((prev) =&gt; prev + chunk);
        }
      } catch (err: any) {
        if (err.name !== "AbortError") console.error(err);
      }
    })();

    return () =&gt; controller.abort();
  }, []);

  return &lt;div&gt;{messages}&lt;/div&gt;;
}
</code></pre>
<p>The cleanup function calling <code>controller.abort()</code> is doing real work here. Without it, navigating away from the component while a stream is still active leaves the fetch running in the background, updating state on an unmounted component.</p>
<h2 id="heading-how-to-use-this-with-react-server-components">How to Use This with React Server Components</h2>
<p>Because the generator yields values one at a time, you can also drive a recursive Suspense boundary directly from the async iterator, streaming HTML to the client as each chunk arrives:</p>
<pre><code class="language-typescript">async function StreamViewer({ iterator }: { iterator: AsyncIterator&lt;string&gt; }) {
  const { value, done } = await iterator.next();
  if (done) return null;

  return (
    &lt;span&gt;
      {value}
      &lt;Suspense&gt;
        &lt;StreamViewer iterator={iterator} /&gt;
      &lt;/Suspense&gt;
    &lt;/span&gt;
  );
}

export default function Page() {
  const dataStream = stream("https://api.example.com/stream");
  const iterator = dataStream[Symbol.asyncIterator]();

  return (
    &lt;Suspense fallback="Loading..."&gt;
      &lt;StreamViewer iterator={iterator} /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>Each recursive call awaits the next chunk and renders a nested <code>Suspense</code> boundary for the rest. React streams each piece of HTML to the client as it resolves, rather than waiting for the entire response.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>A reliable streaming client needs to handle more than the success path. Connections drop, chunks arrive split across line boundaries, and cancellation needs to be distinguished from failure.</p>
<p>Ore's approach to this is built from a small set of ideas:</p>
<ul>
<li><p>Expose streams as async generators so consumers can use <code>for await...of</code></p>
</li>
<li><p>Parse SSE by hand, field by field, respecting the spec's blank-line event boundaries and buffering incomplete lines across chunks</p>
</li>
<li><p>Track <code>Last-Event-ID</code> so reconnection can resume rather than restart</p>
</li>
<li><p>Treat retries and cancellation as separate concerns</p>
</li>
<li><p>Stay framework-agnostic at the core, with thin integration points for React and React Server Components</p>
</li>
</ul>
<p>That combination is what separates a streaming client that works in a demo from one that holds up against real network conditions. You can explore the full source code at <a href="https://github.com/glamboyosa/ore">github.com/glamboyosa/ore</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a PostgreSQL-Backed Job Queue in Go ]]>
                </title>
                <description>
                    <![CDATA[ When you build a web application, not every task should happen inside a user's request. Some work is slow. Some work can fail. Some work should happen later. Sending emails, resizing images, processin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-postgresql-backed-job-queue-in-go/</link>
                <guid isPermaLink="false">6a28a0135ea1e6904efb11dc</guid>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Queues ]]>
                    </category>
                
                    <category>
                        <![CDATA[ golang ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ timothy ogbemudia ]]>
                </dc:creator>
                <pubDate>Tue, 09 Jun 2026 23:21:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f16f87ae-8900-40e9-ba3b-64bf50cc1fe1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you build a web application, not every task should happen inside a user's request.</p>
<p>Some work is slow. Some work can fail. Some work should happen later. Sending emails, resizing images, processing webhooks, generating reports, and retrying third-party APIs are all good examples.</p>
<p>These tasks are usually handled by a background job system.</p>
<p>In this article, you'll use an open source Go project called <a href="https://github.com/glamboyosa/swig">Swig</a> as a practical example of how a PostgreSQL-backed job queue works in practice.</p>
<p>By the end, you'll understand how to build a background job queue with Go and PostgreSQL, and why PostgreSQL is more capable than most developers realize.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-what-is-a-job-queue">What Is a Job Queue?</a></p>
</li>
<li><p><a href="#heading-why-use-postgresql-for-a-queue">Why Use PostgreSQL for a Queue?</a></p>
</li>
<li><p><a href="#heading-swigs-architecture">Swig's Architecture</a></p>
</li>
<li><p><a href="#heading-how-to-represent-jobs-in-postgresql">How to Represent Jobs in PostgreSQL</a></p>
</li>
<li><p><a href="#heading-how-to-define-a-worker-in-go">How to Define a Worker in Go</a></p>
</li>
<li><p><a href="#heading-how-to-register-workers-without-sharing-state">How to Register Workers Without Sharing State</a></p>
</li>
<li><p><a href="#heading-how-to-add-a-job">How to Add a Job</a></p>
</li>
<li><p><a href="#heading-how-to-handle-multiple-workers-safely">How to Handle Multiple Workers Safely</a></p>
</li>
<li><p><a href="#heading-how-to-use-goroutines-for-concurrent-workers">How to Use Goroutines for Concurrent Workers</a></p>
</li>
<li><p><a href="#heading-how-to-wake-workers-with-listennotify">How to Wake Workers with LISTEN/NOTIFY</a></p>
</li>
<li><p><a href="#heading-how-to-elect-a-leader-with-advisory-locks">How to Elect a Leader with Advisory Locks</a></p>
</li>
<li><p><a href="#heading-how-to-handle-failed-jobs">How to Handle Failed Jobs</a></p>
</li>
<li><p><a href="#heading-how-to-abstract-the-database-driver">How to Abstract the Database Driver</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along, you should have:</p>
<ul>
<li><p>Basic familiarity with Go (structs, interfaces, goroutines)</p>
</li>
<li><p>A working understanding of PostgreSQL and SQL</p>
</li>
<li><p>Go installed (1.21 or later)</p>
</li>
<li><p>A PostgreSQL instance available locally or remotely</p>
</li>
</ul>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<ul>
<li><p>How to represent and store jobs in PostgreSQL</p>
</li>
<li><p>How to claim jobs safely across concurrent workers using <code>FOR UPDATE SKIP LOCKED</code></p>
</li>
<li><p>How to wake workers efficiently using <code>LISTEN/NOTIFY</code></p>
</li>
<li><p>How to elect a leader across instances using advisory locks</p>
</li>
<li><p>How Go interfaces, goroutines, contexts, and transactions fit together in a real system</p>
</li>
</ul>
<h2 id="heading-what-is-a-job-queue">What Is a Job Queue?</h2>
<p>A job queue is a system that stores work to be done later.</p>
<p>Your application adds a job to the queue. A worker takes a job from the queue and runs it.</p>
<p>For example, when a user signs up, your application might create the user immediately and then add a job like this:</p>
<pre><code class="language-json">{
  "kind": "send_welcome_email",
  "payload": {
    "to": "user@example.com",
    "subject": "Welcome!"
  }
}
</code></pre>
<p>A background worker later picks up that job and sends the email. This keeps the user request fast. The signup route doesn't need to wait for the email provider before returning a response.</p>
<p>A job queue usually needs to answer a few important questions:</p>
<ul>
<li><p>Where are jobs stored?</p>
</li>
<li><p>How do workers find jobs?</p>
</li>
<li><p>How do you stop two workers from processing the same job?</p>
</li>
<li><p>How do you retry failed jobs?</p>
</li>
<li><p>How do you shut workers down safely?</p>
</li>
<li><p>How do you keep job creation consistent with application data?</p>
</li>
</ul>
<p>Swig answers those questions with Go and PostgreSQL.</p>
<h2 id="heading-why-use-postgresql-for-a-queue">Why Use PostgreSQL for a Queue?</h2>
<p>Many job queues use Redis, RabbitMQ, SQS, or Kafka. Those are all useful tools. But many applications already depend on PostgreSQL. If your app already has Postgres, you may not want to operate another service just to run background jobs.</p>
<p>PostgreSQL gives you several features that are surprisingly useful for queues:</p>
<ul>
<li><p>Tables for durable job storage</p>
</li>
<li><p>Transactions for atomic writes</p>
</li>
<li><p>Row locks for safe concurrent processing</p>
</li>
<li><p><code>SKIP LOCKED</code> for letting workers claim different jobs</p>
</li>
<li><p><code>LISTEN/NOTIFY</code> for waking workers when new jobs arrive</p>
</li>
<li><p>Advisory locks for leader election</p>
</li>
<li><p>JSONB for flexible job payloads</p>
</li>
</ul>
<p>The tradeoff is important. A PostgreSQL-backed queue isn't trying to replace Kafka for event streaming or RabbitMQ for complex routing. It makes common application background jobs simple, reliable, and easy to operate without adding infrastructure.</p>
<h2 id="heading-swigs-architecture">Swig's Architecture</h2>
<p>At a high level, Swig has five parts:</p>
<ol>
<li><p>A <code>swig_jobs</code> table in PostgreSQL</p>
</li>
<li><p>Go workers that process jobs</p>
</li>
<li><p>A worker registry that maps job names to worker types</p>
</li>
<li><p>A driver layer that supports both <code>pgx</code> and <code>database/sql</code></p>
</li>
<li><p>A leader loop for shared maintenance work</p>
</li>
</ol>
<p>The basic flow looks like this:</p>
<ol>
<li><p>Your app calls <code>AddJob</code></p>
</li>
<li><p>Swig serializes the job payload to JSON</p>
</li>
<li><p>Swig inserts a row into <code>swig_jobs</code></p>
</li>
<li><p>PostgreSQL sends a notification that a job was created</p>
</li>
<li><p>A Go worker wakes up and tries to claim one pending job</p>
</li>
<li><p>PostgreSQL row locks ensure only one worker claims that row</p>
</li>
<li><p>The worker runs the job</p>
</li>
<li><p>Swig marks the job as completed or failed</p>
</li>
</ol>
<p>The hard parts are concurrency, failure, connection lifecycle, and shutdown. That's where Go and PostgreSQL work together.</p>
<h2 id="heading-how-to-represent-jobs-in-postgresql">How to Represent Jobs in PostgreSQL</h2>
<p>A simplified version of Swig's job table looks like this:</p>
<pre><code class="language-sql">CREATE TABLE swig_jobs (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  kind TEXT NOT NULL,
  queue TEXT NOT NULL,
  payload JSONB NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  priority INTEGER NOT NULL DEFAULT 0,
  attempts INTEGER NOT NULL DEFAULT 0,
  max_attempts INTEGER NOT NULL DEFAULT 3,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  scheduled_for TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  instance_id UUID,
  worker_id UUID,
  locked_at TIMESTAMPTZ,
  last_error TEXT,
  last_error_at TIMESTAMPTZ
);
</code></pre>
<p>Each row is one job. The important columns are:</p>
<ul>
<li><p><code>kind</code>: the type of job, such as <code>send_email</code></p>
</li>
<li><p><code>payload</code>: the JSON data needed to run the job</p>
</li>
<li><p><code>status</code>: whether the job is pending, processing, completed, or failed</p>
</li>
<li><p><code>attempts</code>: how many times the job has been tried</p>
</li>
<li><p><code>scheduled_for</code>: when the job is allowed to run</p>
</li>
<li><p><code>locked_at</code>: when the job was claimed</p>
</li>
</ul>
<p>The table is the source of truth. PostgreSQL notifications can wake workers, but notifications aren't the durable queue. The rows in <code>swig_jobs</code> are.</p>
<h2 id="heading-how-to-define-a-worker-in-go">How to Define a Worker in Go</h2>
<p>In Swig, a worker is a Go type that knows how to process one kind of job.</p>
<p>Here's a simple email worker:</p>
<pre><code class="language-go">type EmailWorker struct {
    To      string `json:"to"`
    Subject string `json:"subject"`
    Body    string `json:"body"`
}

func (w *EmailWorker) JobName() string {
    return "send_email"
}

func (w *EmailWorker) Process(ctx context.Context) error {
    fmt.Printf("Sending email to %s with subject %s\n", w.To, w.Subject)
    return nil
}
</code></pre>
<p>There are two important methods:</p>
<ul>
<li><p><code>JobName</code> tells Swig what kind of job this worker handles</p>
</li>
<li><p><code>Process</code> contains the actual work</p>
</li>
</ul>
<p>The struct fields are also the job arguments. When you enqueue an <code>EmailWorker</code>, Swig serializes the struct into JSON and stores it in PostgreSQL. Later, a worker claims the row, unmarshals the JSON back into a fresh <code>EmailWorker</code>, and calls <code>Process</code>.</p>
<h3 id="heading-go-interfaces">Go Interfaces</h3>
<p>Go interfaces describe behavior. Swig doesn't need to know the exact concrete type of every worker. It only needs to know that a worker can provide a job name and process a job:</p>
<pre><code class="language-go">type Worker interface {
    JobName() string
    Process(context.Context) error
}
</code></pre>
<p>If a type has those methods, it satisfies the interface with no explicit declaration required. This is one of the reasons interfaces are so useful in Go. They let you design around behavior instead of inheritance.</p>
<h2 id="heading-how-to-register-workers-without-sharing-state">How to Register Workers Without Sharing State</h2>
<p>Swig has a worker registry that maps a job name to a worker type:</p>
<pre><code class="language-go">registry := workers.NewWorkerRegistry()
registry.RegisterWorker(&amp;EmailWorker{})
</code></pre>
<p>Later, when a job row says <code>kind = 'send_email'</code>, Swig looks up the registered worker and runs it.</p>
<p>There's a subtle concurrency issue here. If the registry stored the exact <code>&amp;EmailWorker{}</code> pointer and reused it for every job, multiple goroutines could unmarshal payloads into the same Go value at the same time.</p>
<p>Swig avoids this with a factory approach internally. Registration captures the worker type, and each claimed job gets a fresh worker instance before JSON is unmarshaled. The API stays simple, but internally Swig creates a new <code>EmailWorker</code> for each job. This is a useful Go pattern: keep the public API simple while making the internal lifecycle safer.</p>
<h2 id="heading-how-to-add-a-job">How to Add a Job</h2>
<p>Here's what adding a job looks like from the user side:</p>
<pre><code class="language-go">err := swigClient.AddJob(ctx, &amp;EmailWorker{
    To:      "user@example.com",
    Subject: "Welcome!",
    Body:    "Thanks for signing up.",
})
</code></pre>
<p>Inside Swig, the process is roughly:</p>
<pre><code class="language-go">argsJSON, err := json.Marshal(workerWithArgs)
if err != nil {
    return err
}

_, err = db.ExecContext(ctx, `
    INSERT INTO swig_jobs (kind, queue, payload, priority, scheduled_for, status)
    VALUES (\(1, \)2, \(3, \)4, $5, 'pending')
`, jobName, queue, argsJSON, priority, runAt)
</code></pre>
<h3 id="heading-how-to-enqueue-jobs-inside-transactions">How to Enqueue Jobs Inside Transactions</h3>
<p>One of the best reasons to use PostgreSQL for jobs is transactional enqueueing.</p>
<p>Imagine a user signs up. You want to insert the user and queue a welcome email. If those happen separately, you can get inconsistent states. With a transaction, both succeed or both fail:</p>
<pre><code class="language-go">tx, err := pool.Begin(ctx)
if err != nil {
    return err
}
defer tx.Rollback(ctx)

_, err = tx.Exec(ctx, `INSERT INTO users (email) VALUES ($1)`, email)
if err != nil {
    return err
}

err = swigClient.AddJobWithTx(ctx, tx, &amp;EmailWorker{
    To:      email,
    Subject: "Welcome!",
    Body:    "Thanks for joining.",
})
if err != nil {
    return err
}

return tx.Commit(ctx)
</code></pre>
<p>If the transaction rolls back, the user isn't created and the job isn't queued. This is much harder to guarantee when your database and queue are separate systems.</p>
<h2 id="heading-how-to-handle-multiple-workers-safely">How to Handle Multiple Workers Safely</h2>
<p>A queue gets interesting when many workers run at the same time. Imagine three workers all asking PostgreSQL for the next pending job. You don't want all three to process the same job.</p>
<p>A naïve approach has a race condition. Two workers can select the same job before either one updates it.</p>
<h3 id="heading-postgresql-for-update-skip-locked">PostgreSQL FOR UPDATE SKIP LOCKED</h3>
<p>PostgreSQL can lock rows selected inside a transaction. <code>FOR UPDATE</code> means "lock this row because I plan to update it." <code>SKIP LOCKED</code> means "if another worker already locked a row, skip it and find another one."</p>
<p>This is perfect for a queue:</p>
<ul>
<li><p>Worker A locks job 1</p>
</li>
<li><p>Worker B skips job 1 and locks job 2</p>
</li>
<li><p>Worker C skips jobs 1 and 2 and locks job 3</p>
</li>
</ul>
<p>No central coordinator is needed. Swig uses an atomic update pattern:</p>
<pre><code class="language-sql">UPDATE swig_jobs
SET status = 'processing',
    instance_id = $1,
    worker_id = $2,
    locked_at = NOW(),
    attempts = attempts + 1
WHERE id = (
  SELECT id
  FROM swig_jobs
  WHERE status = 'pending'
    AND scheduled_for &lt;= NOW()
  ORDER BY priority DESC, created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
RETURNING id, kind, payload;
</code></pre>
<p>This query finds a pending job, skips already-locked jobs, marks it as processing, records which worker claimed it, and returns the job data. All of this happens atomically. Workers never do a separate <code>SELECT</code> and hope the later <code>UPDATE</code> is still safe.</p>
<h2 id="heading-how-to-use-goroutines-for-concurrent-workers">How to Use Goroutines for Concurrent Workers</h2>
<p>Swig starts worker loops as goroutines:</p>
<pre><code class="language-go">for i := 0; i &lt; maxWorkers; i++ {
    go s.startWorker(ctx, queueType)
}
</code></pre>
<p>Each worker runs independently. PostgreSQL coordinates which job each worker gets. Go handles concurrency with goroutines, while PostgreSQL handles safe job claiming with locks.</p>
<h3 id="heading-how-to-handle-graceful-shutdown">How to Handle Graceful Shutdown</h3>
<p>When a service shuts down, it should wait for workers to finish cleanly. Go's <code>sync.WaitGroup</code> helps:</p>
<pre><code class="language-go">var wg sync.WaitGroup

wg.Add(1)
go func() {
    defer wg.Done()
    processJobs()
}()

wg.Wait()
</code></pre>
<p>Swig also uses <code>sync.Once</code> to make shutdown idempotent. Calling <code>Stop</code> more than once shouldn't panic because of a double channel close. Shutdown paths are often where production systems behave differently from happy-path demos.</p>
<h2 id="heading-how-to-wake-workers-with-listennotify">How to Wake Workers with LISTEN/NOTIFY</h2>
<p>If workers constantly poll the database for jobs, they waste resources when the queue is empty. PostgreSQL has <code>LISTEN/NOTIFY</code> to solve this.</p>
<p>A connection can listen on a channel:</p>
<pre><code class="language-sql">LISTEN swig_jobs;
</code></pre>
<p>Another session can send a notification:</p>
<pre><code class="language-sql">NOTIFY swig_jobs, '{"id":"job-id"}';
</code></pre>
<p>Swig creates a trigger so PostgreSQL sends a notification after a job is inserted. Workers sleep when there's no work and wake when a new job arrives.</p>
<p>There's an important PostgreSQL detail here: <code>LISTEN</code> is session-scoped. A worker must wait for notifications on the same database session that executed <code>LISTEN</code>. Swig handles this by creating a dedicated listener for each worker that owns one database session throughout its lifecycle.</p>
<p>This is a common backend engineering lesson: abstractions like connection pools are useful, but some database features depend on the lifecycle of a specific connection.</p>
<h2 id="heading-how-to-elect-a-leader-with-advisory-locks">How to Elect a Leader with Advisory Locks</h2>
<p>Some queue maintenance tasks should only run on one instance at a time, including retrying failed jobs, recovering stale jobs, and cleaning old history.</p>
<p>Swig uses PostgreSQL advisory locks for this:</p>
<pre><code class="language-sql">SELECT pg_try_advisory_lock($1);
</code></pre>
<p>If the result is true, that Swig instance becomes the leader. Advisory locks are also session-scoped, so Swig uses a dedicated advisory-lock connection for leadership. If that session ends, PostgreSQL releases the lock and another instance can take over. Simple failover without ZooKeeper or etcd.</p>
<h2 id="heading-how-to-handle-failed-jobs">How to Handle Failed Jobs</h2>
<p>When a worker returns an error, Swig records the error and either retries the job or marks it as failed:</p>
<pre><code class="language-sql">UPDATE swig_jobs
SET status = CASE
    WHEN attempts &gt;= max_attempts THEN 'failed'
    ELSE 'pending'
  END,
  last_error = $2,
  last_error_at = NOW()
WHERE id = $1;
</code></pre>
<h3 id="heading-a-note-on-delivery-semantics">A Note on Delivery Semantics</h3>
<p>It's tempting to say a job queue processes jobs exactly once. In distributed systems, that's a dangerous claim.</p>
<p>Consider this scenario:</p>
<ol>
<li><p>A worker sends an email</p>
</li>
<li><p>The worker crashes before marking the job completed</p>
</li>
<li><p>The job is retried</p>
</li>
<li><p>The email might be sent again</p>
</li>
</ol>
<p>The accurate description is that Swig provides atomic claiming and at-least-once processing. Because jobs can be retried, workers should be idempotent. Running the same operation more than once should produce the same result as running it once.</p>
<h2 id="heading-how-to-abstract-the-database-driver">How to Abstract the Database Driver</h2>
<p>Swig supports both <code>pgx</code> and <code>database/sql</code> through a driver interface:</p>
<pre><code class="language-go">type Driver interface {
    Exec(ctx context.Context, sql string, args ...interface{}) error
    Query(ctx context.Context, sql string, args ...interface{}) (Rows, error)
    QueryRow(ctx context.Context, sql string, args ...interface{}) Row
    WithTx(ctx context.Context, fn func(tx Transaction) error) error
    NewListener(ctx context.Context, channel string) (Listener, error)
    TryAdvisoryLock(ctx context.Context, lockID int64) (AdvisoryLock, bool, error)
}
</code></pre>
<p>The core queue code only depends on behavior, not a specific library. This is a common Go design: define the behavior your core package needs, write small adapters for concrete dependencies, and keep the core logic independent.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>A PostgreSQL-backed queue isn't the right answer for every system. If you need massive event streaming, Kafka may be a better fit. If you need complex routing, RabbitMQ may be better.</p>
<p>But for many Go applications, PostgreSQL is already there. Swig shows how far you can get with a small Go API and a few PostgreSQL features:</p>
<ul>
<li><p>Store jobs in a table</p>
</li>
<li><p>Claim jobs atomically with <code>FOR UPDATE SKIP LOCKED</code></p>
</li>
<li><p>Wake workers with dedicated <code>LISTEN/NOTIFY</code> sessions</p>
</li>
<li><p>Coordinate leadership with advisory locks</p>
</li>
<li><p>Keep app data and jobs consistent with transactions</p>
</li>
<li><p>Manage worker lifecycles with goroutines and contexts</p>
</li>
</ul>
<p>That combination makes a solid foundation for background processing and a great project for learning how Go and PostgreSQL work together in production systems. You can explore the full source code at <a href="https://github.com/glamboyosa/swig">github.com/glamboyosa/swig</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
