<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="/atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <id>https://duggan.ie/</id>
  <title type="text">@duggan</title>
  <updated>2026-03-14T15:35:26.000Z</updated>
  <author>
    <name>Ross Duggan</name>
    <email>ross@duggan.ie</email>
    <uri>https://duggan.ie/</uri>
  </author>
  <icon>https://duggan.ie/favicon.ico</icon>
  <link href="https://duggan.ie/atom.xml" rel="first"/>
  <link href="https://duggan.ie/atom.xml?page=1" rel="last"/>
  <link href="https://duggan.ie/atom.xml" rel="self"/>
  <logo>https://duggan.ie/og-image.png</logo>
  <rights type="text">All rights reserved 2026, Ross Duggan</rights>
  <subtitle type="text">Blog of Ross Duggan, software developer based in Dublin, Ireland.</subtitle>
  <entry>
    <id>https://duggan.ie/posts/build-everything</id>
    <title type="text">Build Everything</title>
    <updated>2026-03-14T15:35:26.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Claude is changing how I write software dramatically.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;My biggest worry, as someone who's been writing software for a long time, is that I will be so blinded by my hard earned intuition on complexity that I will subconsciously be less ambitious than the moment calls for –&amp;nbsp;that some treacherous part of my mind stubbornly &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;knows&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; it's too hard.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When I was 25 I thought it was a given that I would just never stop learning. Back then the risk, from what I could see of middle aged developers, was clownishly bungling your way into some obvious technological cul-de-sac. The intervening 15 years have tested it at times, but I had not considered that the raw intuition itself might end up the problem.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="combinations-and-permutations" class="group relative"&gt;&lt;a href="#combinations-and-permutations" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Combinations and permutations&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For the last few years, new companies were really the only time I got to try out new architectural patterns. They become concrete so quickly, and updating them becomes an exercise in changing the wheels on a moving car. Plus, there's always stuff that seems more important.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;One of the slightly unintentional effects for me has been the ability to experiment with a variety different architecture and deployment patterns.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For a specific example, one of the projects I'm working on at the moment, &lt;/span&gt;&lt;a href="https://rockstar.ninja" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;rockstar.ninja&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (saving and sharing Claude Code sessions), has a macOS taskbar application. I've never written one of these before, and it's been my first experience working with &lt;/span&gt;&lt;a href="https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Apple's notarization system&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The backend server and CLI are written in Go, so I've just been cross-compiling for Linux and deploying the system using &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;rsync&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; over SSH. This is not too far from how we used to deploy boards.ie in 2008. Even the git repo for this one is just on one of my home servers.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For another project, &lt;/span&gt;&lt;a href="https://bewitch.dev" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;bewitch.dev&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (a terminal-based system monitor for Linux), I've gotten to build out the apt repo and multi-architecture build system that I always wanted to have when I started building command-line tools.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This blog itself is something of a continuous project. I started frontend development again in 2024 after our company pivoted, and my blog has been a way for me to explore the space without worrying too much about making mistakes. It started as a Next.js / Supabase project, quickly became an Astro-based project instead, and recently I've been replacing Astro entirely, as it was never quite the right fit.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="i-can-move-through-phases-more-quickly" class="group relative"&gt;&lt;a href="#i-can-move-through-phases-more-quickly" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;I can move through phases more quickly&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Discovering what you're building as you're building it is par for the course in software development.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The essential element of all of this is that, thanks to LLMs, it's been possible to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;iterate rapidly&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. That rapidity has enabled these projects to reach a level of quality (build automation, tests, documentation) that would otherwise have been hugely time consuming to produce.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It is allowing me to start a variety of different projects, integrating architectural and agentic lessons from each one into each other, combining or discarding approaches and trialling new ones.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Certainly, it has sometimes enabled me to go off on ill-advised tangents, over-engineering some corner of the system before I quite understand it. That is not new though– the difference is that now the cost to correct those missteps has also reduced significantly.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="it-s-easier-to-be-more-ambitious" class="group relative"&gt;&lt;a href="#it-s-easier-to-be-more-ambitious" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;It's easier to be more ambitious&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Being able to iterate faster and correct mistakes faster means I get to consider projects and possibilities that would have been out of reach before.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;As a result, I am trying to build everything.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Everything I can think of, every project that has ever sat on the back burner. Any number of ideas for new software, features or fixes might pop into my head a day. If I reckon I have the spare usage to tackle them, they get tackled. I can't even tackle them all with LLMs, but I am sure starting to make a dent.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It's the only way I can think of, in 2026, to try and crack a hole in that calcified intuition and start finding out where the edges are again.&lt;/span&gt;&lt;/p&gt;&lt;section class="changelog"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;time datetime="2026-03-16T21:31:24.000Z"&gt;Mar 16, 2026, 9:31 PM&lt;/time&gt; — Correct tense.&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;</content>
    <link href="https://duggan.ie/posts/build-everything" rel="alternate"/>
    <published>2026-03-16T21:25:39.999Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Claude is changing how I write software dramatically.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;My biggest worry, as someone who's been writing software for a long time, is that I will be so blinded by my hard earned intuition on complexity that I will subconsciously be less ambitious than the moment calls for –&amp;nbsp;that some treacherous part of my mind stubbornly &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;knows&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; it's too hard.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When I was 25 I thought it was a given that I would just never stop learning. Back then the risk, from what I could see of middle aged developers, was clownishly bungling your way into some obvious technological cul-de-sac. The intervening 15 years have tested it at times, but I had not considered that the raw intuition itself might end up the problem.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/building-a-stripe-dashboard-with-an-esp32-desktop-clock-and-rust</id>
    <title type="text">Building a Stripe dashboard with an ESP32 desktop clock and Rust</title>
    <updated>2026-02-13T11:27:12.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;A few weeks ago I built some custom firmware to turn the &lt;/span&gt;&lt;a href="https://www.ulanzi.com/products/ulanzi-pixel-smart-clock-2882" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Ulanzi TC001 desktop clock&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; into a Stripe subscription statistics and notification doodad.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Here's a fun little simulator that demonstrates the various screens and animations it has now:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class="web-embed-block web-embed-inline my-4" style="height: 230px; border: 1px solid rgb(229, 231, 235); border-radius: 0.5rem; overflow: hidden;"&gt;&lt;iframe src="https://duggan.ie/post-assets/stripe-dashboard-sim" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy" style="width: 100%; height: 100%;" title="Stripe Dashboard Simulator"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;span style="white-space: pre-wrap;"&gt;This is a WASM compiled variant of the project's integrated simulator&amp;nbsp;that I added so I could iterate on the design without having to flash the device repeatedly. The buttons along the bottom trigger various screens and are just for debugging; they don't appear on the device.&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;On boot it creates a wireless access point with a captive portal for configuration, like the automatic login page you get with airport WiFi.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;To me, this is a perfect demonstration of the power of frontier coding assistants. The activation energy of this project was high, and even then, it allowed me to quickly become more and more ambitious in a very short period of time.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I've had the Ulanzi TC001 sitting doing nothing other than tell the time in the year since I purchased it – another project component doomed to never get any of my time...&lt;/span&gt;&lt;/p&gt;&lt;h3 id="first-pass-awtrix-and-a-raspberry-pi" class="group relative"&gt;&lt;a href="#first-pass-awtrix-and-a-raspberry-pi" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;div class="web-embed-block web-embed-inline my-4" style="max-width: 550px; margin-left: auto; margin-right: auto; height: 300px;"&gt;&lt;iframe src="/files/f0ec5af5765bdfab.html" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy" style="width: 100%; height: 100%;" title="raspbi"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;span style="white-space: pre-wrap;"&gt;First pass: AWTRIX and a Raspberry Pi&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Initially I started out flashing it with the &lt;/span&gt;&lt;a href="https://github.com/Blueforcer/awtrix3" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;AWTRIX3&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; firmware, and running a Node.js service to update it over the network. It was maybe 30 minutes effort to put together, I showed it off to my cofounders and they loved it! We had just turned on the subscription payments system after iterating on the product with early customers over several months, and it was exciting to see people finding it useful enough to actually pay for it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I thought it would be nice if they could also have one of these, but the fact that you need basically a whole other server to do the custom logic made it a non-starter. AWTRIX is really meant for smart home hackers.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So I had a bright idea: maybe I can customise the firmware? Set it up so it just communicates with Stripe directly?&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Then I had a more fun idea: instead of modifying AWTRIX3, a feature-rich C++ project, how about I get Claude to help me write some firmware from scratch in Rust? Perhaps paring it back to the essentials would be simpler than trying to shoehorn my idea into an already complex system.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I had a vague impression that Rust was good for embedded systems development, but hadn't written any before, so this would really test just how far I could get using a coding assistant to build in a language and runtime environment (ESP32) I was new to.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The convenient thing about this project is that it is fairly unambiguous whether the system works or not –&amp;nbsp;plus it helps that the ESP32 boards are difficult to brick&lt;/span&gt;&lt;span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;, and &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;not too expensive&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; to replace.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I've been using AI coding assistants a lot over the last few months, and I was still genuinely shocked at how easy it was to get to a basic working state using Claude. However, I can guess at some ideas why it might have been more successful than I initially expected.&lt;/span&gt;&lt;/p&gt;&lt;ol&gt;&lt;li value="1"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;Rust&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;A compiled language that has a reputation for being expressive and thoughtfully documented, with safety features that perhaps makes it naturally amenable to the self-directed feedback loop of agents.&lt;/span&gt;&lt;/li&gt;&lt;li value="2"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;The simplicity of the hardware&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;the low resolution display and limited features of the system limits the design space. There's a lot you can do with an ESP32, a 32x8 LED matrix and a piezo buzzer –&amp;nbsp;but not &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;that&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; much.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It wasn't without bugs, of course. For example, initially, the screen glitched out continuously:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/66c0b9c20613f255.mp4" autoplay="" muted="" loop="" playsinline="" data-playback="silentLoop" title="glitch11.mp4" style="max-width: 100%; height: auto"&gt;&lt;/video&gt;&lt;/figure&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;This turned out to be due to "&lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;interrupt latency from WiFi interfering with the WS2812B signal generation in the RMT ISR&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;" – 😵‍💫&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;It actually looks pretty cool, and I'll have to keep it in mind if I want to generate an effect like that for some other project, but it's just distracting in this case.&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Once the features and basic setup were working, I was able to step back and start to think what would make it fun and easy.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="artwork" class="group relative"&gt;&lt;a href="#artwork" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Artwork&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I did not have any success creating actual pixel art with OpenAI. ChatGPT can create things &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;in the style&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; of pixel art, but which are actually thousands of pixels in width and height, but I could not get it to build something to the constraints of the TC001 display. Maybe it's possible, but I did not have the patience to find out.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;On the other hand, Claude Code Opus 4.6 seems to be quite capable of creating that minuscule artwork, and some of it looks quite good. However, I didn't realize Opus 4.6 could actually create images before I'd already hand drawn most of what I wanted using &lt;/span&gt;&lt;a href="http://pixilart.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;PixilArt&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="captive-portal" class="group relative"&gt;&lt;a href="#captive-portal" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Captive portal&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I wasn't really sure what was involved in creating a captive portal. I didn't even Google it before getting Claude to write it, and it turned out to be conceptually relatively straightforward: DNS interception. Your device connects to the access point, and whatever URL it requests, the AP responds with the configuration page.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;There are also some magic URLs that the OS checks for, which seem to trigger the familiar "popup" you get after connecting to a network that supports one:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper" style="width: 357px; margin-left: auto; margin-right: auto; text-align: center"&gt;&lt;img src="/files/8a0691c904941073.png" alt="Screenshot 2026-02-10 at 11.11.32.png" width="357" height="776.138671875" style="max-width: 100%; height: auto;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;All it really does is save the person setting it up the hassle of knowing what IP to visit in their browser, but it's a detail I really enjoy, since it's one of those technical curiosities that I've briefly wondered about – for however long it took to connect to the WiFi – then forgotten.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The native simulator includes a reasonably accurate replication of the TC001's integrated piezo buzzer, as well as a live Stripe API integration. This was an addition that I just took a punt on once the basic tent poles of the system were working.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;From the native simulator, it was a fairly straightforward path to compiling to Wasm, hence the little simulator at the top of the post.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It was fun having a device running in the corner of my office, beeping when a new subscriber arrived, but my original goal was to have a version I could hand to my cofounders. I had ordered another couple of the TC001s on AliExpress, and conveniently they arrived just a day or two before I was due to meet up with David and Clodagh.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This is the point at which I discovered that at some point since I got my own TC001 a year or so back, the manufacturer had apparently decided that 8MB of storage was overkill for a desktop clock, and the new models were shipping with only 4MB of flash memory. This required some slight rejigging of the code – which Claude was more than capable of – ditching my beloved OTA firmware rollback feature.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Another key component is, of course, setup instructions, which I included as a single printed page, with example screens:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/97506d6321126465.jpg" alt="IMG_6549.jpg" width="inherit" height="inherit" style="max-width: 100%; height: auto;"&gt;&lt;/figure&gt;&lt;div class="web-embed-block web-embed-inline my-4" style="height: 1100px; border: 1px solid rgb(229, 231, 235); border-radius: 0.5rem; overflow: hidden;"&gt;&lt;iframe src="/files/e3949aa697e9cec3.html" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy" style="width: 100%; height: 100%;" title="setup-guide"&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;David and Clodagh were delighted, and were fortunately both able to get the devices set up easily when they got home, and now we all have little chirping devices to tell us when new customers arrive!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;One thing I did overlook was that Clodagh tends to bounce between her home office, and a small office we rent, which means she needs multiple wifi connections configured. Will have to fix that in a future firmware update. For better or worse, I was not able to finish my planned auto-update functionality before handing the devices over. Given how new this is to me I am as likely to brick their devices remotely as improve them, so manual updates from the firmware page will do for now!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;PS: I created a tool to create self-contained HTML embeds from Twitter archives while writing this post. &lt;/em&gt;&lt;/i&gt;&lt;a href="https://github.com/duggan/tweetembed" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;You can find it here&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;section class="changelog"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;time datetime="2026-03-12T22:54:25.000Z"&gt;Mar 12, 2026, 10:54 PM&lt;/time&gt; — Fix layout issue.&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Make inaccessible, basically.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;A TC001 on AliExpress is something like €35 delivered.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/building-a-stripe-dashboard-with-an-esp32-desktop-clock-and-rust" rel="alternate"/>
    <published>2026-03-08T21:39:37.677Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Putting together a little desktop notifier for new Stripe subscribers using Rust! 🦀&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/microgpt-on-the-esp32-but-why</id>
    <title type="text">microgpt on the ESP32 – but... why?</title>
    <updated>2026-03-03T11:17:43.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/41a1e2505d620418.mp4" autoplay="" muted="" loop="" playsinline="" data-playback="silentLoop" title="esp32gpt_demo3.mp4" style="max-width: 100%; height: auto"&gt;&lt;/video&gt;&lt;/figure&gt;&lt;span style="white-space: pre-wrap;"&gt;Yesterday I was looking at the &lt;/span&gt;&lt;a href="https://zclaw.dev" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;zclaw project&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and I thought it would be cool if the ESP32 could run an actual language model instead of phoning out to OpenAI, Anthropic, or some other provider.&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Then I remembered that &lt;/span&gt;&lt;a href="https://karpathy.github.io/2026/02/12/microgpt/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Andrej Karpathy had dropped microgpt&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; a couple of weeks ago, and thought it might be fun to try and get a GPT running on the ESP32. I've got a couple of them lying around from a few other tinkering projects, as well as a functioning ESP32 Rust project to base it on.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I am very much the &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;dog-on-computer&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; meme regarding this stuff. I have trained and run LLMs for years, but I have not tried writing them. I have the general idea, tokenizers, transformers, gradient descent, yada yada, but they're mostly just words.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;With the blog post and my other project as a starting point, it was, however, pretty easy to get to a functional port of microgpt training and then generating a continuous stream of names on the ESP32!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://github.com/duggan/esp32gpt" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;The code is here&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and the Claude session that produced the first functioning variant of the project is below:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class="link-preview-card border border-gray-200 rounded-lg overflow-hidden my-4"&gt;&lt;a href="https://rockstar.ninja/s/7yM0cgfXOq8Z" target="_blank" rel="noopener noreferrer" class="no-underline text-inherit flex flex-row"&gt;&lt;div class="flex-1 p-3 min-w-0"&gt;&lt;div class="link-preview-site flex items-center gap-1.5 mb-2 text-xs text-gray-500"&gt;&lt;img src="/files/7c24b933e4c9df9e.svg" class="w-4 h-4 m-0"&gt;&lt;span&gt;rockstar.ninja&lt;/span&gt;&lt;/div&gt;&lt;div class="link-preview-title font-semibold text-base leading-tight mb-1"&gt;For a fun technical challenge, and based on our work in ~/Projects/stripe-dashboard, and the informa&lt;/div&gt;&lt;div class="link-preview-description text-sm text-gray-500 leading-snug line-clamp-2"&gt;opus 4.6 · haiku 4.5 · 92 turns · For a fun technical challenge, and based on our work in ~/Projects/stripe-dashboard, and the information in https://k...&lt;/div&gt;&lt;div class="text-xs text-gray-400 mt-2"&gt;rockstar.ninja&lt;/div&gt;&lt;/div&gt;&lt;div class="w-[200px] shrink-0 overflow-hidden bg-gray-100"&gt;&lt;img src="/files/23581c424049bc81.png" alt="For a fun technical challenge, and based on our work in ~/Projects/stripe-dashboard, and the informa" class="w-full h-full object-cover m-0"&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;&lt;span style="white-space: pre-wrap;"&gt;I also had a fresh session of Claude conduct a review, which spotted that the implementation was missing &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;RMSNorm&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;, and filled in the gaps:&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class="link-preview-card border border-gray-200 rounded-lg overflow-hidden my-4"&gt;&lt;a href="https://rockstar.ninja/s/WGkvb2mtPva1" target="_blank" rel="noopener noreferrer" class="no-underline text-inherit flex flex-row"&gt;&lt;div class="flex-1 p-3 min-w-0"&gt;&lt;div class="link-preview-site flex items-center gap-1.5 mb-2 text-xs text-gray-500"&gt;&lt;img src="/files/7c24b933e4c9df9e.svg" class="w-4 h-4 m-0"&gt;&lt;span&gt;rockstar.ninja&lt;/span&gt;&lt;/div&gt;&lt;div class="link-preview-title font-semibold text-base leading-tight mb-1"&gt;Let's do a thorough review of this repository, and evaluate whether it meets the goal of reproducing&lt;/div&gt;&lt;div class="link-preview-description text-sm text-gray-500 leading-snug line-clamp-2"&gt;opus 4.6 · haiku 4.5 · 48 turns · Let's do a thorough review of this repository, and evaluate whether it meets the goal of reproducing microgpt on the ...&lt;/div&gt;&lt;div class="text-xs text-gray-400 mt-2"&gt;rockstar.ninja&lt;/div&gt;&lt;/div&gt;&lt;div class="w-[200px] shrink-0 overflow-hidden bg-gray-100"&gt;&lt;img src="/files/9fce82853b325987.png" alt="Let's do a thorough review of this repository, and evaluate whether it meets the goal of reproducing" class="w-full h-full object-cover m-0"&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;h3 id="what-s-the-point" class="group relative"&gt;&lt;a href="#what-s-the-point" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;What's the point?&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Well, I learned that training a language model on an ESP32 is something that can be done. I did not know that yesterday. I also found this is not by any means the first or even most interesting attempt. &lt;/span&gt;&lt;a href="https://github.com/DaveBben/esp32-llm" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;David Bennett used a slightly larger ESP32 variant (1MB of RAM, still minuscule) to spit out 20 tokens a second of much more recognizable "LLM" output&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. Two years ago!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I think I've learned a little more than I'd have learned reading a blog post from &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;someone else&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; describing how they'd done it. I did a little post-Claude tinkering with stuff I'd remembered from the last ESP32 project (core pinning, RNG).&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Does this magically make me able to understand the math? No, I'd have to use my brain more for that, but I've read some of the code, and have given Karpathy's post a closer read than I might have done otherwise.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This was a shower thought that was turned into a working demo. A year ago, I'd have forgotten about it and moved on, or jotted something down in a list of rainy day projects that I would realistically never get around to.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Some people will find this unsatisfying, maybe even a quintessential example of the missed learning opportunity that happens when someone uses LLMs. I'm not sure I even disagree in principle. I just know that I was very unlikely to ever even &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;attempt&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; to do this. There are lots of random ideas that I'll never get around to.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The space of possible ideas is large, and time is finite. Maybe noodling on this for an hour or two will open me up to other possibilities. &lt;/span&gt;&lt;a href="https://en.wikipedia.org/wiki/The_old_man_lost_his_horse" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Too early to tell&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I played video games a lot as a teenager, and watched television, and both of these were going to rot brains. Brains are more resilient than they seem to get credit for.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;&lt;section class="changelog"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;time datetime="2026-03-03T17:02:22.000Z"&gt;Mar 3, 2026, 5:02 PM&lt;/time&gt; — Fixed a typo.&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;a href="https://duggan.ie/files/7d49752be1352668.jpg"&gt;[image: https://duggan.ie/files/7d49752be1352668.jpg]&lt;/a&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;(I have no idea what I'm doing)&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/microgpt-on-the-esp32-but-why" rel="alternate"/>
    <published>2026-03-03T16:57:32.445Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Yesterday I was looking at the &lt;/span&gt;&lt;a href="https://zclaw.dev" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;zclaw project&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and I thought it would be cool if the ESP32 could run an actual language model.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Then I remembered that &lt;/span&gt;&lt;a href="https://karpathy.github.io/2026/02/12/microgpt/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Andrej Karpathy had dropped microgpt&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; a couple of weeks ago, and thought it might be fun to try and get a GPT running on the ESP32. I've got a couple of them lying around from a few other tinkering projects, as well as a functioning ESP32 Rust project to base it on.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/cognitive-debt-in-ai-coding</id>
    <title type="text">Cognitive debt in AI coding</title>
    <updated>2026-02-15T10:25:18.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Parachuting into an unfamiliar codebase, where the original author is long gone, is an experience that will be familiar to a lot of developers.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;People writing with AI assistants have been encountering this in a new form.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Simon Willison wrote a brief post about it recently, concluding:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="rich-quote my-8"&gt;&lt;div class="relative px-12 py-6 bg-gray-50 dark:bg-gray-800/50 rounded-lg"&gt;&lt;span class="absolute left-3 top-3 text-6xl leading-none text-gray-300 dark:text-gray-600 font-serif select-none" aria-hidden="true"&gt;“&lt;/span&gt;&lt;span class="absolute right-3 bottom-3 text-6xl leading-none text-gray-300 dark:text-gray-600 font-serif select-none" aria-hidden="true"&gt;”&lt;/span&gt;&lt;blockquote class="italic text-lg leading-relaxed" style="border: 0; padding: 0; margin: 0; quotes: none;"&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I no longer have a firm mental model of what they can do and how they work, which means each additional feature becomes harder to reason about, eventually leading me to lose the ability to make confident decisions about where to go next.&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;figcaption class="text-sm text-gray-500 dark:text-gray-400 mt-4 not-italic flex items-center gap-1"&gt;&lt;span&gt;— Simon Willison&lt;/span&gt;&lt;a href="https://simonwillison.net/2026/Feb/15/cognitive-debt/" target="_blank" rel="noopener noreferrer" class="text-gray-500 dark:text-gray-400 hover:text-gray-300 underline decoration-dotted inline-flex items-center gap-1"&gt;simonwillison.net&lt;svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"&gt;&lt;/path&gt;&lt;polyline points="15 3 21 3 21 9"&gt;&lt;/polyline&gt;&lt;line x1="10" y1="14" x2="21" y2="3"&gt;&lt;/line&gt;&lt;/svg&gt;&lt;/a&gt;&lt;/figcaption&gt;&lt;/div&gt;&lt;/figure&gt;&lt;span style="white-space: pre-wrap;"&gt;Interestingly, this has been my experience in the large codebases of every company I've ever joined. Even with reasonably good documentation, it can be difficult to get to grips with any sufficiently large codebase – say, over a million lines.&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This can also be true of smaller systems. If you're new to programming, you will be learning what works best as you go, and so your codebase will change as you learn. Even an experienced developer can make plenty of design mistakes in a domain they are unfamiliar with.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Working with large, successful systems are the circumstances under which code review skills are built. Something true in one part of the system may not hold in another. Experienced developers have their own methods to build understanding, like creating or adding to test suites, extracting microservices, and cultivating the patience, persistence, and humility to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;read the code&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;What's new, though, is experienced developers needing these skills in their own personal projects.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Systems like Claude Code now have an integrated &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;MEMORY.md&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; that they can use for projects in order to persist comprehension across sessions. I am using it and &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;CLAUDE.md&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; religiously in order to build persistent knowledge of the system into each project&amp;nbsp;–&amp;nbsp;but it's early days.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Coding assistants are taking people from idea to working system much faster than before. They are reaching the points at which design and testing become valuable much more rapidly. Not only that, but it's also possible to turn many ideas that might have remained pleasant daydreams into reality, and dreams brought into reality can sometimes turn out to be nonsense.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This may even tie into at least some of the sense of burnout that people have been reporting. It can be comforting and intrinsically rewarding to tinker away on a codebase, but if you're building a business, or at least software for other people, code is just part of it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It can be quite disheartening to realise that customers won't just appear out of thin air. Building for yourself is fun, but building for &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;nobody&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; is pointless. Claude Code isn't talking to your customers. It's not being thoughtful about the problem. It doesn't have a strong sense of aesthetics.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It doesn't know that what you &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;don't&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; build can be as important as what you &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;do&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; build.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;All sorts of people, developers included, are finding out that coding is not software engineering, and that software projects are not startups. This is not new information, but it can take a long time for it to sink in. If people are able to get to that lesson faster, then it's a good thing.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/cognitive-debt-in-ai-coding" rel="alternate"/>
    <published>2026-02-15T17:37:53.733Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Parachuting into an unfamiliar codebase, where the original author is long gone, is an experience that will be familiar to a lot of developers.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;People writing with AI assistants have been encountering this in another form.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Simon Willison wrote a brief post about it recently, concluding, "I no longer have a firm mental model of what they can do and how they work, which means each additional feature becomes harder to reason about, eventually leading me to lose the ability to make confident decisions about where to go next."&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/i-built-a-terminal-monitoring-app-and-custom-firmware-for-a-desktop-clock-with-claude</id>
    <title type="text">I built a terminal monitoring app and custom firmware for a desktop clock with Claude</title>
    <updated>2026-02-07T16:17:21.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The way that I've used AI for coding has changed drastically over the last year.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In fact, the rate at which it is changing is probably the most drastic element of it –&amp;nbsp;I can't recall any time since my first year of college, twenty years ago, that I've experienced such a rapid evolution in my own ability to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;do stuff&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This week I have written a fully self-contained system monitoring daemon and terminal UI, based on the &lt;/span&gt;&lt;a href="http://charm.land" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Charm&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; toolkit and using &lt;/span&gt;&lt;a href="http://duckdb.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;DuckDB&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. It archives metrics to parquet, shows ECC status, SMART health, temperature, sparklines, historical charts, and even has an alerts system.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/a9aaa6b3317e5fae.mp4" autoplay="" muted="" loop="" playsinline="" data-playback="silentLoop" title="demo.mp4" style="max-width: 100%; height: auto"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;bewitch comes in one colour: hot pink.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I am passingly familiar with Go, but this is a project that's been in the back of my head for a while, and would have been a monumental undertaking. It already works well enough, and I'm using it for a handful of servers.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Also this week, I built custom firmware to show Stripe subscription metrics for the Ulanzi TC001. I've linked to a good overview below if you're not familiar with it – basically a cheap and hackable desktop clock.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class="link-preview-card border border-gray-200 rounded-lg overflow-hidden my-4"&gt;&lt;a href="https://blakadder.com/ulanzi-pixel-clock/" target="_blank" rel="noopener noreferrer" class="no-underline text-inherit flex flex-row"&gt;&lt;div class="flex-1 p-3 min-w-0"&gt;&lt;div class="link-preview-site flex items-center gap-1.5 mb-2 text-xs text-gray-500"&gt;&lt;img src="/files/148396ba0565f6b4.png" class="w-4 h-4 m-0"&gt;&lt;span&gt;Blakadder’s Smarthome Shenanigans&lt;/span&gt;&lt;/div&gt;&lt;div class="link-preview-title font-semibold text-base leading-tight mb-1"&gt;Ulanzi Desktop Pixel Clock TC001 Review&lt;/div&gt;&lt;div class="link-preview-description text-sm text-gray-500 leading-snug line-clamp-2"&gt;As I was trawling AliExpress in search for a decent but cheap photography light I found Ulanzi, a Chinese brand specialized in making video and photography equipment. Browsing their product list something peculiar jumped out in the recommended tab. A desktop pixel clock targeted to YouTubers, the Ulanzi TC001, priced at 59.99$. After various sales and coupons the price dropped down to a very affordable 43$ and that made me click the buy button.&lt;/div&gt;&lt;div class="text-xs text-gray-400 mt-2"&gt;blakadder.com&lt;/div&gt;&lt;/div&gt;&lt;div class="w-[200px] shrink-0 overflow-hidden bg-gray-100"&gt;&lt;img src="/files/9f1bede6662d0c51.jpg" alt="Ulanzi Desktop Pixel Clock TC001 Review" class="w-full h-full object-cover m-0"&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;&lt;span style="white-space: pre-wrap;"&gt;This is the kind of project I have dreamed about putting together, but I've never found the time required to become familiar with firmware development outside of a few Arduino demos many years ago.&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Most of these project ideas go to a list of ideas that I look at and go, "some day, if I have the time." –&amp;nbsp;I rarely have the time. This blog was one such project, and I sat on it for ten years!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://blueforcer.github.io/awtrix3/#/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;AWTRIX3&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; is the go-to firmware for customizing the Ulanzi TC001, but customization requires running a process on another machine and sending data over the network to the device. I wanted something I could bundle up and send to my other cofounders. So, again, I leaned on Claude Code to create custom firmware in Rust that communicates with the Stripe API, providing a subscriber count, MRR, and notification for new subscribers. It runs a wifi access point that you can connect to and configure, after which it connects to the wifi and communicates with Stripe via their API.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Ironically, given how long generative AI has been threatening art, I ended up having to do a lot of the actual pixel art for the device by hand. Precise pixel art is one area chat models remain mysteriously terrible at.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/88013866aee053d5.mp4" autoplay="" muted="" loop="" playsinline="" data-playback="silentLoop" title="clock-demo-1.mp4" style="max-width: 100%; height: auto"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;The Stripe subscription firmware shown here configured with a demo sandbox to test everything out.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I have never written a line of Rust, but I know it is a relatively unforgiving language, and the ESP32 is a less forgiving environment than most I've developed for, with 8MB of flash storage and something like 160KB of RAM.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;These are two projects that would have remained on the back burner forever, requiring activation energy that I just don't have to spare. They were both written over the course of a few days, here and there, between other work, and recovering from a cold.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="a-year-ago" class="group relative"&gt;&lt;a href="#a-year-ago" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;A year ago&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When I was writing the engine that powers this blog in December 2024, I would upload a few files at a time through the Claude or OpenAI web UIs, try to get them to reshape them into something that seemed close enough to what I wanted, and then iterate on that work myself.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This was slow, but the project of putting together a custom editor using Lexical was daunting enough that I would not have attempted it otherwise. At that point I had only about six months of modern frontend development experience and &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;my reach exceeded my grasp&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Then the startup I was working for ran out of runway, we shut down the company, I spent a couple of months learning Japanese, and then &lt;/span&gt;&lt;a href="https://duggan.ie/posts/traveling-to-japan-for-three-months-because-lifes-too-short" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;went bumming around Japan for a few months with my partner Ash&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Fast forward to August 2025, I'm starting a new company, and coding assistants are starting to generate a good bit of heat on Hacker News.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;One of the first things I did after getting back was try to make some headway on my cofounder's MVP, and it seemed like a perfect opportunity to try out Claude Code. I had it write a few helper scripts, add logging, and act as a glorified find-replace in a few areas. For this tedious work it was quite useful, despite frequently giving up or failing the tasks.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I had it write the first draft of a feature that we needed, and iterated on it myself, getting Claude to throw in some fixes here and there.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Actually, it now reminds me of the accounts recorded by the engineers using the first computer being built at Princeton in the 1940s (from &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Turing's Cathedral&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, by George Dyson).&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Essentially, the ENIAC often produced incorrect calculations, overheated and shut down during jobs, and was generally a hassle to work with. Despite all that, it was still better than doing it by hand, turning what would be months of human calculation into mere weeks of computer assisted work.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="december-2025" class="group relative"&gt;&lt;a href="#december-2025" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;December 2025&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;By December, I had written one major piece of functionality with Claude Code. Arguably I would have done a better job doing it by myself, maybe in a similar timeframe if I had been really dialled in and had no distractions.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;All of my experience was with Sonnet at this point, and while it was useful and sometimes surprisingly good, it wasn't a huge accelerant. If pushed, I'd have said something like 1.5x, maybe. My €22/month usage plan was enough to help me write helper scripts and break ground on new features though, which was already worth it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;At the end of November though, Opus 4.5 had been released, and some time in December I decided to give it a try. It didn't seem massively different at first, but I was still working with it like it was Sonnet –&amp;nbsp;giving it small tasks, writing scripts, etc. It absolutely burned usage though, and until Opus I had never run into a limit before my usage counter reset. I literally didn't even realise there &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;was&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; a limit.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It wasn't until I started asking it to implement features that I found it was making far fewer mistakes, and was in fact producing relatively good first passes.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In October I had begun adding Github Copilot to some pull requests, and it had found a few issues. This was great, even if it came with some mistakes and noise. When you're a team of one and a half developers, an extra set of eyes is absolutely worth the trouble.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Eventually the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;Co-authored-by: Copilot&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; notes start showing up more frequently in the commit logs, and I found it useful enough to pay for the basic subscription once my free tier limits ran out.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;By the end of December, I was hitting my Claude Code limits constantly. I started using "extra usage" which lets you spend a little more to get to the end of whatever was being worked on when the included limits were reached.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I experimented with using GPT-5 and Opus via the Copilot extension included with VSCode. Once I run through the limits there, I start going through extra usage on that.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/210bc16b22348def.png" alt="Metered usage.png" width="inherit" height="inherit" style="max-width: 100%; height: auto;"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;The point at which Copilot started being very useful.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Using Copilot, I built the first application that I have not read line-by-line, &lt;/span&gt;&lt;a href="https://duggan.ie/vat-invoice-generator" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a VAT invoice generator&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. I needed to generate mock invoices to do some basic interactive testing of the OCR flow for Manano. As long as it generated something that looked plausible, it was fine, but I found that with GPT-5 doing the bulk of the actual coding, I was able to freewheel on ideas about design and local-first software.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I wanted it to be like &lt;/span&gt;&lt;a href="https://tiddlywiki.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Tiddlywiki&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, where it could be used as a file on the desktop as easily as something at a URL. I wanted it to be WYSIWYG rather than a set of dull forms to fill out. And having a bunch of visually distinctive templates to make sure a variety of visual styles would work.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="christmas-break-structured-prompts" class="group relative"&gt;&lt;a href="#christmas-break-structured-prompts" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Christmas break, structured prompts&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Over the Christmas period, while Ash was visiting her family, I had the opportunity to get stuck into the feature idea backlog for my blog. This felt like a good testing ground for the coding assistants, as it was a bit cobbled together, had gone through a few architectural lurches, but I had written enough of the codebase myself to evaluate the results.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Also, being a blog, the stakes were low. Every time I'd have some idea for the blog I'd jot it down in Apple Notes. Here was what I had built up:&lt;/span&gt;&lt;/p&gt;&lt;aside class="callout my-6"&gt;&lt;div class="border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 px-6 py-4 rounded-r-lg"&gt;&lt;strong class="block text-sm font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300"&gt;duggan.ie features/bugs&lt;/strong&gt;&lt;div class="mt-2 text-base leading-relaxed"&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Saving/updating published posts&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;When a post has been published, any subsequent updates should include an optional reason for the update.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;If a post is published, hitting save or using the save shortcut (Cmd+S) should pop up a dialog with a textbox to describe the reason for the change. This should be plaintext only, no special formatting.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;It should still be possible to change without including a reason. In that case, the update should not change the updated_at marker.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;The “save” target should move to the dialog box, so that hitting Cmd+S again will save the post.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Changelog&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Include the post updates in published posts.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;It should be implemented as a collapsed summary + details element at the bottom of the post.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;It should also be included in the Atom feed.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Feedback system for shared drafts&lt;/strong&gt;&lt;/b&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Goal: Create a feedback system for shared links.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Context:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Shared links are designed to be shared with a single person. The URLs are deliberately obfuscated and they have configurable expiry, after which they return 404.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;The editor is built using Lexical, and stores the lexical node data in the post_revisions table, with a posts table to hold the rendered html and reference to the currently fresh post_revisions record.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Implementation details:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Implement the feedback system as a Lexical plugin.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;The shared links can use a Lexical editor with our feedback system plugin.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Other than using the feedback system, the shared link editor should be read-only – the user should just be making notes, not editing the text directly.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Each shared link will likely need to make a copy of the main editor node data on creation.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Open questions:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;How best to handle when the main editor updates the post data. Should the shared links be updated? Should the user get a note/bar at the top of the screen saying something like “the author has updated the draft, would you like to refresh?” etc.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Opengraph integration for links&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;When pasting a link into the editor, it should give me the option to include it as nicely formatted block with OpenGraph data (image, title, content, etc) like Notion.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Video/image loading skeleton&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Context: images and videos can be dragged and dropped into the editor, and are displayed inline.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Goal: While the images are uploading, there should be a loading skeleton in place to show that it is being processed.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Enhanced video/image resizing controls&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Context: image and video resizing in the editor is very basic&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Goal: enhance the resize functionality so that arbitrary resizing is supported.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Additional support: helpful resize controls, like “original”, “full width”, etc., should be included&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Light / Dark mode&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Initial change to light mode seems to result in a white screen&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Continue button flashes when changing, sometimes on loadMake tags distinctive&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Tag management area&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Include emoji / stickers / SVG images as part of the tags&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Should be able to specify a colour for each tag using a colour wheel, etc.&lt;/span&gt;&lt;br&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;V2&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;More comprehensive emoji selector (with keyword search)&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Post list&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Add a new post status: archived&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;The post list navigation should be broken out by published vs draft vs archived in the left navigation.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It was quite a decent chunk of work in my book.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;These notes were originally just for my own reference, but you'll notice that at some point I started editing the notes with a basic structure for Claude Code that I felt was clearer.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I thought I'd make good headway on a few of them, but over the Christmas break I steamrolled &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;every one of them&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, iterated on a few, and even added some extras once I ran out.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I started out just about coming up short on Claude limits, having to wait 30 minutes or so before starting again. But by the end of this process I was smashing through those limits really quickly, and had to start getting more targeted and careful about what I wanted to work on.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Eventually though, I was paying extra usage on both Copilot and Claude Code, still cheaper than springing for a higher usage plan, but chafing at the limits.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="the-yegge-inflection-point" class="group relative"&gt;&lt;a href="#the-yegge-inflection-point" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;The Yegge Inflection Point&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This month, I finally sprung for Claude Code Max –&amp;nbsp;though only 5x, not yet the 20x. It's just too powerful an upgrade on my capabilities as a developer to pass up. Other people can wait to see where it goes and look back on this period with sagacity and the wisdom of hindsight.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I am still using Claude Code interactively in VSCode, I haven't gotten to the stage where I &lt;/span&gt;&lt;a href="https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;never read the code that's being produced&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, though maybe I'll get there, too. Steve Yegge is there with a few other people, and they are probably living in &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;a&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; version of the future, but it's not the &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;only&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; version of the future.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;There are lots of different kinds of software. Not all of it will be amenable to this style of development.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For a 20 year old aspiring software developer in college, I have no idea how they are going to get to their version of &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;mastery&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;. I suspect it will be very different to mine, and will take a very different route. I think it will be daunting, but I hope exciting. Nobody starts off knowing how everything works.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For me, it's exciting. If at the end of the day all I end up with is a bunch of completed side projects, it will have been money well spent.&lt;/span&gt;&lt;/p&gt;&lt;section class="changelog"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Changelog:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;time datetime="2026-02-08T23:21:38.000Z"&gt;Feb 8, 2026, 11:21 PM&lt;/time&gt; — Fixed formatting in notes.&lt;/li&gt;&lt;/ol&gt;&lt;/section&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;...to paraphrase Robert Browning.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;For want of a slightly less pretentious term. Basically I just mean skilled and experienced enough to be useful at their craft. Being able to dive deep, take responsibility, adapt and learn on the fly.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/i-built-a-terminal-monitoring-app-and-custom-firmware-for-a-desktop-clock-with-claude" rel="alternate"/>
    <published>2026-02-07T21:42:11.094Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The way that I've used AI for coding has changed drastically over the last year.&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In fact, the rate at which it is changing is probably the most drastic element of it –&amp;nbsp;I can't recall any time since my first year of college, twenty years ago, that I've experienced such a rapid evolution in my own ability to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;do stuff&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;This week I have written a fully self-contained system monitoring daemon and terminal UI, as well as custom Rust firmware for the Ulanzi TC001 desktop clock.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;These are two projects that would have remained ideas, never making it out of my notes folder.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/self-hosting-git-and-builds-without-running-a-bunch-of-web-services</id>
    <title type="text">Self-hosting git and builds without running a bunch of web services</title>
    <updated>2026-01-05T20:59:48.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;My main side project at the moment is this blog. I tinker away with it when I feel like it. I'm not working on it with anyone else, not fielding pull requests, and not writing documentation.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;That said, I do still like having it in version control. I like having a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;Dockerfile&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; to encapsulate dependencies and configuration, and using docker compose as a portable runtime manager. It's probably not everyone's idea of a simple setup, but I like it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The blog is deployed to a small VPS in &lt;/span&gt;&lt;a href="http://hetzner.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Hetzner&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, running behind nginx.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;GitHub, Github Actions, and the GitHub Container Registry have been how I've been handling builds, but it's been slow, and it's started to seem absurd to have all these builds occurring in some random datacenter presumably on the other side of the Atlantic ocean, that I am then pulling back to a server in Europe.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I started fishing around for a locally hosted replacement, coming across &lt;/span&gt;&lt;a href="http://forgejo.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Forgejo&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="http://woodpecker-ci.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Woodpecker CI&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and &lt;/span&gt;&lt;a href="https://onedev.io" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;OneDev&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; in addition to more familiar names like &lt;/span&gt;&lt;a href="https://gitlab.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;GitLab&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;a href="http://sourcehut.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;SourceHut&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;At some point it occurred to me that this was a lot of ceremony for something relatively simple. How hard could it be just to have a git remote and hang some builds off it? I'm not trying to launch my own GitHub here.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I want to:&lt;/span&gt;&lt;/p&gt;&lt;ol&gt;&lt;li value="1"&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;git push&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; to a &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;central&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; repo&lt;/span&gt;&lt;/li&gt;&lt;li value="2"&gt;&lt;span style="white-space: pre-wrap;"&gt;When I push, it kicks of a container image build&lt;/span&gt;&lt;/li&gt;&lt;li value="3"&gt;&lt;span style="white-space: pre-wrap;"&gt;That image is pushed to an image registry that I can deploy from&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This does not need several UIs and databases. In any case git being git, you can always switch out for something more complex later.&lt;/span&gt;&lt;/p&gt;&lt;aside class="callout my-6"&gt;&lt;div class="border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 px-6 py-4 rounded-r-lg"&gt;&lt;strong class="block text-sm font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300"&gt;Note&lt;/strong&gt;&lt;div class="mt-2 text-base leading-relaxed"&gt;&lt;p class="mb-0"&gt;One piece of plumbing that makes all this much more flexible is Tailscale. It is not strictly required, but having the various machines on the same private network, regardless of their physical location, makes some parts easier.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;&lt;h3 id="hosting-a-git-repo" class="group relative"&gt;&lt;a href="#hosting-a-git-repo" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Hosting a git repo&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;On GitHub, an SSH clone URL looks something like:&lt;/span&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;git@github.com:duggan/duggan.ie.git&lt;/span&gt;&lt;/code&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It's an SSH URI just like it would be with &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;scp&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; or &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;rsync&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;. A helpful breakdown from &lt;/span&gt;&lt;a href="https://stackoverflow.com/a/70330178" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Mike Slinn and Jaditpol on StackOverflow&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4"&gt;&lt;span style="white-space: pre-wrap;"&gt;    git@github&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;com&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;myuser&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;myrepo&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    \_&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; \________&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; \_______________&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;     &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;|&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;|&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;              &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;|&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    user   host           path&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Since I'm the only user, I don't need to get fancy, and I can just use my own login.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;origin&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; of a git repo is more or less just the contents of the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;.git&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; directory in a remote location. That's it. You don't even need to run a git server if you're happy enough using ssh for transport.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I put it outside my home path just to keep it a little out of the way, as I won't need to interact with it too often:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6"&gt;&lt;span style="white-space: pre-wrap;"&gt;sudo mkdir &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;p &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;srv&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;sudo chown &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;R&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;USER&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;$&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;USER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;srv&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;cd &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;srv&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;mkdir example&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;cd example&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;git init &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;bare&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In my case I've replaced my GitHub &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;origin&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; with this one directly in the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;.git/config&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; of my local checkout, but when I was testing the waters I just added an additional remote:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1"&gt;&lt;span style="white-space: pre-wrap;"&gt;$ git remote add test ross@buildmachine&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;srv&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;duggan&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;ie&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="building-an-image-on-push" class="group relative"&gt;&lt;a href="#building-an-image-on-push" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Building an image on push&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Git's own hooks system, combined with a Makefile and Docker, is enough to put together a pretty flexible build system for anything you can deploy using a container.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In my newly minted upstream repo, I added a file named &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;post-receive&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; into the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;hooks&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; directory, i.e., &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;/srv/git/duggan.ie.git/hooks/post-receive&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; (and &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;chmod +x&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; it):&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38"&gt;&lt;span style="white-space: pre-wrap;"&gt;#!/bin/bash&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;set &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;e&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;# Read the push &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;info&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;branch being updated&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;while&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; read oldrev newrev refname&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;do&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; # Only trigger on main branch&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $refname &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;==&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"refs/heads/main"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; then&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; echo &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"=== Build triggered for main branch ==="&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # Get repo name from current directory&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPO_NAME&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;basename &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$(pwd)"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WORK_TREE&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"/tmp/git-build-${REPO_NAME}"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;GIT_DIR&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;pwd&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # Clean checkout&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rm &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;rf &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$WORK_TREE"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; mkdir &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;p &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$WORK_TREE"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; git &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;work&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;tree&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$WORK_TREE"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;git&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;dir&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$GIT_DIR"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; checkout &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;f main&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; cd &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$WORK_TREE"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # Check &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Makefile and build target&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;f Makefile &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; then&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; grep &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;q &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"^build:"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Makefile&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; then&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; echo &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"=== Running make build ==="&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; make build&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; echo &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"=== Build complete ==="&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;else&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; echo &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Makefile found but no 'build' target, skipping"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fi&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;else&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; echo &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"No Makefile found, skipping build"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; fi&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; # Cleanup&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; rm &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;rf &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"$WORK_TREE"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; fi&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;done&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When I push to the repo, this looks for a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;Makefile&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; in the root of the repo, checks for a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;build&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; command and executes it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/4ab4a809cd6e7fa2.mp4" autoplay="" muted="" loop="" playsinline="" data-playback="silentLoop" title="push1.mp4" style="max-width: 100%; height: auto"&gt;&lt;/video&gt;&lt;/figure&gt;&lt;span style="white-space: pre-wrap;"&gt;My &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;Makefile&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; is super basic, just:&lt;/span&gt;&lt;p&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IMAGE&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;localhost&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;5000&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;duggan&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;ie&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;latest&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;build&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;docker build &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;t &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IMAGE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;docker push &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IMAGE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;As a bonus, I could have thrown that post-receive script into a &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;git template&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; so that new repos are automatically created with a copy of the post receive script, but it doesn't seem necessary just yet.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;On the build machine I've also enabled my user account to run docker directly without requiring &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;sudo&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;div class="link-preview-card border border-gray-200 rounded-lg overflow-hidden my-4"&gt;&lt;a href="https://docs.docker.com/engine/install/linux-postinstall/" target="_blank" rel="noopener noreferrer" class="no-underline text-inherit flex flex-row"&gt;&lt;div class="flex-1 p-3 min-w-0"&gt;&lt;div class="link-preview-site flex items-center gap-1.5 mb-2 text-xs text-gray-500"&gt;&lt;img src="/files/7d2f187075a49630.svg" class="w-4 h-4 m-0"&gt;&lt;span&gt;Docker Documentation&lt;/span&gt;&lt;/div&gt;&lt;div class="link-preview-title font-semibold text-base leading-tight mb-1"&gt;Post-installation steps&lt;/div&gt;&lt;div class="link-preview-description text-sm text-gray-500 leading-snug line-clamp-2"&gt;Find the recommended Docker Engine post-installation steps for Linux users, including how to run Docker as a non-root user and more.&lt;/div&gt;&lt;div class="text-xs text-gray-400 mt-2"&gt;docs.docker.com&lt;/div&gt;&lt;/div&gt;&lt;div class="w-[200px] shrink-0 overflow-hidden bg-gray-100"&gt;&lt;img src="/files/9fdaa65db57e044d.webp" alt="Post-installation steps" class="w-full h-full object-cover m-0"&gt;&lt;/div&gt;&lt;/a&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;h3 id="hosting-an-image" class="group relative"&gt;&lt;a href="#hosting-an-image" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Hosting an image&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;At this point the image is built, which will be fine if I just wanted to run the container on the same machine, but I need to deploy this to my little Hetzner VPS.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This is where Tailscale comes in handy, as it lets me have Docker's own &lt;/span&gt;&lt;a href="https://distribution.github.io/distribution/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;container registry&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; running on my build server, which I then pull from on the VPS. It's very straightforward to set up.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;On the build server I have a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;docker-compose.yml&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; file with the registry configured:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11"&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;services&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; registry&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; image&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; registry&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;2&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; container_name&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; registry&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; restart&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; always&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; ports&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"5000:5000"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; volumes&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;data&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;var&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;lib&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;registry&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; environment&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REGISTRY_STORAGE_DELETE_ENABLED&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"true"&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="deploying-from-a-private-registry" class="group relative"&gt;&lt;a href="#deploying-from-a-private-registry" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Deploying from a private registry&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;With the registry and VPS both on the Tailscale network, I can allow regular HTTP traffic, which requires a small tweak to Docker on the VPS.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;/etc/docker/daemon.json&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3"&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"insecure-registries"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"buildmachine:5000"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;With that done, I'm able to update my &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;make deploy&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; command to point at the new registry:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4"&gt;&lt;span style="white-space: pre-wrap;"&gt;services&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; web&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp; &amp;nbsp; image&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; buildmachine&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;5000&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;duggan&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;ie&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;latest&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This all works much more quickly than before, and since it's running the builds over SSH directly, I'm getting feedback faster than I would refreshing a build log.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;I code from a couple of different machines, so having an actual central remote upstream makes things easier.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;You could probably do a lot of this on just your local machine with a few tweaks.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;For example, you can throw `post-receive` into a directory at `/srv/git/template` then configure via the git config:&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;git config --global init.templateDir /srv/git/template&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/self-hosting-git-and-builds-without-running-a-bunch-of-web-services" rel="alternate"/>
    <published>2026-01-12T00:12:06.449Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;GitHub, Github Actions, and the GitHub Container Registry have been how I've been handling builds, but it's been slow, and it's started to seem absurd to have all these builds occurring in some random datacenter presumably on the other side of the Atlantic ocean, that I am then pulling back to a server in Europe.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I started fishing around for a locally hosted replacement, coming across &lt;/span&gt;&lt;a href="http://forgejo.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Forgejo&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="http://woodpecker-ci.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Woodpecker CI&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and &lt;/span&gt;&lt;a href="https://onedev.io" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;OneDev&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; in addition to more familiar names like &lt;/span&gt;&lt;a href="https://gitlab.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;GitLab&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;a href="http://sourcehut.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;SourceHut&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;At some point it occurred to me that this was a lot of ceremony for something relatively simple. How hard could it be just to have a git remote and hang some builds off it? I'm not trying to launch my own GitHub here.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/new-years-resolutions-2026</id>
    <title type="text">New Year's Resolutions 2026</title>
    <updated>2026-01-02T12:30:12.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/dc649adad4a791dc.jpg" alt="resolutions.jpg" width="inherit" height="inherit" style="max-width: 100%; height: auto;"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;It was impossible for me not to include this image. I am not sorry.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I was never big on New Year's resolutions. However, as I get older I appreciate that there are natural moments of reset in your life. New decades, new jobs, new homes.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;New years are one of the predictable ones. Conveniently, they come with a built in universal counter and even a glass of champagne and fireworks at the end. Might as well take advantage of the infrastructure.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So, I've thought about what I've been doing over the last while that I'd like to continue doing, do better, or do &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;less&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, and come up with what feels like a pretty achievable list:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li value="1"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Run 5km twice weekly&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="2"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Weights twice weekly&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="3"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Eat less meat&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="4"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Drink less, and less frequently&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="5"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Read more adventurously&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="6"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Write every day&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;li value="7"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Publish every week&lt;/em&gt;&lt;/i&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Some of these are measurable, some a bit more vague. &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Drink less&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, for example. I'll get into my rationale below.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="run-5km-twice-weekly" class="group relative"&gt;&lt;a href="#run-5km-twice-weekly" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Run 5km twice weekly&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The idea of running 5 kilometres regularly, and trying to improve my time consistently is far more appealing than aiming for a 10k or marathon, etc.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I've had a couple of false starts at running. It seemed pretty simple: leave house, run.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;But if you just start running without warming up you can do damage. I'm assuming this is something that changes as you age, and so the first time I started running I ran like I would have run for the bus when I was 17 –&amp;nbsp;except this time I was 30 and sedentary. Instant plantar fasciitis. Could barely walk for a week, then hobbled for months. End attempt one.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So now I've got decent running shoes, and after a slow build up got to a decent frequency of running in 2025, interrupted significantly by my three month trip to Japan with Ash. No such once-in-a-lifetime trips planned this year, so back to a &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;regular cadence&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; it is!&lt;/span&gt;&lt;/p&gt;&lt;h3 id="weights-twice-weekly" class="group relative"&gt;&lt;a href="#weights-twice-weekly" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Weights twice weekly&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I've solved a few health issues just by taking up some regular weight training. Nothing spectacular, 15-20 minute sessions with a bench and some adjustable weights. It's been genuinely life changing though. I've been doing it for long enough that it feels like baseline. If I miss it for a few weeks I can feel my shoulders starting to develop twinges. My main goal there is to not let that happen.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="eat-less-meat" class="group relative"&gt;&lt;a href="#eat-less-meat" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Eat less meat&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This one has been on my todo list for a couple of years. Eating meat has various links to cancer, and eating plants has a lot of positive correlations. I can eat less meat and eat more plants, and hopefully end up healthier overall. I also sporadically feel guilt over an animal dying for my food. Not truly enough to stop entirely, but enough that I would like to be some percentage less part of the problem.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt; I've already made pretty good progress, but it's taking me time to build up a repertoire of vegetarian meals that I want, and make the time to prepare them. I mostly have porridge in the mornings now, and a vegan soup for lunch. Dinners have remained elusive though, and that's what I plan to improve this year. &lt;/span&gt;&lt;/p&gt;&lt;h3 id="drink-less-and-less-frequently" class="group relative"&gt;&lt;a href="#drink-less-and-less-frequently" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Drink less, and less frequently&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt; A cop-out, you might say–&amp;nbsp;cold turkey or no turkey! But I'm just not convinced that just cutting out alcohol entirely is the best thing for me. I like drinking socially, I just don't like the increasingly long hangovers that result from overdoing it. Being wooly headed from poor sleep, or suffering a pounding headache, is just not a price I'm willing to pay as often. I'll drink on special occasions, or try &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;zebra striping&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;, and basically just make a more concerted effort to wind down my alcohol consumption.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="read-more-adventurously" class="group relative"&gt;&lt;a href="#read-more-adventurously" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Read more adventurously&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I am an almost embarrassingly single-genre reader, but I've not found myself moved by much that makes the top lists of science fiction for the last few years. I'm probably not checking the right lists. I do still like a well-structured space opera, but my favourite novel in recent memory is &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;The Dispossessed&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, by Ursula K. Le Guin. I found it gripping – beautifully written, desperately and probably eternally relevant, and a revelatory exploration of anarchism.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;It is a book from an author who is considered to transcend genre, and it has made me want to dig deeper to find other novels that require critical engagement, both inside science fiction and in broader literature.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;To that end, my first read of the year is M. John Harrison's &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Light&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, which is certainly turning out to be a worthy contender.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="write-every-day" class="group relative"&gt;&lt;a href="#write-every-day" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Write every day&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I'd like to write more,&lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt; always have, and I keep putting it off&lt;/span&gt;&lt;sup&gt;[3]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;. I'm starting to realise now, though,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; that not only am I not immortal, but in fact, am &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;increasingly mortal&lt;/span&gt;&lt;sup&gt;[4]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;. Anything I'm not immediately working towards is not likely to happen.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I can do a few things with this information. I can be angry about it, deny it, make my peace with it, or I can get working. Maybe all of those things in order.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="publish-every-week" class="group relative"&gt;&lt;a href="#publish-every-week" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Publish every week&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Weirdly I think this will be the hardest one.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;About the only saving grace here is that I am a working software developer and have a close and wry understanding that any software which is not released essentially does not exist.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Another lesson is that releasing software is one step of a many-stepped process when building a product. Not even necessarily the first step, depending on how you want to think about it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Despite knowing this I just don't post anything that feels too incomplete. Yet occasionally I re-read those posts and wonder whether they could ever be more complete than they already were.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Publishing a post feels like a big step, and it is in one sense, but it's also just one step. You can fold in whatever you want after it; sharing the post around, corrections, additions, or responding to feedback/comments.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;But the real outcome of publishing a blog post is mostly nothing external. Maybe some people read it, maybe nobody does. But the act of publishing itself is a useful line to draw under it. A way of moving on to the next thing.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Ultimately that's what we get out of a New Year's resolution, too. Draw a line under the last year, and make some space for a change.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Pun intended. Running jokes! Ho ho ho. 🏃‍♂️&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;b&gt;&lt;strong style="white-space: pre-wrap;"&gt;Zebra striping is when you alternate between alcoholic and non-alcoholic drinks.&lt;/strong&gt;&lt;/b&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Like the black and white stripes of a zebra, this method creates a pattern that helps moderate your drinking. Because of this technique, you’ll find yourself able to still enjoy social occasions that typically involve alcohol.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;via &lt;/span&gt;&lt;a href="https://www.drinkaware.co.uk/news/what-is-zebra-striping-and-why-is-it-becoming-popular" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;drinkaware.co.uk&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;I decided when I was young that the only chance of succeeding at either writing or software engineering, would be to put all my effort into one at the expense of the other. Software won. Not because I thought it was a hot career, but because I thought it would be easier to get a job with than writing. It was 2003, and software was interesting to me, but not seen as prestigious or valuable amongst my peers.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;As far as I know, I'm the only person that pursued software engineering in my entire year of ~150 male students.&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Hence the health goals 😅&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/new-years-resolutions-2026" rel="alternate"/>
    <published>2026-01-04T15:51:36.345Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I was never big on New Year's resolutions. However, as I get older I appreciate that there are natural moments of reset in your life. New decades, new jobs, new homes. New years are one of the predictable ones. Conveniently, they come with a built in universal counter and even a glass of champagne and fireworks at the end. Might as well take advantage of the infrastructure.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So, I've thought about what I've been doing over the last while that I'd like to continue doing, do or better, or do &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;less&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, and come up with what feels like a pretty achievable list:&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;ul&gt;&lt;li value="1"&gt;&lt;span style="white-space: pre-wrap;"&gt;Run 5km twice weekly; weights twice weekly; eat less meat; drink less; and less frequently; write every day; publish every week.&lt;/span&gt;&lt;br&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Some of these are measurable, some a bit more vague. &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Drink less&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, for example. I'll get into my rationale below.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/moving-cloudflare-out-of-the-critical-path</id>
    <title type="text">Moving Cloudflare out of the critical path</title>
    <updated>2025-12-15T00:34:13.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/59ec86af09b7aaef.png" alt="BLOG-3079_2.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;By way of disclaimer: I really like Cloudflare, I've been a customer and (minor) shareholder for years, and I will continue to use them.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;My general principle around infrastructure outages is to stick with the providers that make mistakes. The good ones learn from those mistakes, improve their processes, and improve reliability. Plus I can't even recall the last time a Cloudflare outage occurred before that, let alone took down a good chunk of the Internet. If anything it's a testament to both their scale and remarkable resilience.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt; However, the extended &lt;/span&gt;&lt;a href="https://blog.cloudflare.com/18-november-2025-outage/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;outage on November 18th&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; was long enough that I started wondering whether I &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;really&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; needed Cloudflare to be in the critical request path for my blog. Mostly in the name of being thoughtful about technical decisions.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;During the outage, the only way to potentially route around Cloudflare was using their API to disable request proxying – and this was contingent on your origin server already being set up to handle SSL traffic and being able to handle the increase in load.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=45966041" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Alexander Belanger posted a handy comment on Hacker News detailing the process&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;details class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-2" open="true"&gt;&lt;summary class="cursor-pointer py-1 px-6 relative font-bold list-none outline-none text-zinc-900 dark:text-zinc-100 [&amp;::-webkit-details-marker]:hidden [&amp;::marker]:hidden before:content-[''] before:block before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:w-0 before:h-0 before:border-[6px] before:border-transparent before:border-l-black dark:before:border-l-white before:rotate-0 before:transition-transform [details[open]_&amp;]:before:rotate-90 [div[data-open]_&amp;]:before:rotate-90 hover:bg-zinc-100 dark:hover:bg-zinc-700/50"&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;abelanger | on: Cloudflare Global Network experiencing issues&lt;/span&gt;&lt;/p&gt;&lt;/summary&gt;&lt;div class="px-5 pb-1 pt-0 text-zinc-900 dark:text-zinc-100" data-lexical-collapsible-content="true"&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;If anyone needs commands for turning off the CF proxy for their domains and happens to have a Cloudflare API token.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;First you can grab the zone ID via:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2"&gt;&lt;span style="white-space: pre-wrap;"&gt;    curl &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;X&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;GET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"https://api.cloudflare.com/client/v4/zones"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Authorization: Bearer $API_TOKEN"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Content-Type: application/json"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;|&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; jq &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;r &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'.result[] | "\(.id) \(.name)"'&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;And a list of DNS records using:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2"&gt;&lt;span style="white-space: pre-wrap;"&gt;    curl &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;X&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;GET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Authorization: Bearer $API_TOKEN"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Content-Type: application/json"&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Each DNS record will have an ID associated. Finally patch the relevant records:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2"&gt;&lt;span style="white-space: pre-wrap;"&gt;    curl &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;X&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PATCH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Authorization: Bearer $API_TOKEN"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;H&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Content-Type: application/json"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;data &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'{"proxied":false}'&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Copying from a sibling comment - some warnings:&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;- SSL/TLS: You will likely lose your Cloudflare-provided SSL certificate. Your site will only work if your origin server has its own valid certificate.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;- Security &amp; Performance: You will lose the performance benefits (caching, minification, global edge network) and security protections (DDoS mitigation, WAF) that Cloudflare provides.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;- This will also reveal your backend internal IP addresses. Anyone can find permanent logs of public IP addresses used by even obscure domain names, so potential adversaries don't necessarily have to be paying attention at the exact right time to find it.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This ended up being what I did, with some minor tweaks that I included in the comment thread around using global API keys and checking &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;origin firewall rules&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;My blog does not receive enough ordinary traffic to warrant a globally distributed CDN, and since I use LetsEncrypt on the origin server, I'm not relying on Cloudflare for SSL termination.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The two main things I'm using Cloudflare for are caching and security. I'm interested to see how far I can get with my own efforts on caching – I have spent a lot of time fiddling with caching over the years.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Security, DoS protection, etc., are also something I'm familiar with, but not to the point of reproducing Cloudflare's capabilities. Still, I am willing to see if &lt;/span&gt;&lt;a href="https://github.com/fail2ban/fail2ban" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;fail2ban&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; with some LLM-curated filters and an aggressive policy against scanners is going to be enough to keep things ticking over.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;There are some anti-spam and quality-of-life stuff Cloudflare does too, like &lt;/span&gt;&lt;a href="https://developers.cloudflare.com/waf/tools/scrape-shield/email-address-obfuscation/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;email address obfuscation&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and JS/CSS/image optimisation. I like email obfuscation in theory, but I drop my public email address everywhere, so I'm not sure it has any practical benefit!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The great thing is that my blog is architecturally very simple, with all the content and images in an SQLite database that is live replicated with litestream, and occasionally manually dumped to my own laptop. It's all running on a €5/month VPS with Hetzner, and if it gets taken down reproducing it isn't a big deal.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In fact I'm still using Cloudflare for DNS, so I can just pop over to the dashboard and re-enable the proxy easily enough. If the DNS fails at some point I might revisit :)&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;For my blog, I like the idea of having no intermediate proxy between myself or the reader and the server. It's a bit more old school, but also simpler, and aligns with my desire to occasionally reevaluate my default choices.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Developer folk wisdom tells us to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;use the right tool for the job&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, but also to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;use the tools you know&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, two good principles that can be in tension with each other. It has always felt crucial for me to make sure I am not using familiar tools on the wrong problems though. That is laziness, and not &lt;/span&gt;&lt;a href="https://wiki.c2.com/?LazinessImpatienceHubris" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;the good kind&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;That said, this is not a choice I think I would make in a professional capacity, especially if Cloudflare was handling a good chunk of my CDN traffic. As a passive and inexpensive service, it would need to do a lot worse to turn me off using it. Not a bad idea to have that &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;disable proxy&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; logic written up in a script somewhere though.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;If my blog ends up being defaced, or dropping offline from attacks, I will count it as an interesting, and perhaps very cheap, lesson.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com/item?id=46296051" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;a href="https://news.ycombinator.com/item?id=46296051" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Edit, 2025-12-17: updated to include a link to the Hacker News discussion.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;I had been blocking all non-Cloudflare traffic to the server to stop the drive-by random scans of all IP addresses that otherwise happen constantly.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/moving-cloudflare-out-of-the-critical-path" rel="alternate"/>
    <published>2025-12-16T17:14:45.183Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm a big fan of Cloudflare, but the extended outage on November 18th was long enough that I started wondering whether I really needed it to be in the critical request path for my blog.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/10gbe</id>
    <title type="text">10GbE</title>
    <updated>2025-12-12T17:48:42.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/831a75b477e8d2dc.jpg" alt="fastfive.jpg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;I live my life a quarter gig at a time.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;After a half-price offer expired with Virgin Media, my 1Gb broadband price went from around €42.50 per month to €85, so I called them up to negotiate a better price. Those new customer offers were my target, but I was also willing to switch provider for a much cheaper 1Gb offer.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Siro (fibre delivered via the ESB network) has arrived in my area in the last month or so, and I figured I was in a reasonably good negotiating position. After an hour or so of waiting in queues and chatting to people, I ended up talking myself into signing up for Virgin's recently added &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;5Gb&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; package.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Just to be clear, they did not talk me into it – there was no upsell. They gave me a reasonable offer for 1Gb broadband, and I pushed for 5Gb. It was also still about 25% cheaper than what I had been paying for 1Gb.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="fast-5" class="group relative"&gt;&lt;a href="#fast-5" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Fast 5&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Fast cars have never been my thing, but fast Internet? From my teens I have always wanted &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;the fastest Internet&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, even if it didn't really make much sense. A holdover of those early years sipping 5kB/s over dialup – an unquenchable desire for &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;more speed&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. Just like Vin Diesel. Maybe. I don't really remember what those movies were about.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;In any case, even before I signed up, I knew I was giving myself a small problem: I do not have a single piece of equipment that supports 5Gb networking. Everything I have tops out at 1Gb. My thinking was that this was fine, I would just do some planning and slowly upgrade piecemeal over the next few months. Obviously 1Gb had been more than enough for everything, I had only really called up to get a cheaper price.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Unfortunately as soon as the router arrived, my 1Gb equipment became ancient technology shackling me to the mortal realm.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I didn't realise it, but the 5Gb package is not delivered via the same inconveniently located coax endpoint as the 1Gb. Since that coax was originally intended to deliver cable television, it always ends up being in a living room near wherever a TV might go, which is not necessarily the best place for your router to live.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The new broadband is delivered via a new fibre optic line being routed from inside the apartment building, and so two days later I ended up with a sliver of fibre optic going directly to my office.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This changed things. Instead of running &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;CAT6&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; through the walls myself at some unspecified date in the future, the termination point was now right where I needed it. The only thing holding me back was... everything else.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="10gb-is-not-necessarily-rj45" class="group relative"&gt;&lt;a href="#10gb-is-not-necessarily-rj45" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;10Gb is not necessarily RJ45&lt;/span&gt;&lt;/h2&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Since 1GB had served all my needs up until this point, I actually wasn't very clear on what happened beyond that. The last time real networking hardware was relevant to me – rather than the abstract concept of a network you get via AWS –&amp;nbsp;was the late 2000s, and 1GbE was good enough for the rack at the time.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I was pretty sure that the next step change from 1Gb ethernet was 10Gb ethernet, but there is a halfway house, in fact more of a &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;quarter-way house&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;,&amp;nbsp;between them: 2.5Gb.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Why 2.5Gb? Well, 2.5Gb still runs on CAT5e, which was cheap and widely available for much of the 2000s and 2010s. When your network cards were maxing out at 1Gb anyway, what was the point in paying more for CAT6? Why future-proof for a 10Gb world when your network cards are a mix of 100Mb and 1Gb and your Internet connection is maybe ~25Mb? I still had heaps of old CAT5E cables I cleared out at the start of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;this year&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Today, the networking hardware to handle 2.5Gb is actually more widely available in consumer devices, though not at all ubiquitous. The most recent M4 Mac mini can be upgraded from a 1Gb port to 10Gb, but I don't recall seeing an option like that on any machine I've bought for nearly two decades? Even my 2008 unibody MacBook, practically a museum piece, had gigabit ethernet as standard!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So 2.5Gb is a big speed increase over 1Gb for a moderate price increase, and will work if you've got CAT5E cables in the walls for some reason.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;10Gb is different.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Not only does 10Gb need at least CAT6 cabling, it actually requires a decent amount of energy to operate, which means more heat, more expensive components, and potentially active cooling (i.e., fans).&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;At 10Gb, networking enters a transitional phase where you can use either copper cabling or fibre optic, with the latter being amazing for low-latency, long-distance, low-interference transmission of, like, hundreds of gigabytes per second. As you might imagine, fibre optic, being a way to transmit &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;photons&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; instead of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;electrons&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, requires a different connector than good old 8-pin &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;RJ45&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;. But to help with the transition, there is a clever solution: a hot-swappable modular interface called SFP+, basically an adapter for different types of network interfaces.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Depending on your hardware, you can plug optical or copper cables into ports that support these &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;transceivers&lt;/span&gt;&lt;sup&gt;[3]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/30b26cbfc8cd1a16.mp4" autoplay="" loop="" playsinline="" data-playback="silentLoop" title="meonly.mp4"&gt;&lt;/video&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Presumably, none of this is new if you're a network engineer, but it was new to me and pretty confusing. &lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Wherever you look on Amazon, you mostly find switches that are a mix of 2.5Gb RJ45, 10Gb SFP+, and only occasionally 10Gb RJ45. If you want to go from an eight-port gigabit switch –&lt;/span&gt;&lt;a href="https://www.amazon.ie/NETGEAR-Gigabit-Network-Switch-GS308/dp/B07PWHGQSS/ref=sr_1_5?crid=3TR6B2JPPBJYZ&amp;dib=eyJ2IjoiMSJ9.q7_vpP5Ti6cE49WWITHdry2W743RiSO5kaozrtRYsflrykA2OlgCxNNeNTKM0dpYQnr_R6BPCNwfFOw0v097pcqUltlxTn-fBRvmdy-F1zTsp7eValQfa-TZ1RyQAUiTNMam2mNoBHRx8N_ZslQtg9_opnggkYWTCIZ7UHz-854lTU4Y6h_bwJt31jVwd5b21OypXFDZa_Xt4hEvAuJr8umHDIaFxhH5fszBA8ZF9E5PFAHkbvqLZP2HhGWiL0I8J59EwB5eUU9862NYpD2xYmcrYwG5NbvopGRi6GDN46c.27SZQ2crU1K_NDNn76X8-BlbCQkwbbDMfvCnQvmLEf0&amp;dib_tag=se&amp;keywords=8%2Bport%2Bgigabit%2Bswitch&amp;qid=1765467462&amp;sprefix=8%2Bport%2Bgig%2Caps%2C61&amp;sr=8-5&amp;th=1" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;which you can buy for €20&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; – to an eight-port 10Gb switch, you are talking about &lt;/span&gt;&lt;a href="https://www.amazon.ie/Mikrotik-CRS312-4C-8XG-RM-network-Ethernet/dp/B07VS1XJZ6/ref=sr_1_1?crid=351EV8UI1UPFP&amp;dib=eyJ2IjoiMSJ9.kPFb2tBDAxDuAnDHs0O9hxTuEX9-44VzSMJni0tE04bGjHj071QN20LucGBJIEps.lfzphZnqcaNiawWnpfPJmOwn_GswR_JCdj9vqAYH-_o&amp;dib_tag=se&amp;keywords=CRS312-4C%2B8XG-RM&amp;qid=1765467320&amp;sprefix=crs312-4c+8xg-rm%2Caps%2C52&amp;sr=8-1" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;actively cooled hardware costing anything from €500 up&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I ended up reading a bunch of stuff on &lt;/span&gt;&lt;a href="http://servethehome.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;ServeTheHome.com&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, which seems to have great &lt;/span&gt;&lt;a href="https://www.youtube.com/watch?v=-pYQvEX9Ct0" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;breadth and depth&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; into this particular niche of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;not-quite-home-not-nearly-enterprise&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; hardware.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;One of my main takeaways was that it's a lot easier to stick to a reasonable budget if you're not set on more recognisable brands like Zyxel or Netgear. In my case, I decided getting 10Gb on my primary home server was probably enough, with other equipment running on 2.5Gb, and the rest via WiFi.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Winging their way to me are:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li value="1"&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;A MokerLink Ethernet Switch with 2x10G SFP, 4 x 2.5G Base-T Ports&lt;/span&gt;&lt;sup&gt;[4]&lt;/sup&gt;&lt;/span&gt;&lt;/li&gt;&lt;li value="2"&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;A Binardat 10G SFP+ PCIe Network Adapter&lt;/span&gt;&lt;sup&gt;[5]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;, to handily eclipse the capacity of the two gigabit network ports in my aging-but-durable &lt;/span&gt;&lt;a href="https://www.storagereview.com/review/hp-proliant-microserver-gen8-review" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;HP Proliant Microserver Gen8&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/li&gt;&lt;li value="3"&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;A 10Gtek 10G SFP+ to SFP+ Direct Attach Copper cable&lt;/span&gt;&lt;sup&gt;[6]&lt;/sup&gt;&lt;/span&gt;&lt;/li&gt;&lt;li value="4"&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;A QSFPTEK 10GBASE-T SFP+ RJ45 Transceiver&lt;/span&gt;&lt;sup&gt;[7]&lt;/sup&gt;&lt;/span&gt;&lt;/li&gt;&lt;li value="5"&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;A UGREEN USB C Ethernet Adapter, 2.5Gbps&lt;/span&gt;&lt;sup&gt;[8]&lt;/sup&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I mean, even getting 2.5Gb to most of the machines would be a huge step up – this is a 5Gb connection, so two of those machines could go flat out together, which is wild.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;UGREEN is the only brand I'd even heard of beforehand, and even that's a pretty new one.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt; I'm interested to see how it all fits together. So far, this hardware is just pending orders waiting to be delivered, but at least some of my Christmas holiday period is going to be spent wiring this stuff up – I'm looking forward to it!&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com/item?id=46276295" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;If you're a network engineer, or just an enthusiast who spots something wrong, please do let me know!&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Edit, 2025-12-17: updated to include a link to the Hacker News discussion.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;CAT6 networking cable is made with copper, and enables transmission speeds up to 10Gb/s up to around 50 metres.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;A subsequent standard, CAT6a, extends that out to 100m.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;These cables use RJ45 connectors, which have been around since the 1970s.&lt;/span&gt;&lt;br&gt;&lt;a href="/files/b5f50cc9941698a0.jpg"&gt;[image: /files/b5f50cc9941698a0.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/97254ce0bd542f47.jpg"&gt;[image: /files/97254ce0bd542f47.jpg]&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Hello there!&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Because they can be transmitters and receivers!&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/4db7c66eb5a6c98e.jpg"&gt;[image: /files/4db7c66eb5a6c98e.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/192277070fad4f9a.jpg"&gt;[image: /files/192277070fad4f9a.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/2cd22700bb4975e8.jpg"&gt;[image: /files/2cd22700bb4975e8.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/a0842d386776f9b6.jpg"&gt;[image: /files/a0842d386776f9b6.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/02aad2dc602b339d.jpg"&gt;[image: /files/02aad2dc602b339d.jpg]&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/10gbe" rel="alternate"/>
    <published>2025-12-15T15:56:58.270Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;A price negotiation call to Virgin Media ended up with me upgrading to 5Gb fibre broadband.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This took me on a bit of a journey investigating things that have changed in home networking since 1Gb networking cards hit the scene.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Turns out, somehow both more than I expected and less than I imagined!&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/a-year-of-not-really-blogging</id>
    <title type="text">A year of not really blogging</title>
    <updated>2025-12-14T21:40:19.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;A lot of drafts.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Short posts I should have just hit publish on.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I did not even publish enough posts to make my default pagination &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;hit the second page&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I did have a great year though, including three months in Japan in which I was only rarely tempted to blog, and now almost wish I'd put the effort into. There's a couple of stub posts and notes, but my main instrument was really the camera. I didn't feel like cracking out the laptop to jot down my thoughts most evenings. I didn't even have it with me for weeks at a time as we went &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;bikepacking&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;across&lt;/span&gt;&lt;sup&gt;[3]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;Hokkaido&lt;/span&gt;&lt;sup&gt;[4]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;. Maybe a retrospective photo blog?&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I helped wind down the last company I worked for in January, then &lt;/span&gt;&lt;a href="http://manano.ai" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;I started a new one&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When it comes to writing though, even with the blog, I mostly did what I usually do. I wrote up comments on Hacker News and deleted them before hitting submit.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I jotted down vague ideas for posts in the Notes app – though I did more of this in the blog editor. Looking back over the last year of them, it seems I still can't shake the occasional desire to comment on global affairs/politics. Maybe I should just write these articles up fully and never publish them, just to get the ideas out of my head; and practice writing.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I've really enjoyed working on the blogging software itself, not that I've spent a huge amount of time tinkering with it since January. &lt;/span&gt;&lt;a href="https://duggan.ie/posts/ignoring-good-advice-and-building-my-own-blog-again" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;The original risk I anticipated was that I would agonize over building the blog in and then never &lt;/span&gt;&lt;/a&gt;&lt;a href="https://duggan.ie/posts/ignoring-good-advice-and-building-my-own-blog-again" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;even start&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. I forgot that anything that is not easy to do is something you have to put continuous, deliberate effort into.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So what to do?&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The fact that I've written so little makes me wonder whether I even want to write? Surely I would be making the time! I would have pages and pages to show for the amount of time that has passed.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I still believe writing well is an incredible talent, even more so in a time where mere &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;lexical coherence&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; is no longer a sign that a human being has been thinking somewhere.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;The fact that I'm writing even this is probably enough of an indicator that I still want to keep doing it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I guess I should return to &lt;/span&gt;&lt;a href="https://micro.webology.dev/2024/11/02/please-publish-and.html" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Jeff Triplett's words that started me back blogging in the first place&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;span style="white-space: pre-wrap;"&gt;You don’t have to change the world with every post. You might publish a quick thought or two that helps encourage someone else to try something new, listen to a new song, or binge-watch a new series.&lt;/span&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Maybe they'll sink in this time.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Actually the pagination count was showing something like three pages for a good while due to a bug in the pagination code, but I only noticed that this last week 😅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/b69a6dfcb09acf56.jpeg"&gt;[image: /files/b69a6dfcb09acf56.jpeg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/7e30dc907810028c.jpg"&gt;[image: /files/7e30dc907810028c.jpg]&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="/files/35d3a47621e45897.jpeg"&gt;[image: /files/35d3a47621e45897.jpeg]&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/a-year-of-not-really-blogging" rel="alternate"/>
    <published>2025-12-15T00:13:25.391Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I started building a new blog in December 2024, and wrote my first substantial post in January 2025, but I have only published a handful of posts since then. What does that tell me?&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/sync-new-purchases-to-ipod-classic</id>
    <title type="text">Sync new purchases to iPod Classic</title>
    <updated>2025-10-30T17:13:12.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I recently refurbished an old iPod Classic, and purchased some albums in the iTunes Store (yes, it still exists!) with the idea of trying what counts as oldschool in the 21st century –&amp;nbsp;listening to music on &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;something that doesn't also receive email&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Once I found out that most iPod Classics are capable of playing Apple's lossless format, ALAC, I really wanted to sync them over to experience this &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;heretofore unimaginable audio clarity&lt;/span&gt;&lt;sup&gt;[2]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;However, even though I could download them, the albums refused to show up in the sync list. Switching back to AAC worked fine, but ALAC was not working.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Turns out&lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt; iTunes itself does not provide lossless downloads&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Because I also have an Apple Music subscription, the lossless versions of my albums were being downloaded, but in the protected &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;.movpkg&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; encrypted format that only works with Apple Music.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;So the only way you can sync all your iTunes purchases is setting 256kbps AAC:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/6517b6e50a30f252.png" alt="Screenshot 2025-10-30 at 16.51.32.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Another hiccup is that Apple recently removed some iPod sync support from MacOS Sequoia (15.4.1).&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;This isn't a showstopper for me as I have Macs of various vintages running everything from OS9 to Ventura, but it's an annoying thing for Apple to do right as iPods are having something of a renaissance.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;There is &lt;/span&gt;&lt;a href="https://forums.macrumors.com/threads/i-found-a-way-to-still-use-my-modded-ipod-classic-past-macos-sequoia-15-4-1.2463657/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a reported workaround&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; that seems to work for some, if you happen to run into this.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;As an addendum, here's a photo of my 4th gen classic, refurbished with a new battery from &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;AliExpress&lt;/span&gt;&lt;sup&gt;[3]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;, and the &lt;/span&gt;&lt;a href="https://www.iflash.xyz/store/iflash-ata1/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;iFlash ATA1 microSD adapter&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/b71ff0b02e9c29ea.png" alt="7b95a5516b9f5636.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Zawinksi's Law: &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;"Every program attempts to expand until it can read mail. Those programs which cannot so expand are replaced by ones which can."&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;-- James Zawinski, 1995&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;Only being slightly sarcastic here.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;As I understand it's really only trained listeners on extraordinarily high end equipment that can tell the difference between a high bitrate mp3 and lossless.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Even then, my audio setup for the iPod involves using a 3.5mm to lightning adapter for my AirPods Max, which does involve some re-encoding.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;🤷&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style="white-space: pre-wrap;"&gt;AliExpress sellers frequently appear and disappear, so I can't link to the exactly battery I bought, but if you search for something like "3.7V 1400mAh ipod classic 4th gen" it should get you in the right direction.&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/sync-new-purchases-to-ipod-classic" rel="alternate"/>
    <published>2025-10-30T17:11:43.679Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I recently refurbished an old iPod Classic, and purchased some albums in the iTunes Store (yes, it still exists!).&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Once I found out that most iPod Classics are capable of playing Apple's lossless format, ALAC, I really wanted to sync them over to experience this heretofore unimaginable audio clarity.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;However, even though I could download them, the albums refused to show up in the sync list. Switching back to AAC worked fine, but ALAC was not working.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/vat-invoice-generator</id>
    <title type="text">VAT invoice generator</title>
    <updated>2025-11-19T08:35:56.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I needed an invoice generator to quickly produce some random test invoices while building &lt;/span&gt;&lt;a href="https://manano.ai" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Manano&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, as part of a feature letting tradesmen track receipts by sending photos to our WhatsApp number.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I tried a few invoice generators that showed up in Google results, but most required either registering an account or were awkward to use.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Eventually I realized it would probably be straight forward enough to whip one together, and so here is my own &lt;/span&gt;&lt;a href="https://duggan.ie/vat-invoice-generator" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;VAT invoice generator&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/cd5e8263427cc4c2.png" alt="vat-invoice-generator2.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;You can add line items, it calculates tax, totals, etc.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Since I wrote it for testing a vision-based pipeline, it has a bunch of visually distinctive templates with pre-populated data and a randomzier to produce different figures and line items.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;When you make changes, they are saved locally in your browser, so there's no services to host, and you can even &lt;/span&gt;&lt;span class="cursor-help underline decoration-dotted" data-tooltip="true" data-tooltip-html="&lt;p&gt;&lt;span style=&amp;quot;white-space: pre-wrap;&amp;quot;&gt;Just like the 90s!&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&amp;quot;https://duggan.ie/files/ebae26fb103bc45e.gif&amp;quot; alt=&amp;quot;netscape.gif&amp;quot; width=&amp;quot;inherit&amp;quot; height=&amp;quot;inherit&amp;quot;&gt;&lt;/p&gt;" data-tooltip-content="" title=""&gt;download the page&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; and use it locally. It loads &lt;/span&gt;&lt;a href="http://html2canvas.hertzen.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;html2cavnas&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; for generating the PNG remotely, so it can't work entirely offline, but nothing leaves the browser.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Anyway this was fun to throw together, and might be useful to some other folks. The basics were done pretty quickly, and then I've sort of been doodling away at it every now and then for the last couple of weeks.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer" dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/vat-invoice-generator" rel="alternate"/>
    <published>2025-10-18T17:00:32.499Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I needed an invoice generator to quickly produce some random test invoices while building Manano, as part of a feature letting tradesmen track receipts by sending photos to our WhatsApp number.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;I tried a few invoice generators that showed up in Google results, but all of them were oddly slow, wouldn't remember details without signing up, and sometimes produced odd looking outputs.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Eventually I realized it would probably be straight forward enough to whip one up myself…&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/fixing-a-problem-at-40-that-stumped-me-as-a-10-year-old</id>
    <title type="text">Fixing a problem at 40 that stumped me as a 10 year old</title>
    <updated>2025-09-03T16:12:20.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;A few months ago my parents gave me a box of my stuff they’ve had variously in attics and storage since I moved out in my early twenties.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;One of the things in it was my Sega Mega Drive (called the Genesis in America) — boxed, and with games and accessories. Unfortunately, I didn't have a TV that I could connect it to! The only connector packaged with it was designed to plug into the aerial socket of a CRT television.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;In the last week however, I've gotten hold of a CRT television after a bit of digging around&lt;/span&gt;&lt;sup style="white-space: pre-wrap;"&gt;&lt;span&gt;[1]&lt;/span&gt;&lt;/sup&gt;&lt;span style="white-space: pre-wrap;"&gt;. I hooked it up to the Mega Drive, and incredibly, it worked! Now, I did had to read the manual to figure out how to dial the receiver channel in 🥲&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Channel 36! UHF scan! This is not how I'm used to thinking about screens anymore.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'd plugged in one of the Mega Games cartridges that originally came with the console, with &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Golden Axe&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Streets of Rage&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, and &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Revenge of Shinobi&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. It was amazing to watch the menu come into focus as the channel dialled in, turning first from the noise of the CMB to jagged lines, then resolving into the blinking menu.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/cceb233e0bcced13.mp4" controls="" title="chan_scan.mp4"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Tuning into channel 36 😎&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Initially though it was a little temperamental — if the controller wire pulled at it, or if it was nudged at all, the console would reset. I eventually remembered that I stopped playing it when I was a kid partly out of frustration at this exact problem. The Mega Drive does not come with any built in storage, so game cartridges themselves had to support that functionality. At ten years old, there was nothing quite so frustrating spending an hour playing a game and have a nudge reset you to zero.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Thirty years later and with various gadget repairs and upgrades under my belt, I don't feel so helpless 🙂 I opened up the Mega Drive – at ten it probably didn't even occurred to me as &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;something I could do&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; – and immediately spotted the problem: the solder connecting the power socket to the motherboard had cracked. All I needed to fix it was to resolder the joints, and now it’s as good as new!&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I was honestly just a teensy bit emotional. It's not the most astounding technical achievement, and it's not going to make a difference to anyone else, but it feels a little like I've been able to help that ten year old version of me.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;On the other hand, thirty years has not made me any better at &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Revenge of Shinobi&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="footnotes" class="group relative"&gt;&lt;a href="#footnotes" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Footnotes&lt;/span&gt;&lt;/h3&gt;&lt;ol&gt;&lt;li value="1"&gt;&lt;a href="https://www.wired.com/story/crt-tube-tv-hot-gaming-tech-retro-games/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;The difficultly of getting hold of a CRT television&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; in 2025 was a real surprise to me!&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/fixing-a-problem-at-40-that-stumped-me-as-a-10-year-old" rel="alternate"/>
    <published>2025-09-03T16:12:20.726Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;A few months ago my parents gave me a box of my stuff they’ve had variously in attics and storage since I moved out in my early twenties.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;One of the things in it was my Sega Mega Drive — boxed, and with games and accessories.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Unfortunately, I didn't have a TV that I could connect it to! The only connector packaged with it was designed to plug into the aerial socket of a CRT television.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;In the last week however, I've gotten hold of a CRT television after a bit of digging around.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/japan-may-2025-hokkaido-and-kyushu</id>
    <title type="text">Japan May 2025 – Hokkaido and Kyushu</title>
    <updated>2025-06-03T13:56:04.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;br&gt;&lt;/p&gt;&lt;aside class="callout my-6"&gt;&lt;div class="border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 px-6 py-4 rounded-r-lg"&gt;&lt;strong class="block text-sm font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300"&gt;Note&lt;/strong&gt;&lt;div class="mt-2 text-base leading-relaxed"&gt;&lt;p class="mb-0"&gt;I originally wrote this in June 2025, but never published it because I thought it was too short. Rather than extend and rewrite it as a retrospective, I've decided just to make it public keeping the date to reflect when it was written.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/aside&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;First few weeks of our Japan trip were fantastic. Despite &lt;/span&gt;&lt;span&gt;&lt;span style="white-space: pre-wrap;"&gt;some minor confusion over our visa length on entry&lt;/span&gt;&lt;sup&gt;[1]&lt;/sup&gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;, we had a great start in Sapporo, with nice weather and friendly people.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;In our first two days, two different people stopped to talk to us, one older gent just to ask where we were from and if we were enjoying our visit, and another young chap from a local college who wanted to practice his English. He was doing this almost daily with different strangers who looked like they might speak English in the park! Impressive.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We were also lucky that cherry blossoms were still in bloom in and around Sapporo!&lt;/span&gt;&lt;/p&gt;&lt;div class="grid gap-4 w-full my-4 grid-cols-2" data-lexical-layout-container="true"&gt;&lt;div class="min-h-[50px] rounded-lg border border-zinc-200 p-4 dark:border-zinc-800" data-lexical-layout-item="true"&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper" style="width: 339px"&gt;&lt;img src="/files/f7c614577403a7ad.jpeg" alt="IMG_0893.jpeg" width="339" height="471.82456140350877" style="max-width: 100%; height: auto;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;div class="min-h-[50px] rounded-lg border border-zinc-200 p-4 dark:border-zinc-800" data-lexical-layout-item="true"&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper" style="width: 354px"&gt;&lt;img src="/files/93128ea69051c9fd.jpeg" alt="IMG_0267.jpeg" width="354" height="472" style="max-width: 100%; height: auto;"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I was also introduced to what has been my favourite beer of the trip so far, Sapporo Classic! Unfortunately I found out only after we flew down to Kagoshima that this beer is only sold in Hokkaido. &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Sayonara&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, Sapporo Classic. You were taken from us too soon 🥲.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Since I'm writing this while staying in &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hakata&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, it feels sacrilegious to say this, but the best ramen I've had on this trip was a place we randomly walked into on our first day in Sapporo, &lt;/span&gt;&lt;a href="https://maps.app.goo.gl/qWCmWGpWdyvxN53k6" rel="noreferrer" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Yoshiyama Shouten in Tanukikoji&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;div class="footnotes"&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;ol&gt;&lt;li value="1"&gt;&lt;span style="white-space: pre-wrap;"&gt;Don't put 90 days into the electronic visa system – apparently the boundary checks are for &amp;lt; 90days, not &amp;lt;= 90 days 😅&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</content>
    <link href="https://duggan.ie/posts/japan-may-2025-hokkaido-and-kyushu" rel="alternate"/>
    <published>2025-06-02T16:44:53.000Z</published>
    <summary type="html">&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;First few weeks of our Japan trip were fantastic. Despite some minor confusion over our visa length on entry, we had a great start in Sapporo, with nice weather and friendly people.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/goodbye-hokkaido-next-stop-okinawa</id>
    <title type="text">Goodbye Hokkaido, next stop... Okinawa?</title>
    <updated>2025-04-03T10:45:48.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We've been going over the pros and cons of the current route, and have decided that the V-shaped travel route is not our best option.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;a href="https://duggan.ie/posts/preparing-for-the-japan-trip" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Discussed in my previous post&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, the V-shaped route was intended to start us in cherry-blossom season, take us down to Osaka for a few weeks, then Yakushima, and then slowly back up north. On further examination there are a few of problems with this.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;First, we're planning to spend our initial week or so in Hokkaido, which actually leaves us with maybe ten days to get down to Osaka. That's a lot of interesting territory to cover in only a week! Also, while we'll be in Osaka itself for a couple of weeks, Ash will be preoccupied with exam preparation, leaving me exploring Osaka largely by myself.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;After Osaka, the plan would be to head to Yakushima, then back up to continue the rest of the trip.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;So, new plan!&lt;/span&gt;&lt;sup style="white-space: pre-wrap;"&gt;&lt;span&gt; (Until the next plan)&lt;/span&gt;&lt;/sup&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/d517b993e4dc71e6.png" alt="Screenshot 2025-03-20 at 18.10.13 copy 2.png" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Hokkaido → Kagoshima → Okinawa  Image credit: JNTO.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We'll still start with a week or so in Hokkaido, visiting Lake Toya, Hakodate, and renting a car for a day trip to Matsumae a bit further down the coast. This part of the trip is strongly influenced by &lt;/span&gt;&lt;a href="https://www.youtube.com/watch?v=9FMLmi8VW4U" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a lovely Japan Guide video on a two day trip from Sapporo to Hakodate&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/b417b0aefc6a7e49.jpg" alt="XYZeXYZe5380_1680.jpg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Matsumae Castle, Hokkaido. Image credit: JNTO.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/8a3c0181c2103673.jpg" alt="mv-why-hakodate.jpg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Hakodate, Hokkaido. Image credit: JNTO.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Once we've completed our mini Hokkaido tour, we'll head back up to Sapporo, catch a flight to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Kagoshima&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; (flights are inexpensive), spend a few days there, then take a short ferry to Yakushima island for four days of hiking around the forest island that inspired Princess Mononoke.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Ferries and car rental for regional islands aren't really available through the usual providers you might use, like booking.com, etc, but honestly not that difficult to figure out.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;For car rental we're using &lt;/span&gt;&lt;a href="https://yakushima-navi.com/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Yakushima NAVI&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (personally I am very much looking forward to my first time driving a keicar), and you can book a hydrofoil to/from Yakushima with &lt;/span&gt;&lt;a href="https://www.tykousoku.jp/reserve/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Toppy &amp; Rocket&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/0ffd2d82ca64bf46.jpg" alt="Toyota_PIXIS_MEGA_L&amp;quot;SA&amp;quot;2WD_(DBA-LA700A-GBMF)_front.jpg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Kei car! Photo By Tokumeigakarinoaoshima - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=85137674&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;When we wave goodbye to Yakushima, we'll make our way towards Okinawa to camp out for Ash's exams for a couple of weeks. Once the exams are over, our travel arrangements will be a lot more flexible, and we can stick around, fly, or ferry our way back to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Kyūshū&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We actually originally planned to start our trip in Okinawa and work north, but were drawn to Hokkaido by the chance of catching cherry blossom season.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="ky-sh" class="group relative"&gt;&lt;a href="#ky-sh" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Kyūshū&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt; With a more relaxed itinerary, it will be nice to have the option of dropping into Aso-Kuju National Park on our way up towards Fukuoka.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/48a04cb612ab782d.jpeg" alt="aso-kuju_aso_nekodake_gx82iw.jpeg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Aso-Kuju National Park. Image credit: JNTO.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;One of the things I'm most looking forward to about Fukuoka is the chance to visit the home of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;tonkotsu ramen&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, where the talented Cork chef &lt;/span&gt;&lt;a href="https://ichigoichie.ie/chef-miyazaki/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Takashi Miyazaki&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; hails from.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The only wrinkle in the south-to-north itinerary is that it sort of coincides with &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;tsuyu&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; (rainy season):&lt;/span&gt;&lt;/p&gt;&lt;table class="[&amp;_td_p]:m-0 [&amp;_th_p]:m-0"&gt;&lt;colgroup&gt;&lt;col&gt;&lt;col&gt;&lt;col&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;Region&lt;/strong&gt;&lt;/b&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;Start&lt;/strong&gt;&lt;/b&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;End&lt;/strong&gt;&lt;/b&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1247.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Okinawa&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;May&amp;nbsp;8&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;June&amp;nbsp;23&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1108.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Southern Kyushu&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;May&amp;nbsp;29&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;July&amp;nbsp;13&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1107.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Shikoku&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;June&amp;nbsp;4&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;July&amp;nbsp;17&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1105.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Kansai&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp;(incl. Kyoto)&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;June&amp;nbsp;6&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;July&amp;nbsp;19&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1103.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Kanto&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;&amp;nbsp;(incl. Tokyo)&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;June&amp;nbsp;8&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;July&amp;nbsp;20&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr class="even:bg-gray-50 dark:even:bg-gray-800/30"&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;a href="https://www.japan-guide.com/list/e1102.html"&gt;&lt;span style="white-space: pre-wrap;"&gt;Northern Tohoku&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;June&amp;nbsp;12&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;td class="border border-gray-200 dark:border-gray-700 px-3 py-1" style="border: 1px solid black; width: 75px; vertical-align: top; text-align: start;"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;July&amp;nbsp;27&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Realistically though there's no way to avoid rainy season when traveling during the summer months. Plus, coming from Ireland, we are no strangers to rain 😅 Warm rain even sounds like an upgrade!&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Since regional flights appear to be relatively inexpensive, it may even just make more sense for us to hop around a bit more rather than trying to make everything fit into train journeys. The more I see of Hokkaido through various YouTube videos the more time I want to spend there. We'll definitely have to head back there when the weather is warming up.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/goodbye-hokkaido-next-stop-okinawa" rel="alternate"/>
    <published>2025-04-03T10:05:00.000Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We've been going over the pros and cons of the current route, and have decided that the V-shaped travel route is not our best option.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Instead, we're going to jump from Hokkaido to Okinawa, and head back up from there!&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/preparing-for-the-japan-trip</id>
    <title type="text">Preparing for the Japan trip</title>
    <updated>2025-04-11T21:23:51.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;a href="https://duggan.ie/posts/traveling-to-japan-for-three-months-because-lifes-too-short" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;As I wrote about in February, Ash and I are traveling to Japan for three months&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (May → August). It's a trip I've always wanted to take, and since I'm between work engagements I've been taking some time to prepare.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Three months is a long time to spend in a country you've never visited, and I'm going to go over a few ways we've been preparing.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="skipping-the-detailed-itinerary" class="group relative"&gt;&lt;a href="#skipping-the-detailed-itinerary" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Skipping the detailed itinerary&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We've decided that a detailed itinerary does not make sense over a three month period. This runs counter to a lot of Japan travel advice that I've read or watched, but it seems likely that this advice is geared towards people who have a two week trip and want to squeeze in as many highlights as possible.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;If you read reddit, or travel blogs, or watch various YouTube videos, much of the prevailing advice on traveling to Japan is to book everything from accommodation to events months ahead. Ghibli Park sells out months in advance, accommodation can be hard to find during peak season, special discounts are only available months in advance, etc.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Historically, this is not how Ash and I have enjoyed traveling. Sure, there are one or two things that we'll plan in advance for, but otherwise we'll be happy to go off the beaten path rather than queue for the most popular attractions.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;An itinerary so long and detailed would become a straitjacket, sucking all the fun and serendipity out of the trip. We want to have the flexibility to allow for chance and discovery. No matter how much research we could do ahead of time, once we're on the ground we will undoubtedly start to find people, places, and opportunities that no amount of preparation could account for. We spent some time figuring out the high level shape of our trip, with a handful of specifics planned, then will handle the rest as we go.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We're prepared to camp or even head to another country for a bit if the relaxed plan isn't working out. I doubt it will come to that, but it's been a relief to realize that ultimately we can just peace out and go somewhere else if it's not working 🙂&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The only specific dates we've nailed down so far are flights and accommodation around our first week upon landing in Japan, and then later a few weeks accommodation in Osaka around Ash's exams.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;That doesn't mean we're going unprepared, though. I've gotten an International Driving Permit, have been learning Japanese, and roughly sketched out what the trip might look like, as well as booking a few key accommodations and event tickets (e.g., Expo 2025 in Osaka).&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The general idea is a sort of V-shaped itinerary (below), in which we start in the north (&lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Sapporo, Hokkaido&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;) during spring, head down to Osaka for a bit, then proceed to Yakushima, maybe Okinawa, and then make our way north to Hokkaido again as the summer heats up. This really depends on how we're feeling about the weather – I think I'm relatively comfortable in high heat and humidity, but we'll adjust our plans as needed.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;It's a bit odd maybe, but Japan's public transport infrastructure is what has made it possible to even consider a zigzagging plan like this. This is not how I would travel around South Africa, for example –&amp;nbsp;you'd be driving and flying everywhere.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Given the season, duration, and availability of public transport, this seems like a plan that will let us visit a lot of places, as well as make return trips or catch things we may have missed on the southward leg.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/17170c3c9ccceba5.png" alt="japan-travel-2.png" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;We're starting off in Hokkaido, since it turns out the dates we booked to arrive coincide with the peak of the cherry blossom season there! Map via japan.travel.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We also want to do some hiking –&amp;nbsp;maybe the &lt;/span&gt;&lt;a href="https://www.tb-kumano.jp/en/kumano-kodo/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Kumano Kodo&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, though there's no shortage of places to walk –&amp;nbsp;and even camping if the opportunity arises!&lt;/span&gt;&lt;/p&gt;&lt;h3 id="researching-a-trip-in-2025" class="group relative"&gt;&lt;a href="#researching-a-trip-in-2025" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Researching a trip in 2025&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;In terms of research, I've been watching a lot of Youtube videos. Like, &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;a lot&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. There are a ton of recent and detailed guides put out by a variety of native Japanese, as well as foreigners who have been living in Japan for years. Back when I first considered a Japan trip the best option was an infrequently updated Lonely Planet guide. I was fishing around for a while looking for that guide again before I realized that it was the year 2025 and I was being a moron.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Something else I learned is that the &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;Japan Rail Pass&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;, which used to be practically a no-brainer for foreign travellers, is &lt;/span&gt;&lt;a href="https://www.travelcaffeine.com/japan-rail-pass-worth-money-tips/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;less of a no-brainer now since price increases in 2023&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;span style="white-space: pre-wrap;"&gt;For most first-timers to Japan, whether the JR Pass is for you is quite a simple question to answer. It usually boils down to whether you’re traveling to another major city after going from Tokyo to Kyoto via the Shinkansen (bullet train).&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;In other words, you need to be taking at least three Shinkansen rides in a 7-day window.&lt;/span&gt;&lt;/blockquote&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Frankly, the Japan Rail Pass probably didn't even make sense for our trip &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;before&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; the price changes. Three months slowly meandering around the entire country is not what it was designed for.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Just to call out some of the YouTube channels I found most useful:&lt;/span&gt;&lt;/p&gt;&lt;ul&gt;&lt;li value="1"&gt;&lt;a href="https://www.youtube.com/@KenshoQuest" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Kensho Quest&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;detailed and up to date information on the practicalities of traveling to Japan, from what to pack, how to get around, and what to expect by season.&lt;/span&gt;&lt;/li&gt;&lt;li value="2"&gt;&lt;a href="https://www.youtube.com/@Toshi-Guide-from-Japan" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Toshi Guide&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; – (maybe unintentionally?) hilarious with lots of dramatic stock footage, Toshi gets into the details of how travel to Japan is changing over the last couple of years, as well as specific details on things like smoking bans in Osaka, improving border control, and avoiding tourist traps and scams.&lt;/span&gt;&lt;/li&gt;&lt;li value="3"&gt;&lt;a href="https://www.youtube.com/@japanguide" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Japan Guide&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;wonderful mini travel documentaries in which someone takes a short trip to some part of Japan, showing some highlights and transport options. Great inspiration for places to visit.&lt;/span&gt;&lt;/li&gt;&lt;li value="4"&gt;&lt;a href="https://www.youtube.com/@MrsEats" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Mrs. Eats&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;intentionally hilarious insights from a Japanese woman on Japanese culture, and nerdy things to do when in Japan.&lt;/span&gt;&lt;/li&gt;&lt;li value="5"&gt;&lt;a href="https://www.youtube.com/@JapanbyFood" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Japan By Food&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;so many amazing looking restaurants to visit everywhere 🤤&lt;/span&gt;&lt;/li&gt;&lt;li value="6"&gt;&lt;a href="https://www.youtube.com/@shoheikondo" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Shohei Kondo&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; –&amp;nbsp;Shohei reviews and critiques the videos that various gaijin (foreigner) influencers produce about Japan. Really great to have another perspective on popular TikTok and YouTube videos that otherwise might have you worried about visiting Japan.&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;h3 id="backpack-or-suitcase" class="group relative"&gt;&lt;a href="#backpack-or-suitcase" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Backpack or suitcase? 🎒か🧳？&lt;/span&gt;&lt;/h3&gt;&lt;p&gt;&lt;span style="white-space: pre-wrap;"&gt;Both of us like traveling light. For me there's a Tetris-y, technical optimization element to it – I like to pack as much flexibility as possible into as few grams and items as possible. For example, making sure most devices are USB-C, or using my laptop as a battery pack instead of bringing a separate power bank.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Ash is the true backpacker type –&amp;nbsp;by which I mean she'd happily throw a toothbrush, change of clothes, and running shoes into a rucksack and traipse around India for a month.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We'll both have real-life things to accomplish during this trip&lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt; &lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;– Ash will be doing exams, and I'll be coding, writing, and keeping up with Japanese study.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;At the moment, backpack is winning out. We'll be moving around a lot, and ideally to less-traveled places, which means relying on &lt;/span&gt;&lt;a href="https://www.japan-guide.com/e/e2278.html" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;takuhaibin&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; seems awkward. Plus, if we're wrong it seems easier to bump up to a suitcase trip than it would be to downsize to a backpack trip.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="learning-japanese" class="group relative"&gt;&lt;a href="#learning-japanese" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Learning Japanese 🇯🇵の📚&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Ash is a bit too busy doing concurrent qualifications in both art and quantum physics (😵‍💫) to also learn a language, so I'll be the one attempting to learn enough of the language to be polite and navigate food, transport and accommodation.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;While years of watching anime has given me a smattering of vocabulary and some familiarity with pronunciation, it's a whole other thing to converse in real time, read, and parse numbers.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I've been attending a weekly class in Portobello, which has been an excellent starting point, and is a good place for practicing and keeping me accountable.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm using&lt;/span&gt;&lt;a href="https://www.audible.com/series/Pimsleur-Japanese-Audiobooks/B01CRMUIKK" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt; Pimsleur's Japanese audiobooks&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; to drum in sentence construction. I'm not sure these are as well known as I thought they were (maybe it's a bit old-school and expensive in 2025), but I've found them a good accompaniment to the regular classes. Sitting there for 30 minutes each day trying to build sentences before the recorded response kicks in has been a great motivator.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm also using &lt;/span&gt;&lt;a href="https://apps.ankiweb.net" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Anki&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, which I'd heard of but never used before. I'm using &lt;/span&gt;&lt;a href="https://ankiweb.net/shared/info/1196762551" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a premade "deck" of 1500 basic vocabulary words&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; to try and boost my vocabulary at the same time as maybe helping me parse signage, documents, etc. while I'm in Japan.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Including hiragana/katakana practice and other comprehension exercises, all of this adds up to a couple of hours day of deliberative effort. Hopefully it pays off.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I can imagine a version of this whole thing that is more targeted at my specific requirements, i.e., navigation, food, transport, and accommodation, but I'm willing to press on in the hopes that it will leave me with some basic level of preparation that I can improve upon when I'm actually in Japan.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/preparing-for-the-japan-trip" rel="alternate"/>
    <published>2025-03-19T18:24:42.295Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;a href="http://duggan.ie/posts/traveling-to-japan-for-three-months…" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;As I wrote about in February, Ash and I are traveling to Japan for three months&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (May → August). It's a trip I've always wanted to take, and since I'm between work engagements I've been taking some time to prepare.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Three months is a long time to spend in a country you've never visited, and I'm going to go over a few ways we've been preparing.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/traveling-to-japan-for-three-months-because-lifes-too-short</id>
    <title type="text">Traveling to Japan for three months because life's too short</title>
    <updated>2025-02-10T16:49:16.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I've been fascinated by Japan since I was a teenager. Thanks to &lt;/span&gt;&lt;a href="https://" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Hayao Miyazaki&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (among others) the food, landscape, and architecture all exist in some dreamy, liminal space in my mind alongside wood sprites and lost civilizations.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Maybe it sounds daft, but the idea of actually visiting the country always seemed like it would break the spell somehow? Like anywhere, Japan has crime, social problems, bad weather, &lt;/span&gt;&lt;a href="https://www.youtube.com/watch?v=TbwlC2B-BIg" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;and if I go, me&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/87e8a7a28fa7d6b9.mp4" controls="" title="same-sad-you.mp4"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Same sad you.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;That said, I've had a plan (concepts of a plan 👐) to visit for years. I've recently dug out an old copy of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Rough Guide to Tokyo&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; that dates from 2011, which was the first time I started vaguely planning to go.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;However, that was also the year I started working for Engine Yard, which changed the entire trajectory of my career. This was a huge deal for me, but Engine Yard landing in Dublin was also a big deal for the local startup community – &lt;/span&gt;&lt;a href="https://www.siliconrepublic.com/start-ups/eamon-leonard-serves-as-standard-bearer-for-irelands-software-community" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Silicon Republic had an article in 2013 that gives a bit of a retrospective&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Plans to visit Japan went on the back burner.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="fast-forward-a-few-years" class="group relative"&gt;&lt;a href="#fast-forward-a-few-years" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Fast forward a few years&lt;/span&gt;&lt;/h3&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Speaking of back burners, in 2019, I was burning out at my job. This was well past Engine Yard, which I had left in 2014. Our startup had been acquired, and corporate bureaucracy was slowly draining away my will over the subsequent three year earn-out. There were plenty of smart and thoughtful people around, but somehow I felt like I was getting stupider.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The work was the most mind numbing drudgery. I had begun to hate writing software, and was terrified that if I didn't leave, I might never recover my enthusiasm.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Then sometime in early 2020 I read a series of blog posts by someone who had cycled the length of Japan, and it rekindled those old plans, but now on a grander scale. This was something I could aim for –&amp;nbsp;I was going to cycle the entire length of Japan.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Clearly this was a huge "my life is derailing, I must escape" signal, but my girlfriend was having a similar crisis and so we agreed on the broad strokes of a New Zealand / Japan cycling tour, taking six months or so to do the whole thing. We both had enough savings to take an extended break from work. I started cycling training in earnest.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;On March 1st 2020, I handed in my notice – this was going to be the year I did Japan.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: center;"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/f26d80379d7d484f.gif" alt="fate-it-seems-is-not-without-its-sense-of-irony.gif" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/f309f83061a5eb1e.mp4" controls="" title="covid-19-rte.mp4"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;RTÉ News from March 19th, 2020.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;As COVID-19 wore on, it became increasingly clear that there would be no Japan trip. I finished out my notice period, and it would be almost a year before I felt like I could write code again professionally. I spent the intervening time doing not very much – I exercised, lost weight, went for long rambles in the Phoenix Park. Visited South Africa while there were still travel restrictions – that was memorable – had to get "war zone" travel insurance as it was the only option that would provide cover during the pandemic.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;While I was in South Africa in 2021, David Coallier called and asked me to join as CTO of his new company, Clearword. I was ready to let go of Japan – I didn't want to spend another year on the sidelines, &lt;/span&gt;&lt;a href="https://www.irishtimes.com/business/technology/cork-based-clearword-raises-3-25m-in-seed-funding-1.4775688" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;and so I joined David&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; on what ended up being the most interesting and, hilariously, least personally remunerative startup we've done together. We had customers. We talked to them. We zeroed in on a niche. It felt really close to being something – but it never quite got there.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;So it goes.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="2025-the-year-we-make-contact" class="group relative"&gt;&lt;a href="#2025-the-year-we-make-contact" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;2025, the year we make contact&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;And so here we are, in 2025. I am unemployed but am not immediately looking for work.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Emotionally though, things could not be more different.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I was disappointed about Clearword, but the writing had been on the wall for a while, and I came to terms with it months ago. I'll write that all down another time, but the gist is that I am feeling ready to start something new.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I am also turning 40 this year. &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Life moves pretty fast&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;, and while I feel fitter and healthier today than I did ten years ago, I have to acknowledge that time is passing, and if I put this off now I may not have the opportunity again for a long time. And it's not just my own calendar that has to line up, but that of my girlfriend.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I also no longer feel the need to find myself by cycling the length of a country I've never even been to. I don't need a grand goal. I am going instead to soak in it. To be in the place and take it as it is. I want to do it peacefully and respectfully.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;There are places I want to visit, of course – Yakushima island, &lt;/span&gt;&lt;a href="https://www.behance.net/gallery/63811505/Yakushima-The-Forest-Spirit" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;the inspiration for the ethereal scenery&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; of &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Princess Mononoke&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; being top of my list.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/5f6fa32c5fd46e7c.jpg" alt="19897102270_7c4938cc49_k.jpg" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Photo by Takeshi Kuboki, via Flickr.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Climbing Mount Fuji seems like it would be neat, if it works out, but mostly we both want to go &lt;/span&gt;&lt;a href="https://www.japan-guide.com/news/overtourism.html" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;off the beaten path&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and avoid bustling around in crowds gawping at geishas, etc.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;So –&amp;nbsp;flights are booked. We are going for three months, start of May to start of August. We've booked our flights to arrive in Seoul rather than flying directly to Japan, as it gives us some options on whether we want to go north or south to start (regional flights from Seoul are relatively cheap). North, to Hokkaido, is sort of appealing with the chance of catching the sakura blossoms, but Yakushima is in the south, and it will be better to do that before summer gets into full swing.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;In the mean time, I am attending evening classes in the city to learn hopefully enough Japanese to get by. Thanks to years watching anime, I have a surprising amount of vocab floating around in my head, if not all of the practical variety. This week I managed to write out my basic &lt;/span&gt;&lt;a href="https://www.busuu.com/en/japanese/alphabet/hiragana" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;hiragana&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; table from memory, which feels impressive, yet is just the tip of the iceberg.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Three months sounds like a long time, but anyone I've spoken to tells me however long they spent, it wasn't enough. We'll be traveling light and keeping our plans loose, and we are going to have some adventures.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Can't wait. 🇯🇵&lt;/span&gt;&lt;/p&gt;&lt;details class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-2"&gt;&lt;summary class="cursor-pointer py-1 px-6 relative font-bold list-none outline-none text-zinc-900 dark:text-zinc-100 [&amp;::-webkit-details-marker]:hidden [&amp;::marker]:hidden before:content-[''] before:block before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:w-0 before:h-0 before:border-[6px] before:border-transparent before:border-l-black dark:before:border-l-white before:rotate-0 before:transition-transform [details[open]_&amp;]:before:rotate-90 [div[data-open]_&amp;]:before:rotate-90 hover:bg-zinc-100 dark:hover:bg-zinc-700/50"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Sources&lt;/span&gt;&lt;/p&gt;&lt;/summary&gt;&lt;div class="px-5 pb-1 pt-0 text-zinc-900 dark:text-zinc-100" data-lexical-collapsible-content="true"&gt;&lt;ol&gt;&lt;li value="1"&gt;&lt;a href="https://www.flickr.com/photos/kuboki/19897102270/in/photolist-wjeRiq-6zdkN3-Bo7KZF-Ay1FNM-7YQRtS-wA8pdh-gUDjjq-6y7cLt-caGDgy-gUDgSj-wBpfQB-wA8tKy-wjjUUH-2kWGUEv-7YiJXK-wjcLLb-2kWNA1J-7WLKr7-7XFUgT-7Wr93k-7XKaEA-7Wr96D-6snE6a-wjeGAo-7YMCFe-526Ddr-vDWYwi-wjcPS1-vDXbUz-Bo7FoM-526Dct-wAQ3Jc-7YiKeH-vE2EzB-Lrspmw-7YmZpy-wjeYbd-6sffrd-7WLKBE-wAQeyt-Bt6YEC-7Wr8YR-vDSrVs-wywHWj-7YMCP4-AxTuXU-7Wuo4Y-wywWV5-wAaemu-vDYYJi" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Yakushima photo by Takeshi Kuboki, via Flickr.&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;&lt;/details&gt;&lt;p dir="ltr"&gt;&lt;br&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/traveling-to-japan-for-three-months-because-lifes-too-short" rel="alternate"/>
    <published>2025-02-10T12:25:27.547Z</published>
    <summary type="html">&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I've been fascinated by Japan since I was a teenager. Thanks to &lt;/span&gt;&lt;a href="https://" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Hayao Miyazaki&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (among others) the food, landscape, and architecture all exist in some dreamy, liminal space in my mind alongside wood sprites and lost civilizations.&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Maybe it sounds daft, but the idea of actually visiting the country always seemed like it would break the spell somehow? Like anywhere, Japan has crime, social problems, bad weather, and if I go, me.&lt;/span&gt;&lt;br&gt;&lt;/p&gt;&lt;p style="text-align: start;"&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/5f6fa32c5fd46e7c.jpg" alt="19897102270_7c4938cc49_k.jpg" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/hacking-together-a-job-runner-using-supabase-edge-functions</id>
    <title type="text">Hacking together a Job Runner using Supabase Edge Functions</title>
    <updated>2025-02-11T00:00:22.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;If you're not familiar with &lt;/span&gt;&lt;a href="http://supabase.com/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, it's a collection of backend services (Postgres, PostgREST auth service, S3 storage, function runtime) that you can develop against locally and then deploy to production.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The Supabase team have also spent time making each of these parts complement each other, and since they're stitching together different open source projects this is seriously impressive. They also have in-depth documentation and examples, and the dashboard is top notch.&lt;/span&gt;&lt;/p&gt;&lt;details class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-2"&gt;&lt;summary class="cursor-pointer py-1 px-6 relative font-bold list-none outline-none text-zinc-900 dark:text-zinc-100 [&amp;::-webkit-details-marker]:hidden [&amp;::marker]:hidden before:content-[''] before:block before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:w-0 before:h-0 before:border-[6px] before:border-transparent before:border-l-black dark:before:border-l-white before:rotate-0 before:transition-transform [details[open]_&amp;]:before:rotate-90 [div[data-open]_&amp;]:before:rotate-90 hover:bg-zinc-100 dark:hover:bg-zinc-700/50"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;ℹ️ What makes Supabase so useful?&lt;/span&gt;&lt;/p&gt;&lt;/summary&gt;&lt;div class="px-5 pb-1 pt-0 text-zinc-900 dark:text-zinc-100" data-lexical-collapsible-content="true"&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;For example, items uploaded to the storage service are accessible via the database. The auth service is also hooked directly into the database, with convenience functions to wire up user permissions to the authenticated users. This means you can enforce object storage access permissions (&lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;using Postgres Row-Level Security&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;) all the way through the stack via the database:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4"&gt;&lt;span style="white-space: pre-wrap;"&gt;create policy &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Individual user Access"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;on storage&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;objects &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; select&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;to authenticated&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;using&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; owner_id&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;uuid &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Additionally, the database is presented via a REST interface using &lt;/span&gt;&lt;a href="https://docs.postgrest.org/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;PostgREST&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. With the aforementioned RLS policies, this gives you a permission-controlled API that you can auto-generate clients for. That means a React application can have a type-hinted, robustly authorized interface to the database – this makes it easier to build systems that are both secure and have a nice developer experience.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is powerful stuff, and not necessarily easy to grok, but ultimately you're just learning how to use Postgres, so the knowledge is portable.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Anyway, this is all great stuff, but at some point I wanted to be able to run a bunch of asynchronous work in the background.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="background-jobs" class="group relative"&gt;&lt;a href="#background-jobs" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Background jobs&lt;/span&gt;&lt;/h2&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;What you need out of a background job runner varies wildly. Supabase provides some helpful primitives around this that are enough to get started with –&amp;nbsp;you can fire off &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/functions" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Edge Functions&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; after a request and set them to process as &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/functions/background-tasks" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Background Tasks&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, and you can use &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_cron&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; to run them on a schedule.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;If you just want to run a few static tasks regularly with Supabase, send emails, or fire off an API request after a user action, it's pretty straight forward. There are also ways to set up triggers, and &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/database/webhooks" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;fire webhooks&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; on database events.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;What I needed was a system where I could regularly run tasks in the context of a user –&amp;nbsp;AI agents running on behalf of the user, checking third party services for new information, etc. Something which might take a while to run, or is part of automation that will trigger another action, maybe alert a customer.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I wanted to be able to queue up sometimes thousands of jobs to run –&amp;nbsp;this would help me stagger their execution so that I wasn't overwhelming either the Supabase Edge Function dispatcher or my own APIs.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;It was also important to me to keep the overall infrastructure stack as lean as possible, because I was deploying a stack per-company to keep resources isolated, as well as geographically proximate to the people using it.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="color: rgb(0, 0, 0); font-weight: 400; text-decoration: none; white-space: pre-wrap;"&gt;There are, of course, third-party job runner systems like &lt;/span&gt;&lt;a href="http://inngest.com/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Inngest&lt;/span&gt;&lt;/a&gt;&lt;span style="color: rgb(0, 0, 0); font-weight: 400; text-decoration: none; white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="http://hatchet.run" rel="noreferrer"&gt;&lt;span style="color: rgb(0, 0, 0); font-weight: 400; text-decoration: none; white-space: pre-wrap;"&gt;Hatchet&lt;/span&gt;&lt;/a&gt;&lt;span style="color: rgb(0, 0, 0); font-weight: 400; text-decoration: none; white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="https://trigger.dev/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;trigger&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, or &lt;/span&gt;&lt;a href="https://temporal.io" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Temporal&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, some of which are open source, have UIs, a local dev system, and I'm sure lots of other useful features. But this is a class of problem that ranges from "Google Calendar reminder for me to run a script manually on Sundays" to "distributed MapReduce operation across a Kubernetes cluster" –&amp;nbsp;you'll want something that suits your own circumstances.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="cobbling-together-a-job-runner" class="group relative"&gt;&lt;a href="#cobbling-together-a-job-runner" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Cobbling together a job runner&lt;/span&gt;&lt;/h2&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The Supabase Edge runtime environment takes the form of Deno functions triggered via HTTP, and authorized using &lt;/span&gt;&lt;a href="https://jwt.io/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;JWT&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. They are normal HTTP API endpoints, you can send payloads to them, stream results and return status codes.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase also provides a PostgreSQL database with a variety of Postgres extensions installed, including &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_cron&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_net&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;With those pieces in place, we have the primitives for a system that can:&lt;/span&gt;&lt;/p&gt;&lt;ol&gt;&lt;li value="1" style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;store a list of jobs and config&lt;/span&gt;&lt;/li&gt;&lt;li value="2" style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;run Typescript functions (with some resource limits, see &lt;/span&gt;&lt;a href="#caveats" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Caveats&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;/li&gt;&lt;li value="3" style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;run tasks on a schedule&lt;/span&gt;&lt;/li&gt;&lt;li value="4" style="text-align: start;"&gt;&lt;span style="white-space: pre-wrap;"&gt;execute and evaluate the success of functions called over HTTP&lt;/span&gt;&lt;/li&gt;&lt;li value="5"&gt;&lt;span style="white-space: pre-wrap;"&gt;propagate user authorization context into the edge functions&lt;/span&gt;&lt;/li&gt;&lt;li value="6"&gt;&lt;span style="white-space: pre-wrap;"&gt;be permission controlled via RLS&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The basic idea is that you write your jobs as Supabase Edge Functions, and you control when they're executed and what data they are passed using the database. The name of the job should map directly to the name of the Supabase Edge Function associated with it. &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_cron&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; is used to pick up new jobs and clean up old ones.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Because it's implemented in the database, you can use it via Supabase's dashboard too, so if you want to see what jobs are running, fix them or run them manually, you can do it easily via the UI:&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;/p&gt;&lt;figure class="video-wrapper"&gt;&lt;video src="/files/5f94342354608ec1.mp4" controls="" title="queue_job.mp4"&gt;&lt;/video&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Running queue_job via the Supabase Dashboard.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase's dashboard gives you the option to impersonate a user and run queries as them, which is immensely helpful when you have a lot of business logic and permissions encoded in SQL.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;I found the only public endpoints I needed were some functions to queue and dequeue tasks on behalf of the user, everything else was internal to the job system and rarely needs to be used directly.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;So I ended up with a system where a job is queued from the code like:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9"&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; error &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;await&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; supabase&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;rpc&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"queue_job"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_job_name&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"example-job"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_request_body&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"data"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenBoolean" style="white-space: pre-wrap;"&gt;true&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Or more frequently as part of a database trigger:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;queue_directory_agent_for_meeting_trigger&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TRIGGER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    existing_run &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Check &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; an agent run is already pending&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;running &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;this&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; meeting&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; existing_run&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;agent_run&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; request_body &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'meetingId'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NEW&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;meeting_id&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;text&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; agent_name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'directory-agent'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; existing_run &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; No pending&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;/&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;running run exists&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; queue the agent &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the meeting&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;queue_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'directory-agent'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'meetingId'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NEW&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;meeting_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NEW&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TRIGGER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; queue_directory_agent_after_insert_update&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AFTER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INSERT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user_meetings&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EACH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ROW&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXECUTE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;queue_directory_agent_for_meeting_trigger&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;That then gets picked up by the job system, which then runs the task via Supabase Edge Functions.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;There is a lot of code on this page – feel free to copy and paste it into an LLM to ask questions and customize. &lt;/span&gt;&lt;a href="https://github.com/duggan/supabae-job-runner" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;You can find all the code in this Github repository&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="assumptions" class="group relative"&gt;&lt;a href="#assumptions" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Assumptions&lt;/span&gt;&lt;/h2&gt;&lt;p dir="ltr"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;This setup assumes you want to run a job in the permission context of each user.&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; There's nothing to stop you having global jobs, I just didn't need/want them so they're not accounted for in this set up.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;Every Edge Function needs to accept and respond with headers it receives from the job runner&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; for tracking purposes (&lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;X-Job&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;X-Correlation-ID&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;). I originally tried doing this with internal request IDs for &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_net&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;, but they're just integers that are reset when the database is restarted, so not reliable.&lt;/span&gt;&lt;/p&gt;&lt;h2 id="some-initial-configuration" class="group relative"&gt;&lt;a href="#some-initial-configuration" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Some initial configuration&lt;/span&gt;&lt;/h2&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm making use of a few extensions and a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;private&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; schema:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXTENSION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXISTS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; pg_cron &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SCHEMA&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXTENSION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXISTS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; pgjwt &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SCHEMA&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXTENSION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXISTS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; pg_net &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SCHEMA&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SCHEMA&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;And the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;supabase_url()&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; helper function is something I picked up from example projects. It's useful for getting the URL for your REST endpoints within the database (part of how the system will execute Edge Functions):&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;supabase_url&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  secret_value &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; decrypted_secret &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; vault&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;decrypted_secrets &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'supabase_url'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;You may have noticed this pulls a &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/database/vault" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase Vault&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; secret. This is really just somewhere to keep a variable for the project URL. You can configure this locally with a statement in your &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;seed.sql&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; file like:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    secret_value &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Check &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the secret already exists&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; decrypted_secret &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; vault&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;decrypted_secrets &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'supabase_url'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; If secret_value is &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;null&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; it means the secret does not exist&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; so create it&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; vault&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;create_secret&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'http://api.supabase.internal:8000'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'supabase_url'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;But if you're deploying this to production, you'll want to copy your supabase URL from the &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;API&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; → &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;API Settings&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; page:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/8433ca1804068610.png" alt="Screenshot 2025-01-15 at 16.39.34.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Into Vault:&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/5a54d496b20ed906.png" alt="Screenshot 2025-01-15 at 16.46.01.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Due to security changes in November 2024, we also need a function for retrieving the JWT secret:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jwt_secret&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  secret_value &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; decrypted_secret &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; vault&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;decrypted_secrets &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'app.jwt_secret'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; secret_value&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Again, you'll need to populate this secret value via the Vault interface. You can find the appropriate a few settings below the Project URL:&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/0ef7656c9ab84f02.png" alt="Screenshot 2025-01-21 at 16.35.16.png" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;h2 id="tables" class="group relative"&gt;&lt;a href="#tables" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Tables&lt;/span&gt;&lt;/h2&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I've defined three tables, &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;job&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;job_config&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;, and &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;job_logs&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt;. Everything else is private functions for managing the state in those tables and in &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_net&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;, and defining &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_cron&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; jobs.&lt;/span&gt;&lt;/p&gt;&lt;h4 id="job" class="group relative"&gt;&lt;a href="#job" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;/h4&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is the execution information for a specific job. Most of the column names are self-explanatory, but just to highlight that &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;request_body&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; is what will be send as a JSON payload to the defined Supabase Edge Function.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;An RLS policy allows the user CRUD operations on the table for their own jobs.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Indexes are included which might be useful by the time your job system has been running for a while, but at the same time you might be just as well deleting older &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;completed&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; jobs.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PRIMARY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;KEY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uuid_generate_v4&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    user_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REFERENCES&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;users&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DELETE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CASCADE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    job_name &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CHECK&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TRIM&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;lt;&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;''&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    request_body &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSONB&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;0&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    max_retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;3&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    created_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOW&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    updated_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOW&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    last_run_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    next_run_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    locked_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CONSTRAINT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; chk_valid_state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CHECK&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'completed'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'failed'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'canceled'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ALTER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ENABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ROW&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LEVEL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;POLICY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Users can manage their own job runs"&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ALL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; authenticated &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;USING&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user_id&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CHECK&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user_id&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INDEX&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; idx_job_user_jobname &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INDEX&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; idx_job_state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;state&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INDEX&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; idx_job_locked_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;locked_at&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INDEX&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; idx_job_pending &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h4 id="job-config" class="group relative"&gt;&lt;a href="#job-config" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;job_config&lt;/span&gt;&lt;/h4&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is used to control configuration for all jobs with a specific name, in this case enabling/disabling the jobs and controlling the concurrency. RLS is enabled, but no policy is added, meaning it is not directly accessible to users.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job_config&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    job_name &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PRIMARY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;KEY&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    concurrency_limit &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CHECK&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;concurrency_limit &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;0&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    enabled &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BOOLEAN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ALTER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_config &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ENABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ROW&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LEVEL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h4 id="job-logs" class="group relative"&gt;&lt;a href="#job-logs" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;job_logs&lt;/span&gt;&lt;/h4&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Basic job logging. This could be made more useful –&amp;nbsp;what it does at the moment is log when a job starts and finishes. RLS policy for user CRUD also included.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job_run_logs&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PRIMARY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;KEY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uuid_generate_v4&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    job_run_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REFERENCES&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job_run&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DELETE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ACTION&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    log_message &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    log_level &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'INFO'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    created_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIME&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ZONE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ALTER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_run_logs &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ENABLE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ROW&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LEVEL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;POLICY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Users can manage their own job run logs"&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_logs &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ALL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; authenticated &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;USING&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXISTS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_logs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WITH&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CHECK&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;EXISTS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_logs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h2 id="public-functions" class="group relative"&gt;&lt;a href="#public-functions" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Public functions&lt;/span&gt;&lt;/h2&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;As I mentioned, there's really only two functions I needed to use on a regular basis, one to queue up a new job, and another to remove a job from the queue. You could have functions to list jobs, etc, but I found just &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;SELECT&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; over the table was all I needed.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42"&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Public methods&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;queue_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_name &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _request_body &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSONB&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _max_retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; search_path &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  new_job_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Insert a &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the current user&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INSERT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; request_body&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; max_retries&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VALUES&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;uid&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _request_body&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _max_retries&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNING&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; new_job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Log the creation &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;of&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;new_job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run created'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Return the &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;of&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; new_job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;dequeue_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; search_path &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Update the job to &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'canceled'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; it is still &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;in&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the pending state&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'canceled'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Log the cancellation &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;of&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run canceled'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h2 id="management-functions" class="group relative"&gt;&lt;a href="#management-functions" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Management functions&lt;/span&gt;&lt;/h2&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;All of these are created in a schema named &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;private&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;, which by default has no permissions attached to it, and is not presented via the REST API. That means (without specific configuration) they are executable only by special roles like the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;postgres&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; user, which is exactly what we need.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="helper-generate-a-jwt-for-a-user" class="group relative"&gt;&lt;a href="#helper-generate-a-jwt-for-a-user" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Helper: generate a JWT for a user&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is a helper function that lets your job management system create a valid JWT token that it can use to execute Edge Functions on behalf of a user.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;This is handy, and also one you have to be very careful about. You don't want to accidentally allow anyone to mint a JWT for any user via the API just by suppling the user's ID 😅&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Being in the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;private&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; schema should prevent this, and the user_id is a UUID (so not amenable to iterating through like integers), but just keep in mind that you probably don't want a &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;public&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; schema version of this function.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;generate_user_jwt&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;p_user_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_role &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_email &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; search_path &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  jwt_token &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  exp_time &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Calculate expiration &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;time&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; hour from now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;exp_time&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;+&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; interval &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'1 hour'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Generate the &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JWT&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;jwt_token&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;sign&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;payload&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;json_build_object&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'sub'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'aud'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_role&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'role'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_role&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'email'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; p_user_email&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'iat'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;extract&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;epoch from &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'exp'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;extract&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;epoch from exp_time&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;json&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;secret&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;select &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jwt_secret&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;algorithm&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'HS256'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'Bearer '&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;||&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; jwt_token&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="running-jobs" class="group relative"&gt;&lt;a href="#running-jobs" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Running jobs&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;These two functions handle job initialization. The http request lifecycle is managed via &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_net&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;, the rest of the work is in checking job state and concurrency management.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;run_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_name &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _request_body &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSONB&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _user_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _role &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _email &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  timeout_milliseconds &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;5&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;60&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1000&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  auth_header &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Generate the &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JWT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the current user based on their role and email&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;auth_header&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;generate_user_jwt&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _role&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _email&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    net&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;http_post&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;url&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;supabase_url&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;||&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'/functions/v1/'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;||&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'Content-Type'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'application/json'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'Authorization'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; auth_header&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'X-Correlation-ID'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_job_id&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'X-Job'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'true'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;body&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _request_body&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;timeout_milliseconds&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; timeout_milliseconds&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;run_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; search_path &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    job_group &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RECORD&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    max_concurrency &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    running_jobs &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    available_slots &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    job_enabled &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BOOLEAN&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    current_job &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RECORD&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Fetch all user&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job pairs &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;with&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; pending jobs&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;locked_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;next_run_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;next_run_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;lt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;GROUP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Skip &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job is disabled&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; enabled &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_enabled&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_config&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NOT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_enabled &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RAISE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOG&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job %s disabled via job_config'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CONTINUE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Get the global concurrency limit &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; concurrency_limit &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; max_concurrency&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_config&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Default to &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; not specified&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; max_concurrency &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;max_concurrency&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Count the number &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;of&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; currently running jobs &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;this&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job pair&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;COUNT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; running_jobs&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user_id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Calculate available slots &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;this&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job pair&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;available_slots&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; max_concurrency &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; running_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; available_slots &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;0&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Select pending jobs up to the available slots&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; current_job &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;request_body&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; u&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; u&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;role&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; u&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;email&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JOIN&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;users u &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ON&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; u&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_group&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;locked_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;next_run_at &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;next_run_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;lt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ORDER&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;created_at&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LIMIT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; available_slots&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SKIP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOCKED&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Update the job to &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;set&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;last_run_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Log the start &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;of&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the job run&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run started'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Execute the job using pg_net&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;run_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_name&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;request_body&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;role&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                    current_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;email&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;                &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="process-jobs" class="group relative"&gt;&lt;a href="#process-jobs" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Process jobs&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This will check through &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_net&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt;'s internal &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;_http_response&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; table for requests that have been annotated with the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;X-Job&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; header.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;From there, it matches up the &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;X-Correlation-ID&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; header to the corresponding &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;job&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; table entry, and decides whether the job was successful based on the HTTP status code returned.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;process_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; search_path &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; extensions&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  response &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RECORD&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  job_record &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RECORD&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  normalized_headers &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSONB&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; net&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_http_response&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SKIP&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOCKED&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Lock the response to prevent concurrent processing&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Normalize headers to lowercase&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jsonb_object_agg&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;lower&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;key&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; value&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; normalized_headers&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;jsonb_each_text&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; normalized_headers &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;?&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'x-job'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Find and lock the corresponding job record&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; normalized_headers &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;?&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'x-correlation-id'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_record&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;normalized_headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'x-correlation-id'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FOUND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DELETE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; net&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_http_response &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status_code &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;200&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status_code &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;201&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status_code &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;204&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Success&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Update job to &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'completed'&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'completed'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job_record&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Log the successful completion&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_record&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run completed successfully'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ELSE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Failure&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Handle retries or mark &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;as&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; failed&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;fail_job_with_retry&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_record&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;PERFORM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_record&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;            &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run failed and will be retried'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ELSE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RAISE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOG&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'job run failed due to a missing X-Correlation-ID header. '&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;              &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'This is a permanent failure and will not be retried.'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DELETE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; net&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_http_response &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LOOP&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="logging" class="group relative"&gt;&lt;a href="#logging" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Logging&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is relatively barebones, and just tracks job starting/finishing.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12"&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;public&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log_job&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _log_message &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _log_level &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;TEXT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'INFO'&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INSERT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;job_logs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; log_message&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; log_level&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VALUES&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;_job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _log_message&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _log_level&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="handling-failed-jobs" class="group relative"&gt;&lt;a href="#handling-failed-jobs" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Handling failed jobs&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Jobs fail, the system will handle failures with configurable retries, and reset jobs where the edge function never returned a status (due to resource limits, this can happen).&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49"&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; mark a job &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;as&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; failed&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; and queue &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; a retry &lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;fail_job_with_retry&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _job_id &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UUID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DECLARE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _max_retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INT&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Fetch the current retries and max_retries &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;for&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; the job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; retries&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; max_retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTO&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _retries&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _max_retries&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FROM&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _retries &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;+&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _max_retries &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;THEN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Mark job &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;as&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; permanently failed&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'failed'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;ELSE&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Increment retry count and schedule next retry&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; retries &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _retries &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;+&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;     &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;	&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; Exponential backoff&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        next_run_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;+&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTERVAL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'5 minutes'&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;*&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;2&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;^&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _retries&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; id &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _job_id&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;IF&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;--&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; When a job has expired&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; reset its status to pending&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;CREATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;OR&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;REPLACE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;FUNCTION&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;reset_stuck_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  _timeout &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;INTERVAL&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFAULT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'15 minutes'&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;RETURNS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;VOID&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SECURITY&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;DEFINER&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AS&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;BEGIN&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;UPDATE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; job&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SET&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;NULL&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'pending'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; updated_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;WHERE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; locked_at &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;lt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;now&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; _timeout&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;AND&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; state &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'running'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;END&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;$$ &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;LANGUAGE&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; plpgsql&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="pg-cron" class="group relative"&gt;&lt;a href="#pg-cron" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;pg_cron&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;With &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;pg_cron&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; enabled, you can configure the management functions to execute on a schedule that suits your workloads.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Below I've chosen to run the jobs very frequently because I wanted a relatively high throughput, but it can be tweaked to your preferences. One thing though: I would try to have &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;run-jobs&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;process-jobs&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; run out of sync with each other. You don't really want the job that picks up work running at the exact same time as the job that determines whether the work is completed, as it might introduce delays.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24"&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; cron&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;schedule&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'run-jobs'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'20 seconds'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;run_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; cron&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;schedule&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'process-jobs'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'6 seconds'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$ &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;process_jobs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; cron&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;schedule&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'reset-stuck-jobs'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'*/8 * * * *'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SELECT&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;private&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;reset_stuck_job_runs&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;'8 minutes'&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  $$&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h3 id="edge-functions" class="group relative"&gt;&lt;a href="#edge-functions" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Edge functions&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is a barebones edge function that contains only the logic required to successfully process a job.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;The important thing is that it responds in a way that the management functions can process. In the Postgres functions above, this means accepting and responding with specific headers to identify the request as coming from the job runner, and returning HTTP status codes to indicate whether the job was successful or not.&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82"&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;import&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"@supabase/edge-runtime"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;import&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; createClient &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;from&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"@supabase/supabase-js"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Deno&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;serve&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;async&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;req&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&amp;gt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; data &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;await&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; req&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;json&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenComment" style="white-space: pre-wrap;"&gt;// X-Job and X-Correlation-ID are used as part of the Job Runner system.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; correlationId &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; req&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;get&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"X-Correlation-ID"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; isJob &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; req&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;get&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"X-Job"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;correlationId &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;||&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;isJob&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;error&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Missing required Job Runner headers"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;return&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;Response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSON&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;stringify&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;error&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Missing required Job Runner headers"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;400&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"Content-Type"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"application/json"&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"X-Correlation-ID: "&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; correlationId&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"X-Job"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; isJob&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; authHeader &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; req&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;get&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Authorization"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; supabase &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; createClient&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;lt;&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;Database&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;&amp;gt;&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    Deno&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;env&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;get&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"SUPABASE_URL"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;??&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;""&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    Deno&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;env&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;get&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"SUPABASE_ANON_KEY"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;??&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;""&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;global&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;Authorization&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; authHeader &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; token &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; authHeader&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;replace&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Bearer "&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;""&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;data&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; user &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;await&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; supabase&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;auth&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;getUser&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;token&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenComment" style="white-space: pre-wrap;"&gt;/**&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenComment" style="white-space: pre-wrap;"&gt;   * Only run if we have a user context&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenComment" style="white-space: pre-wrap;"&gt;   */&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;user&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;?.&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;email&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;error&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"No valid user token received"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;return&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;Response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;JSON&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;stringify&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;error&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;401&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"Content-Type"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"application/json"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Correlation-ID"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; correlationId&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;          &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Job"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; isJob&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenComment" style="white-space: pre-wrap;"&gt;// This is about where the rest of your worker logic will go.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;const&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; success &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;=&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;!&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;data&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;success&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;log&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Successfully processed request"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;return&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;Response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;null&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;204&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"Content-Type"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"application/json"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Correlation-ID"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; correlationId&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Job"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; isJob&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  console&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;error&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"Failed to process request"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;return&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;new&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenClassName" style="white-space: pre-wrap;"&gt;Response&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;null&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;status&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;500&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;headers&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;{&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"Content-Type"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"application/json"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Correlation-ID"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; correlationId&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;"X-Job"&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; isJob&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;  &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;}&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;h2 id="caveats" class="group relative"&gt;&lt;a href="#caveats" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Caveats&lt;/span&gt;&lt;/h2&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;For me, this has worked best for tasks that are mostly I/O-bound – API calls, reading from the database, saving to the database, transforming JSON. Supabase Edge Functions have &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/functions/limits" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a few limits&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, but working around the &lt;/span&gt;&lt;b&gt;&lt;strong class="font-semibold" style="white-space: pre-wrap;"&gt;two second&lt;/strong&gt;&lt;/b&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;CPU time&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; one is particularly awkward. Forget running ML tasks, and decompressing a large-ish file won't work either.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The good news is that it seems Supabase will be adding flexibility there in 2025:&lt;/span&gt;&lt;/p&gt;&lt;blockquote style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;We have a very exciting roadmap planned for 2025. One of the main priorities is to provide customizable compute limits (memory, CPU, and execution duration). We will soon announce an update on it.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;–&amp;nbsp;&lt;/span&gt;&lt;a href="https://supabase.com/blog/edge-functions-background-tasks-websockets" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase blog&lt;/span&gt;&lt;/a&gt;&lt;/blockquote&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;In the mean time it is prudent to break jobs up into discrete units of work. For example, instead of a single job that does a given task for all users, create a "router" task that queues up the task for each user.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;There's plenty of ways you can extend this –&amp;nbsp;I never added recurring jobs, for example, but I don't think it would be too tricky. You might also not like other decisions I made, like wiring the job names directly to the functions, but that is an intentional choice on my part to try and ensure the functions didn't end up too large.&lt;/span&gt;&lt;/p&gt;&lt;p style="text-align: start;" dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Most importantly, this is a system that worked for me. A a job system goes, it is, shall we say... &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;no frills&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. But it might be interesting to some others.&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;br&gt;&lt;br&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Corrections and suggestions also welcome, you might prefer to use the &lt;/em&gt;&lt;/i&gt;&lt;a href="https://github.com/duggan/supabae-job-runner" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;accompanying Github repository&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt; if so.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/hacking-together-a-job-runner-using-supabase-edge-functions" rel="alternate"/>
    <published>2025-01-23T17:39:18.671Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase is a collection of backend services (Postgres, PostgREST auth service, S3 storage, function runtime) that you can develop against locally and then deploy to production.&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;They don't yet provide a fully integrated background job running system. This post shows a detailed example of how you might keep your stack slim and build a system like this for yourself in Supabase, rather than using a third party!&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/ignoring-good-advice-and-building-my-own-blog-again</id>
    <title type="text">Ignoring good advice and building my own blog (again)</title>
    <updated>2025-02-11T00:00:34.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;For various reasons, I stopped blogging about ten years ago.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I ended up redirecting the energy into coding, and occasionally writing posts for the blogs of companies I worked for. But after Barricade was acquired by Sophos in 2016 I stopped writing almost entirely.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;My current company, &lt;/span&gt;&lt;a href="http://clearword.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Clearword&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, is winding down and I'm lucky enough that I don't have to line up any work immediately, so I've decided to start writing again –&amp;nbsp;and build some software along the way.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;It's kind of a classic developer mistake to build a blog before they start writing. &lt;/span&gt;&lt;a href="https://micro.webology.dev/2024/11/02/please-publish-and.html" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;As Jeff Triplett writes&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;blockquote dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;"Write and publish before you write your own static site generator or perfect blogging platform. We have lost billions of good writers to this side quest because they spend all their time working on the platform instead of writing."&lt;/span&gt;&lt;/blockquote&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is good advice. I am just preternaturally incapable of following it.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/a1ab54a9fb768bfe.jpg" alt="lolscorpion.jpg" width="inherit" height="inherit"&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The first time I build any significant software was when I cobbled together my own blogging system in college. It was styled with tables and rounded corner gifs and stitched together with PHP 4:&lt;/span&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="image-wrapper"&gt;&lt;img src="/files/e808965a05136485.png" alt="cult-redbrick.png" width="inherit" height="inherit"&gt;&lt;figcaption class="not-prose font-sans text-sm text-neutral-400 whitespace-pre-wrap break-words mt-2"&gt;Mad skillz.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;After college I used Wordpress, Blogspot and Medium as I moved away from writing web apps and into managing web infrastructure and writing backend services. For me, writing my own blogging system again ~20 years later feels like coming home.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="anti-goals" class="group relative"&gt;&lt;a href="#anti-goals" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Anti-goals&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;When I was jotting down ideas, I put some of these under the heading "anti-goals" – stuff I wanted to consciously avoid.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I didn't want to use a hosted third-party platform. I've gone down that route a couple of times now, and the results have always been unsatisfactory.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I also wanted to explicitly avoid building anything that was throwing up newsletter popups, social logins, invasive analytics scripts, engagement tricks, progress bars and the inevitable creep of AI writing assistance.&lt;/span&gt;&lt;/p&gt;&lt;details class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-2" open="true"&gt;&lt;summary class="cursor-pointer py-1 px-6 relative font-bold list-none outline-none text-zinc-900 dark:text-zinc-100 [&amp;::-webkit-details-marker]:hidden [&amp;::marker]:hidden before:content-[''] before:block before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:w-0 before:h-0 before:border-[6px] before:border-transparent before:border-l-black dark:before:border-l-white before:rotate-0 before:transition-transform [details[open]_&amp;]:before:rotate-90 [div[data-open]_&amp;]:before:rotate-90 hover:bg-zinc-100 dark:hover:bg-zinc-700/50"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;On Ghost&lt;/span&gt;&lt;/p&gt;&lt;/summary&gt;&lt;div class="px-5 pb-1 pt-0 text-zinc-900 dark:text-zinc-100" data-lexical-collapsible-content="true"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;One thing that I missed entirely in my first survey was &lt;/span&gt;&lt;a href="http://ghost.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Ghost&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. I remember looking into it a few years ago and not liking it for some (probably inane) reason. It's marketed as a professional publishing platform, with subscriber content, newsletters, etc., none of which is what I want. However, I ended up checking it out after I'd built a good fraction of my own blog and found they'd made similar choices to me, which means I think they are pragmatic and have excellent taste 🧑‍🍳.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Something else I wanted to avoid was writing everything in Markdown and committing to git. There are understandable reasons why many developers choose it – they do not (as a rule) like running databases. Markdown is text, and git is basically a database for text without all the hassle of a live process and SQL.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;The idea of writing long-form English posts in Markdown is just anathema to me, even as a developer. It's barely a step above writing BBCode (shots fired!) and feels like giving up too much of the experience of writing. Fortunately this is the web, and we can all make different choices 🙂&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;What I do want out of my blog is still a work in progress. Partly what I want from it is for it to &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;be&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; a work in progress, not a publishing platform. I've spent the last fifteen years building software for other people, and I'm keen to get back to building software that is just for myself. Not that I won't share parts of it when I think I have something useful to share. However, I don't want to become a blogging platform maintainer. I don't want users. I don't want customers.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="dual-stack" class="group relative"&gt;&lt;a href="#dual-stack" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Dual stack&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Maybe it sounds obvious, but a blog is two software stacks – one for reading, and one for writing. It's much more obvious with static site generators, since you are perhaps using a text editor to write your posts, committing to git, and then the rest is taken up by a build process in Github, which creates the HTML, CSS, etc.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I only really considered this when I started thinking about what I wanted from a blog, and two stand out goals were a rich Notion-like authoring experience, but a fast, cacheable, low-Javascript experience for readers. These goals seemed to be in conflict.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Application developers build for multiple audiences all the time, but when you're building a B2B SaaS app that may just mean API consumers and logged in humans. Blogs are interesting because the largest visual component of the system should appear very similar for both sets of users, it's just that the author also needs a bunch of bells and whistles for managing the text.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt; For me, the ideal writing experience sits somewhere between the minimalism of Medium and the digital Swiss Army Knife documentation authoring of Notion (sans the slightly too in-your-face AI bits).&lt;/span&gt;&lt;/p&gt;&lt;h3 id="the-next-social-web" class="group relative"&gt;&lt;a href="#the-next-social-web" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;The next social web&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I used to be very online, and specifically very online on Twitter. That has waned in recent years, but I'm interested in where things are going with &lt;/span&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Mastodon&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;a href="https://bsky.app/profile/duggan.ie" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Bluesky&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. I have a preference for Mastodon, partly because Bluesky has more visible US political drama, and partly because I have an anarchist streak in me that wants to see social media unbundled.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;As a result, I want to make the blog have a native understanding of at least ActivityPub. I love the idea that my blog could be followed on Mastodon (still working on this). It will probably be a little complicated to provide a legible variant of the posts, but perhaps not much more complicated than Atom/RSS.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Speaking of which, I also wanted to provide an Atom/RSS feed, even though I'm not convinced there's a large audience. I always wanted a nicely styled, standards-compliant feed though, so it's there.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="passkeys" class="group relative"&gt;&lt;a href="#passkeys" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Passkeys&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I follow &lt;/span&gt;&lt;a href="https://hachyderm.io/@rmondello" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Ricky Mondello&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; on Mastodon, and they have a nuanced perspective on password managers and &lt;/span&gt;&lt;a href="https://webauthn.guide" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;WebAuthn&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;. As a result, passkeys were unusually present in my thoughts when I was building the system, and so I included passkey support using the excellent (if search-resistant) &lt;/span&gt;&lt;a href="https://www.better-auth.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Better-Auth&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; library.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;Better-Auth has support for passkeys, but last I checked the user must first register with a password, which means passkeys are only a second-factor – not ideal. I haven't checked whether this can be easily patched yet, though it's on my list.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="experiments-and-fun" class="group relative"&gt;&lt;a href="#experiments-and-fun" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Experiments and fun&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I also wanted to have some fun, muck around and get deep into various parts of the stack. There's folk wisdom amongst software developers that &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;code is a liability, but knowledge is an asset&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;. The idea is that code requires maintenance, and the more of it you have the harder it is to manage effectively. It may even become an impediment to building what you need in the future. However, what you &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;learn&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; writing that code can have compounding value, making for better developers and better teams.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;That's not to suggest that I'm going to religiously account for every tangent as a "learning opportunity" – I'm my only real audience here. I don't have to attend a standup and sheepishly  tell people I spent a day or two making a kaleidoscopic rainbow button on a whim before discarding it as a bad idea. Or that while mucking around with SVG animations I accidentally created &lt;/span&gt;&lt;a href="http://duggan.ie/404" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;a noisy "O" effect on my 404 page&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and kept it because it reminds me of the Holy Grail from &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Fate/stay night&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;A lot of the original idea was just writing down developer experience goals like "I want to style everything with &lt;/span&gt;&lt;a href="https://tailwindcss.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Tailwind&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;" or "I want to use &lt;/span&gt;&lt;a href="https://ui.shadcn.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;shadcn&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; for editor components" and then doggedly pursuing those goals in the face of all headwinds just because it's an aesthetic choice I like.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="technical-choices" class="group relative"&gt;&lt;a href="#technical-choices" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Technical choices&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm in two minds about whether to even write these down at this stage, but maybe a quick overview is informative about where I started versus where I ended up? No blanket technological judgements intended 🙂&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;It's odd to realize this, but as someone with a long history in operations, operational simplicity was not an explicit goal when I began – I was initially more interested in how quickly I could put it together.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;I think that's reflective of the last half a year or mad dash building &lt;/span&gt;&lt;a href="https://oggam.ai" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Oggam&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I started with &lt;/span&gt;&lt;a href="http://nextjs.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Next.js&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and &lt;/span&gt;&lt;a href="http://supabase.com" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Supabase&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; because that's what I picked for Oggam, and I found them to be very powerful as someone who has not done much frontend dev for the last 15 years. Ultimately, though, after a couple of weeks I switched out to &lt;/span&gt;&lt;a href="http://astro.build" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Astro&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and SQLite.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I had started asking myself if the complexity of the Supabase self-hosted stack was something I actually needed for something as architecturally simple as a blog, and I felt Astro would give me more flexibility and control over the HTML that was being served. I'm also entertaining the idea that I can use Astro's static site building capabilities to regularly produce an entirely static failover/backup for the live version of the system. That would be pretty amazing.&lt;/span&gt;&lt;/p&gt;&lt;details class="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-2" open="true"&gt;&lt;summary class="cursor-pointer py-1 px-6 relative font-bold list-none outline-none text-zinc-900 dark:text-zinc-100 [&amp;::-webkit-details-marker]:hidden [&amp;::marker]:hidden before:content-[''] before:block before:absolute before:left-2 before:top-1/2 before:-translate-y-1/2 before:w-0 before:h-0 before:border-[6px] before:border-transparent before:border-l-black dark:before:border-l-white before:rotate-0 before:transition-transform [details[open]_&amp;]:before:rotate-90 [div[data-open]_&amp;]:before:rotate-90 hover:bg-zinc-100 dark:hover:bg-zinc-700/50"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;On Supabase&lt;/span&gt;&lt;/p&gt;&lt;/summary&gt;&lt;div class="px-5 pb-1 pt-0 text-zinc-900 dark:text-zinc-100" data-lexical-collapsible-content="true"&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I would pick Supabase again in a heartbeat if I were building another SaaS application. There are some things I would change in their stack around &lt;/span&gt;&lt;a href="https://supabase.com/edge-functions" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Edge Functions&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; (the hard CPU time limits are frustrating to work around), but I was very lucky to have picked this just based on a handful of Hacker News mentions and a YC podcast.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;If you embrace &lt;/span&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;RLS&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; and Edge Functions you can produce some sophisticated behaviours while keeping infrastructure costs very low.&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/details&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I cycled through several text editors (&lt;/span&gt;&lt;a href="http://tiptap.dev" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;TipTap&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="http://platejs.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;PlateJS&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, &lt;/span&gt;&lt;a href="http://blocknotejs.org" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;BlockNote&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;) for authoring, but found that many of them required payment for the functionality I wanted, or were too opinionated, focused on collaborative editing, or looked tricky to make modifications.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I've been drawn to &lt;/span&gt;&lt;a href="https://lexical.dev" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;Lexical&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; since I first found it, though since it's pitched as a framework for building editors I thought it would be overkill. I'd been steering clear of it in the name of keeping things easy, but in the end, a framework is actually what I needed.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;Definitely a bigger investment of effort than the other editors, but has enabled me to be as idiosyncratic as I wanted in my choices, including rewriting chunks of it to use Tailwind and shadcn.&lt;/span&gt;&lt;/p&gt;&lt;h3 id="deploying" class="group relative"&gt;&lt;a href="#deploying" class="absolute -left-6 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-gray-400 hover:text-gray-600 no-underline"&gt;#&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;Deploying&lt;/span&gt;&lt;/h3&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm not particularly misty-eyed over the days when hosting meant &lt;/span&gt;&lt;a href="https://web.archive.org/web/20110704151555/http://blog.boards.ie/2010/05/27/cleaning-up-a-few-years-of-incremental-infrastructure-growth/" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;buying a server and hauling it into a rack myself&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;, but there is something primordially &lt;/span&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;right&lt;/em&gt;&lt;/i&gt;&lt;span style="white-space: pre-wrap;"&gt; about being able to run everything on a single server, and copy files around.&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;At the moment the whole thing is deployed on a $6 Hetzner VPS via Github Actions, managed via Docker Compose and running behind nginx and Cloudflare. To me, this is a simple, powerful setup. With some tricks, it even allows me to get maybe-zero-noticeable-downtime deployments 😏&lt;/span&gt;&lt;/p&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;You can configure Docker compose to do rolling deploys (!):&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18"&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;healthcheck&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;test&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;[&lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"CMD"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"curl"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"-f"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;,&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"http://localhost:4321/health"&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;]&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;interval&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 5s&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;timeout&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 3s&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;retries&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;5&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;start_period&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 5s&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;deploy&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;replicas&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;1&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;update_config&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;order&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; start&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;first&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;failure_action&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; rollback&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;delay&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 0s&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;stop_signal&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenConstant" style="white-space: pre-wrap;"&gt;SIGTERM&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;stop_grace_period&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 60s&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;    &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;logging&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;      &lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;options&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        max&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;size&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"10m"&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        max&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;-&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;file&lt;/span&gt;&lt;span class="editor-tokenOperator" style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenString" style="white-space: pre-wrap;"&gt;"3"&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;However it did still result in 5-10 seconds or so of visible &lt;/span&gt;&lt;code spellcheck="false" style="white-space: pre-wrap;"&gt;&lt;span class="bg-gray-50 dark:bg-gray-800/30 font-mono px-2 py-0.5 rounded border border-gray-200/60 dark:border-gray-700/50 text-gray-800 dark:text-gray-200 relative before:absolute before:inset-0 before:bg-gradient-to-r before:from-blue-50/30 before:to-purple-50/30 dark:before:from-blue-900/20 dark:before:to-purple-900/20 before:-z-10"&gt;Origin Unreachable&lt;/span&gt;&lt;/code&gt;&lt;span style="white-space: pre-wrap;"&gt; errors from Cloudflare. Since this is a blog though, not a complex multi-user web app, I can cheat a little by getting nginx to serve "stale" cached content for the posts while the app backend is down:&lt;/span&gt;&lt;/p&gt;&lt;pre class="editor-code line-numbers" spellcheck="false" data-highlight-language="javascript" data-gutter="1
2
3
4
5
6
7
8"&gt;&lt;span style="white-space: pre-wrap;"&gt;        # Enable caching globally&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        proxy_cache cache&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        # Cache &lt;/span&gt;&lt;span class="editor-tokenFunction" style="white-space: pre-wrap;"&gt;validity&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;(&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt;adjust &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;as&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; needed&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;)&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        proxy_cache_valid &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;200&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;301&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; &lt;/span&gt;&lt;span class="editor-tokenNumber" style="white-space: pre-wrap;"&gt;302&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; 10m&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        # Serve stale content &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; upstream is down&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        # This does not serve stale &lt;/span&gt;&lt;span class="editor-tokenKeyword" style="white-space: pre-wrap;"&gt;if&lt;/span&gt;&lt;span style="white-space: pre-wrap;"&gt; upstream is healthy&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;.&lt;/span&gt;&lt;br&gt;&lt;span style="white-space: pre-wrap;"&gt;        proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504&lt;/span&gt;&lt;span class="editor-tokenPunctuation" style="white-space: pre-wrap;"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;There are still plenty of bugs, but the bugs are all of my own creation, and most of them affect only me, so it's kind of great?&lt;/span&gt;&lt;/p&gt;&lt;hr&gt;&lt;p dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;There is currently no comments system. If you'd like to share an opinion either with me or about this post, please feel free to do so with me either via email (&lt;/em&gt;&lt;/i&gt;&lt;a href="mailto:ross@duggan.ie" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;ross@duggan.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) on Mastodon (&lt;/em&gt;&lt;/i&gt;&lt;a href="http://mastodon.ie/@duggan" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;@duggan@mastodon.ie&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;) or even on &lt;/em&gt;&lt;/i&gt;&lt;a href="https://news.ycombinator.com" rel="noreferrer"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;Hacker News&lt;/em&gt;&lt;/i&gt;&lt;/a&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;.&lt;/em&gt;&lt;/i&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/ignoring-good-advice-and-building-my-own-blog-again" rel="alternate"/>
    <published>2025-01-12T10:53:09.222Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;It's kind of a classic developer mistake to build a blog before they start writing. &lt;/span&gt;&lt;a href="https://micro.webology.dev/2024/11/02/please-publish-and.html" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;As Jeff Triplett writes&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt;:&lt;/span&gt;&lt;/p&gt;&lt;blockquote dir="ltr"&gt;&lt;i&gt;&lt;em class="italic" style="white-space: pre-wrap;"&gt;"Write and publish before you write your own static site generator or perfect blogging platform. We have lost billions of good writers to this side quest because they spend all their time working on the platform instead of writing."&lt;/em&gt;&lt;/i&gt;&lt;/blockquote&gt;&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;This is good advice. I am just preternaturally incapable of following it.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
  <entry>
    <id>https://duggan.ie/posts/hello</id>
    <title type="text">Hello!</title>
    <updated>2024-12-15T11:35:55.000Z</updated>
    <author>
      <name>Ross Duggan</name>
      <email>ross@duggan.ie</email>
      <uri>https://duggan.ie/</uri>
    </author>
    <content type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm still putting things together here, but I've got an &lt;/span&gt;&lt;a href="https://duggan.ie/about" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;about page&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; that gives a brief summary of who I am.&lt;/span&gt;&lt;/p&gt;</content>
    <link href="https://duggan.ie/posts/hello" rel="alternate"/>
    <published>2024-12-15T11:35:55.009Z</published>
    <summary type="html">&lt;p dir="ltr"&gt;&lt;span style="white-space: pre-wrap;"&gt;I'm still putting things together here, but I've got an &lt;/span&gt;&lt;a href="https://duggan.ie/about" rel="noreferrer"&gt;&lt;span style="white-space: pre-wrap;"&gt;about page&lt;/span&gt;&lt;/a&gt;&lt;span style="white-space: pre-wrap;"&gt; that gives a brief summary of who I am.&lt;/span&gt;&lt;/p&gt;</summary>
  </entry>
</feed>