<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Matty Stratton</title>
    <description>The latest articles on DEV Community by Matty Stratton (@mattstratton).</description>
    <link>https://dev.to/mattstratton</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F38834%2F68a98062-d7f1-4797-8a79-a34d8d217a27.jpg</url>
      <title>DEV Community: Matty Stratton</title>
      <link>https://dev.to/mattstratton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mattstratton"/>
    <language>en</language>
    <item>
      <title>I Contain Multitudes (and Also Three Git Repos)</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Mon, 06 Jul 2026 01:17:28 +0000</pubDate>
      <link>https://dev.to/mattstratton/i-contain-multitudes-and-also-three-git-repos-33pf</link>
      <guid>https://dev.to/mattstratton/i-contain-multitudes-and-also-three-git-repos-33pf</guid>
      <description>&lt;p&gt;My blog is older than most of the tools I use to run it. The oldest posts started life on LiveJournal sometime in the early 2000s, got dragged through a WordPress import at some point in the mid-2000s, spent the better part of a decade as a Hugo site, and, about a week ago, moved into an Astro 5 site that reproduces every one of those old URLs byte for byte. That's 2,630 posts spanning 2001 to 2020, frozen exactly where they landed.&lt;/p&gt;

&lt;p&gt;At the same time, &lt;a href="https://speaking.mattstratton.com" rel="noopener noreferrer"&gt;my speaking site&lt;/a&gt; (106 talks going back to 2012) moved off Notist and onto something I actually own. And the tool that syncs my dev.to drafts with git finally works the way it was supposed to the whole time, instead of the way it actually did for a while.&lt;/p&gt;

&lt;p&gt;All three of those used to be separate repos. As of earlier today, they're one monorepo, with the real git history from all three preserved, not squashed into "initial commit" and called a day. Here's the tour: how the two sites work, how dev.to fits into either of them, and a couple of things that broke in interesting ways along the trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it today
&lt;/h2&gt;

&lt;p&gt;Three systems, one person, now one &lt;a href="https://github.com/mattstratton/mattstratton-web" rel="noopener noreferrer"&gt;repo&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mattstratton.com&lt;/strong&gt; is the personal site. It carries the 2,630-post legacy archive and a newer, general-purpose &lt;code&gt;/writing/&lt;/code&gt; section for anything I want to write that isn't archival.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;speaking.mattstratton.com&lt;/strong&gt; is the talk archive: every deck, every video, every event, self-hosted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mattstratton-dev-to&lt;/strong&gt; isn't a website at all. It's a small tool that keeps dev.to drafts and this git repo in sync, and it's the thing that lets a post published on dev.to also show up natively on &lt;a href="https://mattstratton.com" rel="noopener noreferrer"&gt;mattstratton.com&lt;/a&gt;, which is what's about to happen to this exact post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're merged into one monorepo now, but they're still three genuinely separate concerns underneath, with their own configs and their own reasons for existing. Let's take them one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  mattstratton.com: preserving twenty years of receipts
&lt;/h2&gt;

&lt;p&gt;The current site is Astro 5 and Tailwind v4. The single rule that shaped every other decision in the migration: &lt;strong&gt;URL preservation is the prime directive.&lt;/strong&gt; &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/astro.config.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt;&lt;/a&gt; sets &lt;code&gt;trailingSlash: 'always'&lt;/code&gt; and builds in directory format specifically to reproduce the old Hugo URLs exactly, which in turn had reproduced the WordPress URLs before that. Old inbound links, whether from a 2009 blog roll or a conference site linking a talk recap from 2014, still resolve. Nobody has to fix a dead link because I decided to rewrite my site in a different framework.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;posts&lt;/code&gt; collection is the frozen 2001 to 2020 archive, bulk-converted from Hugo. It doesn't grow. It just needs to keep existing. Everything new goes into &lt;code&gt;writing&lt;/code&gt;, and that one's got more going on than I expected when I started writing this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field guide: my own posts plus the good stuff I wrote elsewhere
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/writing/&lt;/code&gt; used to be scoped much more narrowly. The working plan before this project had it locked to Postgres-focused, authority-building content, an idea inherited from a content strategy document written for a completely different audience than the one reading my personal blog. The pushback that killed that scoping was simple: I contain multitudes, and a personal site that can only hold one topic isn't really personal anymore.&lt;/p&gt;

&lt;p&gt;What's actually interesting about &lt;code&gt;/writing/&lt;/code&gt; now, though, is that it isn't only my own posts. It's a merge of two genuinely different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native entries in &lt;code&gt;src/content/writing/&lt;/code&gt;, defined by the schema in &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/src/content.config.ts" rel="noopener noreferrer"&gt;&lt;code&gt;content.config.ts&lt;/code&gt;&lt;/a&gt;: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;pubDate&lt;/code&gt;, &lt;code&gt;topics&lt;/code&gt;, &lt;code&gt;draft&lt;/code&gt;, an optional &lt;code&gt;part&lt;/code&gt; for a curated four-part Postgres arc, and an optional &lt;code&gt;canonicalUrl&lt;/code&gt; for when the real original lives somewhere else entirely.&lt;/li&gt;
&lt;li&gt;A hand-maintained list of external links to posts I've written for my employer, Tiger Data, defined in &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/src/data/field-guide.ts" rel="noopener noreferrer"&gt;&lt;code&gt;field-guide.ts&lt;/code&gt;&lt;/a&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FieldGuideLink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mechanics&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;limits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;traps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 14 of those right now, all pointing at tigerdata.com, each slotted into one of the same four &lt;code&gt;part&lt;/code&gt; buckets native posts can opt into. &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/src/lib/writing.ts" rel="noopener noreferrer"&gt;&lt;code&gt;writing.ts&lt;/code&gt;&lt;/a&gt; normalizes both shapes into one common &lt;code&gt;GuideItem&lt;/code&gt; (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;href&lt;/code&gt;, &lt;code&gt;external&lt;/code&gt;) so the template can render them identically, native entries first within each part, external links after, no interleaving by date. The one visual tell is a small "tigerdata.com ↗" badge next to external titles. No new tab, just an honest label about where you're about to land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The newsletter: Buttondown does the sending, Astro just serves the pages
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/newsletter/&lt;/code&gt; works the same way as most of this site: fetch at build time, serve static files forever. &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/src/lib/buttondown.ts" rel="noopener noreferrer"&gt;&lt;code&gt;buttondown.ts&lt;/code&gt;&lt;/a&gt; is an Astro content-layer loader that calls one endpoint, &lt;code&gt;GET /emails?ordering=-publish_date&lt;/code&gt; against Buttondown's API, follows pagination until it runs out of pages, and hands the result to Astro as the &lt;code&gt;newsletter&lt;/code&gt; collection. Every issue page, the archive index, and the RSS feed are then just static output from a normal &lt;code&gt;getCollection()&lt;/code&gt; call, same as any local markdown file.&lt;/p&gt;

&lt;p&gt;Two details worth calling out for anyone tempted to do the same thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;BUTTONDOWN_API_KEY&lt;/code&gt; isn't set, the loader logs a warning and returns an empty collection instead of failing the build. That's not an accident: it means anyone can clone this repo and build it locally without needing my API key.&lt;/li&gt;
&lt;li&gt;Buttondown stores each issue's body as Markdown, so the loader runs it through &lt;code&gt;marked.parse()&lt;/code&gt; before it ever reaches a template. The site never touches raw HTML from the API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BUTTONDOWN_API_KEY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BUTTONDOWN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BUTTONDOWN_API_KEY not set: newsletter archive will build empty.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  speaking.mattstratton.com: getting my slides back from a CDN I don't own
&lt;/h2&gt;

&lt;p&gt;The old speaking site lived on Notist, and Notist had two problems that eventually became one problem: video embeds had already started quietly breaking, and every slide deck was served off Notist's CDN. The day that account lapses, the images and PDFs go with it. A core part of a speaker's professional identity, every talk I've ever given, was living in someone else's database with a thin export option.&lt;/p&gt;

&lt;p&gt;So the new site's whole design goal is asset ownership and durability. Plain content in my own repo, on my own domain, that keeps working with no third-party dependency that can be shut off or go quietly stale. Not a redesign. A portability project.&lt;/p&gt;

&lt;p&gt;The interesting piece is the slide pipeline, because it runs entirely &lt;strong&gt;locally, before a commit ever happens&lt;/strong&gt;, and never on the deploy host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;originals/{id}.pdf --optimize(gs, 300dpi)--&amp;gt; public/slides/{id}.pdf --rasterize--&amp;gt; public/slides/{id}/{n}.webp
   (gitignored, full-res)          (committed, served, downloadable)     (committed, served, viewer images)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason it has to run locally: static hosts don't give you root, and you need root (or at least a real package manager) to run &lt;code&gt;pdftoppm&lt;/code&gt; and &lt;code&gt;gs&lt;/code&gt;. Trying to shell out to Ghostscript from inside a Netlify build is a fight you will lose. Doing the rasterization on my own machine and committing the output keeps the actual deploy build a boring, fast, host-agnostic &lt;code&gt;astro build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Video embeds got the same "own it, don't proxy it" treatment. Instead of reproducing Notist's approach (a proxy that, again, had already started breaking), each talk stores a plain provider and video ID in frontmatter, and the page renders a lightweight embed facade that only loads the real player on click.&lt;/p&gt;

&lt;p&gt;The current numbers: 106 talks from 2012 to 2026, 93 events, 36 videos, 3,684 slide images. All of it committed, none of it one CDN outage away from disappearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the talks searchable
&lt;/h2&gt;

&lt;p&gt;Video is the one asset class the speaking site doesn't fully own. It can host the reference, but not the file itself. What it can own is the transcript, and that turned into a genuinely useful feature: full-text search across every talk I've ever given.&lt;/p&gt;

&lt;p&gt;Raw transcripts come from YouTube's auto-generated captions, pulled by &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-speaking/scripts/transcripts.ts" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/transcripts.ts&lt;/code&gt;&lt;/a&gt; via &lt;code&gt;yt-dlp&lt;/code&gt;, cleaned up, and written to &lt;code&gt;public/transcripts/{id}.txt&lt;/code&gt;. Auto-generated captions are rough: no punctuation, mangled proper nouns, every "um" and "uh" transcribed faithfully. A &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-speaking/.claude/skills/transcript-cleanup/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;transcript-cleanup&lt;/code&gt;&lt;/a&gt; Claude Code skill handles the cleanup pass, with a strict ruleset: fix punctuation, capitalization, and proper nouns, remove disfluencies, but never reword, paraphrase, or reorder anything actually said. When in doubt, the instruction is to leave the text alone rather than guess.&lt;/p&gt;

&lt;p&gt;Search itself runs client-side against two separate JSON payloads, built at deploy time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-speaking/src/pages/search-index.json.ts" rel="noopener noreferrer"&gt;&lt;code&gt;search-index.json.ts&lt;/code&gt;&lt;/a&gt; is small: title, abstract, event, tags, year, per talk. Loaded on first open of the search box.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-speaking/src/pages/deep-index.json.ts" rel="noopener noreferrer"&gt;&lt;code&gt;deep-index.json.ts&lt;/code&gt;&lt;/a&gt; is the heavy one, concatenating each talk's transcript and slide text into a single searchable blob, fetched lazily only once you actually type a query:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;readTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;readSlideText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;talkUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-speaking/src/components/Search.astro" rel="noopener noreferrer"&gt;&lt;code&gt;Search.astro&lt;/code&gt;&lt;/a&gt; builds a Fuse.js fuzzy index over each payload and merges the results, so searching for a phrase I actually said mid-talk, not just a word in the title, finds the right talk. No server, no database, just two static JSON files and a fuzzy-matching library doing all the work in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three repos become one
&lt;/h2&gt;

&lt;p&gt;The three-repos-in-one-monorepo move wasn't really about tidiness. Managing three repos independently was mild friction, sure, but the actual trigger was realizing that between a frozen archive and a newly-scoped &lt;code&gt;/writing/&lt;/code&gt; section, there was no home left on my own site for general writing. Once that was the real problem, "merge some repos" turned into "fix the architecture, and while we're in there, get dev.to crossposting actually working."&lt;/p&gt;

&lt;p&gt;The mechanism for the merge itself was &lt;code&gt;git subtree add&lt;/code&gt;, not a squash and not a filter-repo rewrite. That preserves full commit history from both source repos as real, verifiable ancestry, not just "the files are here now, trust me." Both source repos were young (about 50 and 22 commits respectively), so this wasn't decades of archaeology to protect. It was doing it right on principle, which is its own kind of satisfying.&lt;/p&gt;

&lt;p&gt;A few things bit us on the way that are worth flagging if you're ever tempted to merge repos that each bring their own CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions workflows are only discovered at the true repo root's &lt;code&gt;.github/workflows/&lt;/code&gt;, never in a subdirectory.&lt;/strong&gt; The dev.to workflows got left nested under the subproject's own &lt;code&gt;.github/workflows/&lt;/code&gt; after the initial merge, which made them completely invisible to GitHub. No error. No warning. &lt;code&gt;gh workflow list&lt;/code&gt; just didn't show them, and they silently never ran again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify doesn't scope rebuilds by subdirectory on its own.&lt;/strong&gt; Every push rebuilds every connected site unless you add an explicit &lt;code&gt;ignore&lt;/code&gt; check in each site's own &lt;code&gt;netlify.toml&lt;/code&gt;. And when Netlify skips a build for having no relevant changes, it shows that as "Canceled build due to no content change," which reads exactly like a failure in your inbox even though nothing is actually wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Base directory" and "Package directory" are different Netlify settings&lt;/strong&gt;, and the re-link flow for a repo with no npm workspace only shows you Base directory. Package directory just never appears. Worth reading the actual docs instead of assuming, which is a sentence I apparently still need to relearn every few years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;astro check&lt;/code&gt; doesn't respect subproject boundaries.&lt;/strong&gt; Run from the monorepo root, it scanned an unrelated subproject's content collections using the other subproject's generated types and produced over 200 false type errors. One explicit &lt;code&gt;exclude&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; took that down to 3, all pre-existing and unrelated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If two or more of those apply to whatever you're about to merge, budget yourself an extra afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  dev.to: ownership, not direction
&lt;/h2&gt;

&lt;p&gt;The dev.to sync tool is built around one idea I'd genuinely recommend to anyone syncing content between a platform and git: &lt;strong&gt;ownership, not direction.&lt;/strong&gt; The full mental model lives in &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/mattstratton-dev-to/CLAUDE.md" rel="noopener noreferrer"&gt;the repo's &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/a&gt;, but the short version:&lt;/p&gt;

&lt;p&gt;It's tempting to think of this as bidirectional sync, keep dev.to and git in lockstep, push changes both ways. Don't do that. It produces an endless phantom-diff loop where each side keeps thinking it's the one with the real version. Instead, a post is dev.to-owned while it's being drafted, and git ignores it entirely. Once a human merges an import PR, it's git-owned forever, marked by the presence of an &lt;code&gt;id&lt;/code&gt; field in its frontmatter. Ownership transfers exactly once, at a deliberate moment, and it never automatically flips back.&lt;/p&gt;

&lt;p&gt;Two small workflows do the actual work. &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/.github/workflows/devto-import.yml" rel="noopener noreferrer"&gt;&lt;code&gt;devto-import.yml&lt;/code&gt;&lt;/a&gt; runs on a cron, pulls any newly-published dev.to posts, and opens a PR for review, never touching a file that already has an &lt;code&gt;id&lt;/code&gt;. &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/.github/workflows/devto-publish.yml" rel="noopener noreferrer"&gt;&lt;code&gt;devto-publish.yml&lt;/code&gt;&lt;/a&gt; runs on push to files under &lt;code&gt;posts/&lt;/code&gt;, and pushes local edits back to dev.to, writing the &lt;code&gt;id&lt;/code&gt; back on a post's very first publish. Their domains never overlap, which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crosspost pipeline (and yes, this post is going through it)
&lt;/h2&gt;

&lt;p&gt;Once a post is git-owned, I can opt it into crossposting by adding &lt;code&gt;crosspost: true&lt;/code&gt; to its frontmatter, which republishes it as a native entry in mattstratton.com's &lt;code&gt;/writing/&lt;/code&gt; collection and rewrites the dev.to post's &lt;code&gt;canonical_url&lt;/code&gt; to point back at mattstratton.com, via &lt;a href="https://github.com/mattstratton/mattstratton-web/blob/master/scripts/crosspost-devto.ts" rel="noopener noreferrer"&gt;&lt;code&gt;crosspost-devto.ts&lt;/code&gt;&lt;/a&gt;. That flag only gets added after a post already has an &lt;code&gt;id&lt;/code&gt;, meaning it's already live on dev.to. It's a deliberate, later opt-in, never part of the initial draft template. This post is a good example of the "before":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Contain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Multitudes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Also&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Three&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Git&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Repos)"&lt;/span&gt;
&lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;astro&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;webdev&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;showdev&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;id&lt;/code&gt;, no &lt;code&gt;crosspost&lt;/code&gt;, no &lt;code&gt;canonical_url&lt;/code&gt;. Once this is published and I decide it belongs on mattstratton.com too, it'll pick up all three, the same way &lt;code&gt;decouple-release-from-deploy.md&lt;/code&gt; already has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4061454&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-03T15:43:16Z"&lt;/span&gt;
&lt;span class="na"&gt;crosspost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.mattstratton.com/writing/decouple-release-from-deploy/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design detail I actually like: this collapses into one atomic commit, not a two-phase publish-then-circle-back dance. mattstratton.com's URL for a crossposted piece is fully deterministic from the filename before any build even happens. dev.to's own article ID is server-assigned and unknowable in advance. So one commit creates the new &lt;code&gt;/writing/&lt;/code&gt; entry and rewrites the dev.to source's &lt;code&gt;canonical_url&lt;/code&gt; in the same diff, and one push correctly triggers both the Netlify deploy and dev.to's existing publish workflow, without anyone babysitting a second step.&lt;/p&gt;

&lt;p&gt;One trap worth naming because it's the kind of thing that looks obviously fine until it isn't: the &lt;code&gt;writing&lt;/code&gt; schema has had an optional &lt;code&gt;canonicalUrl&lt;/code&gt; field since the very first Astro migration commit, for a case that had never actually come up: a native &lt;code&gt;/writing/&lt;/code&gt; entry that's really a copy of something whose true original lives elsewhere, like a mirrored Tiger Data post. Before the crosspost script existed, the only place that field showed up at all was a commented-out line in the placeholder template file, &lt;code&gt;# canonicalUrl: ... # set when cross-posted&lt;/code&gt;. Nobody had ever actually set it on a real post. For a dev.to crosspost, though, mattstratton.com is &lt;em&gt;becoming&lt;/em&gt; the canonical source, the opposite of what that field was reserved for, so the crosspost script has to know to leave it unset on the entries it generates instead of reaching for an already-named field out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few weird-ass bugs, for flavor
&lt;/h2&gt;

&lt;p&gt;This post is meant to be "here's how it's set up," not "here's a bug report," but a couple of the failures along the way are too good not to mention briefly.&lt;/p&gt;

&lt;p&gt;The best one: the publish workflow started failing with a 422 on any post whose &lt;code&gt;canonical_url&lt;/code&gt; changed after the first publish, with the error body &lt;code&gt;{"error":"Tried to load unspecified class: Time","status":422}&lt;/code&gt;. Root cause was a full YAML round-trip silently dropping quotes from a timestamp that doesn't strictly need them under the JS YAML library's own rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before the fix (breaks dev.to's Ruby backend)&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-07-03T15:43:16Z&lt;/span&gt;
&lt;span class="c1"&gt;# after (quoted, parses fine on both sides)&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-07-03T15:43:16Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfectly legal YAML either way, by the JS library's rules. But dev.to's backend is Ruby, and Ruby's YAML parser auto-detects that now-unquoted, ISO-8601-shaped scalar as a native &lt;code&gt;Time&lt;/code&gt; object, which its safe loader then refuses to instantiate. Two implementations of "the same" file format, quietly disagreeing about what a bare string is allowed to become. Fixed by doing a targeted single-line replace on just the &lt;code&gt;canonical_url&lt;/code&gt; line instead of re-serializing the whole file.&lt;/p&gt;

&lt;p&gt;A few smaller ones, quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two files edited in a different tool picked up Windows line endings, and &lt;code&gt;crosspost: true\r&lt;/code&gt; parses as the string &lt;code&gt;"true\r"&lt;/code&gt;, not the boolean &lt;code&gt;true&lt;/code&gt;. Those two posts just silently never showed up as eligible.&lt;/li&gt;
&lt;li&gt;A 7-part series had dev.to's actual Series feature turned on for exactly 2 of the 7 posts, predating any of this tooling.&lt;/li&gt;
&lt;li&gt;Rewriting internal links across that same series hit a substring-replace bug where a typo'd slug happened to be a literal prefix of a different, correct slug. Fixed by sorting replacements longest-slug-first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fun stuff: memory and gains
&lt;/h2&gt;

&lt;p&gt;Two smaller pieces worth a callout because they're genuinely neat and don't fit anywhere else in this tour.&lt;/p&gt;

&lt;p&gt;The speaking site's Claude Code skills (adding a new talk, cleaning up a transcript, resyncing talk memory) don't need &lt;a href="https://memory.build" rel="noopener noreferrer"&gt;memory.build&lt;/a&gt; to work. They ran fine without it. But they now also include it, a permanent memory layer for AI agents, so that when I'm working with an agent on content, it can pull up what it already knows about a talk or a previous session instead of me re-explaining context every single time. Nice to have, not load-bearing.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;/fitness&lt;/code&gt; pulls my workout history straight from &lt;a href="https://www.liftosaur.com" rel="noopener noreferrer"&gt;Liftosaur&lt;/a&gt;'s API at build time. The reason it exists at all: Liftosaur's own public profile page just didn't show as much as I wanted, no real trend view, nothing I could link people to that told the whole story. Liftosaur doesn't hand back tidy JSON for a workout either; it returns a compact Liftoscript-text blob, so a small parser extracts the date, program, exercises, and sets from that text. Personal records and trend sparklines get computed from that parsed data at render time, not fetched as a separate call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still on my plate
&lt;/h2&gt;

&lt;p&gt;The crosspost trigger right now is a manual CLI command I have to remember to run. The fix I actually want: since the dev.to workflow is already entirely PR-centric (import arrives as a PR, &lt;code&gt;id&lt;/code&gt; gets written back as an auto-commit on that same push, never a second spawned PR), the crosspost step should follow the exact same shape. A GitHub Action on &lt;code&gt;pull_request&lt;/code&gt; that detects &lt;code&gt;crosspost: true&lt;/code&gt;, generates the &lt;code&gt;/writing/&lt;/code&gt; entry, and pushes it as an additional commit onto the same PR, instead of a manual script run or a redundant second PR. I filed &lt;a href="https://github.com/mattstratton/mattstratton-web/issues/53" rel="noopener noreferrer"&gt;an issue for it&lt;/a&gt; while writing this post, because I'd apparently been assuming for a while that one already existed and it hadn't.&lt;/p&gt;

&lt;p&gt;A couple of others I'm actually looking forward to: &lt;a href="https://github.com/mattstratton/mattstratton-web/issues/51" rel="noopener noreferrer"&gt;a map of everywhere I've spoken&lt;/a&gt; on the speaking site (the event data already has lat/lng, so this should be a fun afternoon, not a project), and &lt;a href="https://github.com/mattstratton/mattstratton-web/issues/41" rel="noopener noreferrer"&gt;real hero and thumbnail image design&lt;/a&gt; for &lt;code&gt;/writing/&lt;/code&gt; and the legacy archive, since the crosspost script currently papers over that gap by embedding the cover image inline in the post body instead of rendering it properly. There's also the usual pile of housekeeping (a &lt;code&gt;master&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; rename, a Node 24 upgrade) that's real but not exactly a fun read.&lt;/p&gt;

&lt;p&gt;None of it was urgent enough to hold up shipping the parts that already work. That's usually how the good list gets built anyway.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your Agent Didn't Break Prod. Your Pipeline Did.</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:43:16 +0000</pubDate>
      <link>https://dev.to/mattstratton/your-agent-didnt-break-prod-your-pipeline-did-4g9o</link>
      <guid>https://dev.to/mattstratton/your-agent-didnt-break-prod-your-pipeline-did-4g9o</guid>
      <description>&lt;p&gt;Picture a pipeline that looks pretty reasonable on paper. An agent opens pull requests. CI gates the merge to main: lint, tests, build, all green or it doesn't land. A scheduled job periodically promotes whatever's sitting in staging straight to production. No individual approval per feature. Just a batch cutover on a timer.&lt;/p&gt;

&lt;p&gt;That pipeline will absolutely, eventually, wreck your week.&lt;/p&gt;

&lt;p&gt;Not because the agent did anything malicious. Because two very different questions got quietly collapsed into one gate: "did this pass CI" and "is this safe for a real person to see right now." Those have never been the same question, not for humans and not for robots. We just used to have enough friction in the deploy process that the gap between them rarely mattered.&lt;/p&gt;

&lt;p&gt;I've &lt;a href="https://dev.to/mattstratton/how-my-coworker-who-didnt-know-cd-shipped-to-production-3j6j"&gt;written before&lt;/a&gt; about the scaffolding that lets a non-engineer safely drive a coding agent against a real codebase. Rules, skills, hooks. Three layers of paranoia built into the system instead of relying on any one person's vigilance on a random Friday. That post was about the merge gate: what stops garbage from landing on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Part of that merge gate is worth calling out on its own: a second pass on the same PR, same harness, different system prompt, catches things the first pass missed. You don't need a different vendor or a different model for that to work, just a fresh set of eyes that didn't write the code. &lt;a href="https://www.mattstratton.com/newsletter/was-it-the-model-or-just-a-fresh-pass-uncommitted/" rel="noopener noreferrer"&gt;I wrote more about why that works here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post is about the gate after that one. Passing CI and landing on &lt;code&gt;main&lt;/code&gt; solves exactly one problem, and it is not the problem of "will my users see something half-finished this afternoon."&lt;/p&gt;

&lt;p&gt;I've been talking about &lt;a href="https://www.youtube.com/watch?v=Qf1-CRT0pvY&amp;amp;t=178s" rel="noopener noreferrer"&gt;shifting things left in the pipeline&lt;/a&gt; for the better part of a decade, usually about security. Same instinct, one checkpoint further down the line: not just "is this code safe to merge," but "is deploying this code, right now, safe."&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous delivery is not continuous deployment
&lt;/h2&gt;

&lt;p&gt;This isn't a new idea. It's an old one that a lot of teams politely ignored for a decade, mostly because deploys used to be scary enough that nobody wanted to do more than one a sprint.&lt;/p&gt;

&lt;p&gt;Continuous delivery means every merge is &lt;em&gt;releasable&lt;/em&gt;. It does not mean every merge is &lt;em&gt;released&lt;/em&gt;. Continuous deployment collapses those two things into the same action, and that's a choice you make on purpose, not a default you inherit by moving fast.&lt;/p&gt;

&lt;p&gt;If your pipeline treats "merged" and "live" as synonyms, you've opted into continuous deployment without ever deciding to. The fix isn't slowing the pipeline down. It's separating the two decisions so a merge event and a "customers can see this" event stop being the same button.&lt;/p&gt;

&lt;p&gt;Feature flags are one popular way to do that separation, and if you already run one, great, keep running it. But a flag is a mechanism, not the point. I don't run a feature-flag system myself, and I still get the same separation. What matters is the decoupling, not the specific tool you use to get it.&lt;/p&gt;

&lt;p&gt;In case it's not obvious what one even looks like: a feature flag, at its most embarrassingly simple, is a boolean and an &lt;code&gt;if&lt;/code&gt; statement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newInvoiceExport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderNewExport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderOldExport&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need a vendor with a pricing page to get started. A config value, an environment variable, a column on a settings table, all of that counts as day one. Upgrade to something like LaunchDarkly or Unleash once the dumb version is the thing slowing you down, not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a gate on the release event, not just the merge event
&lt;/h2&gt;

&lt;p&gt;My own setup looks like this: PRs merge to &lt;code&gt;main&lt;/code&gt;, but &lt;code&gt;main&lt;/code&gt; isn't what's live. A separate release step promotes &lt;code&gt;main&lt;/code&gt; to a &lt;code&gt;production&lt;/code&gt; branch, and that promotion only happens after I explicitly say go. Nothing gets pushed to production on a timer, and nothing gets pushed because a batch of unrelated changes happened to be sitting around.&lt;/p&gt;

&lt;p&gt;Once that promotion kicks off, it isn't done just because Vercel says "build succeeded." The release process waits until Vercel confirms the new build is actually the one serving traffic, then runs an automated check against production itself: hit a handful of endpoints that have to work, confirm they do, and only then call the release finished. If that check fails, I find out. A user does not.&lt;/p&gt;

&lt;p&gt;There's a second, slower layer after that: a manual pass through the specific things that changed in this release, because an automated check can tell you the server responds, not that the feature behaves the way you meant it to.&lt;/p&gt;

&lt;p&gt;None of that requires a flag anywhere. The gate isn't "does this code know how to hide itself." The gate is "does a human, and then a machine, and then a human again, all get a chance to say no before this reaches a real user." Feature flags earn you a &lt;em&gt;fourth&lt;/em&gt; layer, an inside-the-code one, on top of that. Nice to have. Not the load-bearing part.&lt;/p&gt;

&lt;h2&gt;
  
  
  An automated check is monitoring with a head start
&lt;/h2&gt;

&lt;p&gt;I've spent a lot of years on the incident response and observability side of this industry, enough to have &lt;a href="https://speaking.mattstratton.com/talk/the-proactive-approach-data-driven-observability-incident-response" rel="noopener noreferrer"&gt;given entire talks on it&lt;/a&gt;. The lesson that refuses to go out of style: an automated check after a release doesn't exist to catch every possible bad deploy. It exists to catch the &lt;em&gt;slow&lt;/em&gt; discovery of a bad deploy.&lt;/p&gt;

&lt;p&gt;A user hitting your bug is monitoring too. It's just the worst possible kind, because you find out from a support ticket instead of a dashboard, and by the time the ticket lands, it's already been somebody's bad afternoon for a while.&lt;/p&gt;

&lt;p&gt;A post-deploy check that hits the handful of things that actually matter isn't a replacement for real observability: dashboards, alerts, tracing, whatever you've already built. It's a compressed version of the same instinct, aimed at the exact moment you're most likely to have broken something, which is right after you changed something.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fallback: revert fast, and don't paint yourself into a corner
&lt;/h2&gt;

&lt;p&gt;A release gate handles the "I'm not ready" case. It doesn't handle the "I was wrong" case, where the promotion went through, real users saw it, and it turns out there's a bug that only shows up under actual traffic. For that you need the other half: the ability to revert fast, which mostly comes down to one boring rule.&lt;/p&gt;

&lt;p&gt;Don't ship a database migration that the previous version of your code can't survive.&lt;/p&gt;

&lt;p&gt;This is the expand/contract pattern, and if you've been doing this long enough you've relearned it under a new name roughly every three years. Add the new column nullable. Backfill it. Write to both the old and new column. Read from the new one once you trust it. Only then, in a later release, drop the old column. Every step in that sequence is safe to roll back from, because the previous version of the app never depends on something that doesn't exist yet.&lt;/p&gt;

&lt;p&gt;Skip that discipline and your revert button becomes decorative. You can &lt;code&gt;git revert&lt;/code&gt; the code in about four seconds. If the migration already ran and already dropped a column the old code expects, reverting the code just gives you a fancier way to be down.&lt;/p&gt;

&lt;p&gt;This is also, not coincidentally, &lt;a href="https://speaking.mattstratton.com/talk/don-t-panic-effective-incident-response" rel="noopener noreferrer"&gt;incident response 101&lt;/a&gt;. Effective incident response runs on having a small number of pre-agreed moves you can execute without a meeting: revert, roll back, page someone, whatever applies. The worst possible moment to invent your rollback plan is during the incident it's supposed to fix. If reverting requires an engineer to reason live about which of three interdependent migrations is safe to undo, you don't have a rollback plan. You have a discussion topic, and discussion topics don't resolve outages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Someone still has to own saying "go"
&lt;/h2&gt;

&lt;p&gt;None of the automation above changes who's accountable for a release. It changes what that person is accountable &lt;em&gt;with&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I've talked before, on stages instead of on a blog, about how &lt;a href="https://speaking.mattstratton.com/talk/the-lifecycle-of-a-service" rel="noopener noreferrer"&gt;the lifecycle of a service&lt;/a&gt; doesn't end the moment something reaches production, and ownership doesn't evaporate the second it ships. Somebody owns a service in production the same way somebody owns the decision to promote a build to it. If your team can't answer "who decided to release this" for a given deploy, an agent that opens PRs quickly is not actually your biggest problem.&lt;/p&gt;

&lt;p&gt;In my setup, that person is me, for now. It might be someone else on my team next month, and the gate doesn't care which name is attached, only that a name is. The promotion step has an owner. It is never "whichever cron job happened to fire at 3am."&lt;/p&gt;

&lt;h2&gt;
  
  
  Same guardrails, new reason to need them
&lt;/h2&gt;

&lt;p&gt;None of this is agent-specific. Gated promotions, expand/contract migrations, decoupling deploy from release: this is stuff engineering orgs have known for well over a decade. It got treated as optional at a lot of small teams because a human was in the loop on every deploy, and a human noticing "wait, this doesn't feel done yet" was doing the job the gate should have been doing.&lt;/p&gt;

&lt;p&gt;Take that human out of the loop, or just make them faster and more numerous by handing them an agent, and the discipline you skipped stops being optional. It was never really optional. It was just being covered for by somebody's judgment call at 4:58pm on a Thursday.&lt;/p&gt;

&lt;p&gt;The agent didn't create this problem. It just removed the friction that used to hide it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same as last time: if you want the receipts on how the merge-side guardrails work, &lt;a href="https://dev.to/mattstratton/how-my-coworker-who-didnt-know-cd-shipped-to-production-3j6j"&gt;that's here&lt;/a&gt;. This one's the other half.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cicd</category>
    </item>
    <item>
      <title>What's Actually in My .zshrc (and Why)</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:39:44 +0000</pubDate>
      <link>https://dev.to/mattstratton/whats-actually-in-my-zshrc-and-why-53oe</link>
      <guid>https://dev.to/mattstratton/whats-actually-in-my-zshrc-and-why-53oe</guid>
      <description>&lt;p&gt;Your shell config is mostly a graveyard. A line you added in 2019 to fix a problem you don't remember having. An alias you're afraid to remove because what if you need it. Mine is no different, but there are a handful of pieces in there that are worth explaining, not because they're clever, but because most people have never thought about why they'd want them.&lt;/p&gt;

&lt;p&gt;This isn't a full dump of my config. If you want that, it's all on GitHub at &lt;a href="https://github.com/mattstratton/matty-dotfiles" rel="noopener noreferrer"&gt;matty-dotfiles&lt;/a&gt;. This post is about the config itself: the logic, the functions, the stuff that changes how the shell behaves depending on who, or what, is actually running it. (There's a quick note at the bottom on how the files are actually organized, if you're wondering why I keep saying "files" instead of "file.")&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually matters: AGENT_MODE
&lt;/h2&gt;

&lt;p&gt;Here's the thing that made me want to write this post in the first place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$npm_config_yes&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$-&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;i&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your shell used to have one audience: you. That's not true anymore. If you're running Claude Code, or any other coding agent that shells out to run commands on your behalf, your shell now has a second audience that doesn't want the same things you do. It doesn't want &lt;code&gt;thefuck&lt;/code&gt; to interactively suggest a correction. It doesn't want a confirmation prompt before &lt;code&gt;rm&lt;/code&gt; deletes something. It doesn't want your fancy multi-line prompt burning tokens or confusing a parser.&lt;/p&gt;

&lt;p&gt;So I detect it. If &lt;code&gt;npm_config_yes&lt;/code&gt; is set, or &lt;code&gt;CI&lt;/code&gt; is set, or the shell isn't interactive (&lt;code&gt;$-&lt;/code&gt; doesn't contain &lt;code&gt;i&lt;/code&gt;), I flip &lt;code&gt;AGENT_MODE&lt;/code&gt; on. Later in &lt;code&gt;.zshrc_shared&lt;/code&gt;, that flag does real work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENT_MODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;EDITOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'nvim'&lt;/span&gt;
  &lt;span class="nv"&gt;PROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%n@%m:%~%# '&lt;/span&gt;
  &lt;span class="nv"&gt;RPROMPT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;
  unsetopt CORRECT
  unsetopt CORRECT_ALL
  setopt NO_BEEP
  setopt NO_HIST_BEEP
  setopt NO_LIST_BEEP
  &lt;span class="nb"&gt;alias rm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rm -f'&lt;/span&gt;
  &lt;span class="nb"&gt;alias cp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cp -f'&lt;/span&gt;
  &lt;span class="nb"&gt;alias mv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mv -f'&lt;/span&gt;
  &lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;npm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'npm --no-fund --no-audit'&lt;/span&gt;
  &lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;yarn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'yarn --non-interactive'&lt;/span&gt;
  &lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;pip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'pip --quiet'&lt;/span&gt;
  &lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;git&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'git -c advice.detachedHead=false'&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No beeps, no interactive confirmations, a boring flat prompt instead of starship, and force flags on the destructive file commands so an agent doesn't get stuck at a prompt it can't answer. &lt;code&gt;EDITOR&lt;/code&gt; switches to plain &lt;code&gt;nvim&lt;/code&gt; instead of &lt;code&gt;code --wait&lt;/code&gt;, because a headless agent trying to wait on VS Code to close a file is not a fun afternoon.&lt;/p&gt;

&lt;p&gt;I'd bet a decent chunk of you reading this have never thought about the fact that your interactive shell defaults are actively hostile to a non-interactive process. Worth fixing before it bites you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets that don't live in plaintext
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/keychain-environment-variables.sh

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable GITHUB_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CHANGELOG_GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable GITHUB_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BOWIE_GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable GITHUB_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HOMEBREW_GITHUB_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable GITHUB_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_PERSONAL_ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable GITHUB_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BUTTONDOWN_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;keychain-environment-variable BUTTONDOWN_API_KEY&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;keychain-environment-variable&lt;/code&gt; is a function that pulls a secret out of macOS's Keychain at shell startup instead of having it sit around in plaintext in a dotfile that's synced to GitHub. I did not write this myself, I adapted it from &lt;a href="https://www.netmeister.org/blog/keychain-passwords.html" rel="noopener noreferrer"&gt;a blog post&lt;/a&gt;, and I only know that because the comment at the top of the file still says so. It just shells out to &lt;code&gt;security find-generic-password&lt;/code&gt; under the hood, nothing exotic, but it means the thing I &lt;code&gt;git push&lt;/code&gt; never has a credential in it.&lt;/p&gt;

&lt;p&gt;It's worth calling out that this function has to live somewhere. It's not in &lt;code&gt;.zshrc_shared&lt;/code&gt; itself, it's a separate file (&lt;code&gt;keychain-environment-variables.sh&lt;/code&gt;) that gets sourced, and yes, it's yadm-alternated too (&lt;code&gt;##os.Darwin&lt;/code&gt; and &lt;code&gt;##os.Linux&lt;/code&gt; versions, same symlink dance as the zshrc files). If you go copy the alias/export lines above without also grabbing that script, none of this works. Ask me how I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;gh&lt;/code&gt; alias that fixes a dumb problem
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_TOKEN= command gh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small, but it earns its place. The &lt;code&gt;gh&lt;/code&gt; CLI has its own auth flow, and it checks &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; first if it's set, which it is, everywhere, because half my other tools need it exported. Without this alias, &lt;code&gt;gh&lt;/code&gt; quietly uses that token instead of my actual &lt;code&gt;gh auth login&lt;/code&gt; session, and I get weird permission mismatches that take way too long to debug. This alias just unsets the variable for the one command that has opinions about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shortcut for working an issue with Claude Code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;issue&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; claude &lt;span class="nt"&gt;--permission-mode&lt;/span&gt; plan &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"issue-&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"/implement issue #&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;issue 412&lt;/code&gt; and it spins up Claude Code in plan mode, in its own worktree named &lt;code&gt;issue-412&lt;/code&gt;, already pointed at a slash command that implements GitHub issue #412. It's a one-liner, but it's the kind of one-liner that removes enough friction that I actually use it instead of doing the same five commands by hand every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  atuin and starship deserve more than a name-drop
&lt;/h2&gt;

&lt;p&gt;I mentioned in &lt;a href="https://dev.to/mattstratton/my-brewfile-1pob"&gt;My Brewfile&lt;/a&gt; that I install both of these, but I glossed over why, so let's fix that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;atuin init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://atuin.sh/" rel="noopener noreferrer"&gt;atuin&lt;/a&gt; replaces your plain old shell history file with a searchable SQLite database, and it hooks into your shell so every command gets logged with context: what directory you were in, how long it ran, whether it exited cleanly. The part that actually changed how I work is the search. Instead of mashing the up arrow forty times or grepping through &lt;code&gt;.zsh_history&lt;/code&gt; and hoping, I hit a keybinding and fuzzy-search across every command I've ever run, filtered by directory if I want. If you sync it, that history follows you across machines too. I didn't realize how much time I spent reconstructing commands from memory until I stopped having to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;starship init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://starship.rs/" rel="noopener noreferrer"&gt;starship&lt;/a&gt; is the prompt. That's it, that's the whole job, but it does that job well: git branch and status, language versions, command duration, all rendered fast and only when relevant, so it doesn't clutter the prompt with things that don't apply to the directory you're in. The reason it matters more than "cosmetic" makes it sound is that a good prompt answers questions before you have to ask them. Am I in a dirty git tree? Which Node version is active here? Starship tells me before I type anything.&lt;/p&gt;

&lt;p&gt;Also notice that starship shows back up in the &lt;code&gt;AGENT_MODE&lt;/code&gt; block from earlier, sort of. When agent mode is on, I explicitly override &lt;code&gt;PROMPT&lt;/code&gt; to something flat and boring instead of letting starship render. Fancy prompts are a UX win for a human; they're just noise (or worse, a parsing hazard) for a process reading your terminal output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aliases are underrated, actually
&lt;/h2&gt;

&lt;p&gt;People treat aliases like a beginner's trick, something you graduate out of once you "really" know the shell. That's backwards. An alias is just muscle memory you get to define yourself, for the commands you actually run, not the ones a tutorial thinks you should run. A few from my shared config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'clear'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;sz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'source ~/.zshrc'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;zshconfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"nvim ~/.zshrc"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gpom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git push origin main"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gitmain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git checkout main &amp;amp;&amp;amp; git pull origin main"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;cpenv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cp ~/src/github.com/timescale/tiger-den/.env ./'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;c&lt;/code&gt; has been in my shell configs for so many decades at this point that I genuinely forget, in the moment, that &lt;code&gt;c&lt;/code&gt; isn't actually a real command. It's &lt;code&gt;clear&lt;/code&gt;. It has always been &lt;code&gt;clear&lt;/code&gt;. I have simply stopped knowing that.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sz&lt;/code&gt; and &lt;code&gt;zshconfig&lt;/code&gt; exist because I edit and reload this file constantly, and typing &lt;code&gt;nvim ~/.zshrc##os.Darwin&lt;/code&gt; correctly every time is not a skill I've bothered to develop. &lt;code&gt;gpom&lt;/code&gt; and &lt;code&gt;gitmain&lt;/code&gt; are the ones I'd actually recommend you steal: they encode the exact git incantations you run dozens of times a day into something you can type without thinking. &lt;code&gt;cpenv&lt;/code&gt; is hyper-specific to one project of mine, and that's fine too. Aliases don't need to be generalizable to be worth having.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stuff I'll just mention
&lt;/h2&gt;

&lt;p&gt;A few plugins and settings that do work but don't need a full section: &lt;code&gt;thefuck&lt;/code&gt; for command correction, &lt;code&gt;autojump&lt;/code&gt; for directory jumping, oh-my-zsh plugins like &lt;code&gt;git-extras&lt;/code&gt; and &lt;code&gt;history-substring-search&lt;/code&gt;, and a couple of bindkeys so option+arrow jumps by word instead of doing nothing useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bindkey &lt;span class="s2"&gt;"^[[1;3C"&lt;/span&gt; forward-word
bindkey &lt;span class="s2"&gt;"^[[1;3D"&lt;/span&gt; backward-word
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All small. All the kind of thing you only notice once it's gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: why there are three files, not one
&lt;/h2&gt;

&lt;p&gt;I manage my dotfiles with &lt;a href="https://yadm.io/" rel="noopener noreferrer"&gt;yadm&lt;/a&gt;, which supports "alternate files" keyed on things like OS. So instead of one &lt;code&gt;.zshrc&lt;/code&gt;, I actually have &lt;code&gt;.zshrc##os.Darwin&lt;/code&gt; and &lt;code&gt;.zshrc##os.Linux&lt;/code&gt;, and yadm symlinks the right one to &lt;code&gt;.zshrc&lt;/code&gt; depending on the machine. Both of those source a third file, &lt;code&gt;.zshrc_shared&lt;/code&gt;, which holds everything that doesn't care what kernel it's running on. Same setup applies to the keychain script above.&lt;/p&gt;

&lt;p&gt;That's really it. It's a small mechanism, not a deep one, which is exactly why it's a bonus section and not its own post. If enough people want the full "how yadm alternates work" writeup, say so in the comments and I'll do it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go poke around
&lt;/h2&gt;

&lt;p&gt;The full files are in &lt;a href="https://github.com/mattstratton/matty-dotfiles" rel="noopener noreferrer"&gt;matty-dotfiles&lt;/a&gt; if you want the parts I skipped. If you've got a shell trick worth stealing, I want to hear about it in the comments.&lt;/p&gt;

</description>
      <category>shell</category>
      <category>zsh</category>
      <category>dotfiles</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Postgres Extensions Cheat Sheet: Replace 7 Databases With SQL</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Sat, 02 May 2026 20:47:24 +0000</pubDate>
      <link>https://dev.to/tigerdata/postgres-extensions-cheat-sheet-replace-7-databases-with-sql-ded</link>
      <guid>https://dev.to/tigerdata/postgres-extensions-cheat-sheet-replace-7-databases-with-sql-ded</guid>
      <description>&lt;p&gt;This post is a practical companion to &lt;a href="https://www.tigerdata.com/blog/its-2026-just-use-postgres" rel="noopener noreferrer"&gt;&lt;u&gt;It's 2026, Just Use Postgres&lt;/u&gt;&lt;/a&gt;. That post makes the architectural case for consolidating on Postgres. This one shows you how.&lt;/p&gt;

&lt;p&gt;Below are working SQL examples for each use case. Every extension listed here is available on &lt;a href="https://console.cloud.timescale.com" rel="noopener noreferrer"&gt;&lt;u&gt;Tiger Cloud&lt;/u&gt;&lt;/a&gt; with no additional setup. If you're self-hosting, each section links to the extension's repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll be able to do after reading this:&lt;/strong&gt; Set up Postgres extensions for full-text search, vector search, time-series, caching, message queues, document storage, geospatial queries, and scheduled jobs. Each section is self-contained, so you can skip to what you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Everything
&lt;/h2&gt;

&lt;p&gt;Here's the full set. You probably don't need all of them. Pick the ones that match your workload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_textsearch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- BM25 full-text search&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Vector search (pgvector)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;vectorscale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- DiskANN index for vectors&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- AI embeddings and RAG workflows&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Time-series&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Message queues&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_cron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Scheduled jobs&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;postgis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Geospatial&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Full-Text Search (Replace Elasticsearch)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;a href="https://github.com/timescale/pg_textsearch" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pg_textsearch&lt;/code&gt;&lt;/u&gt;&lt;/a&gt; (true BM25 ranking)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; Elasticsearch (separate JVM cluster, complex mappings, sync pipelines), Solr, or Algolia ($1 per 1,000 searches).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; The same BM25 algorithm that powers Elasticsearch, running natively in Postgres. No separate cluster. No sync jobs. No data drift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create a BM25 index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_articles_bm25&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Search with BM25 scoring&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database optimization'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database optimization'&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deep dive:&lt;/strong&gt; &lt;a href="https://www.tigerdata.com/blog/you-dont-need-elasticsearch-bm25-is-now-in-postgres" rel="noopener noreferrer"&gt;&lt;u&gt;You Don't Need Elasticsearch: BM25 is Now in Postgres&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Vector Search (Replace Pinecone)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extensions:&lt;/strong&gt; &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pgvector&lt;/code&gt;&lt;/u&gt;&lt;/a&gt; + &lt;a href="https://github.com/timescale/pgvectorscale" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pgvectorscale&lt;/code&gt;&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; Pinecone ($70/month minimum, separate infrastructure, data sync), Qdrant, Milvus, or Weaviate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; pgvectorscale uses the DiskANN algorithm (from Microsoft Research). On a &lt;a href="https://www.tigerdata.com/blog/pgvector-vs-pinecone" rel="noopener noreferrer"&gt;&lt;u&gt;50M vector benchmark&lt;/u&gt;&lt;/a&gt;, it achieved 28x lower p95 latency and 16x higher throughput than Pinecone at 99% recall.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;vectorscale&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- High-performance DiskANN index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_docs_embedding&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;diskann&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Find similar documents&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[0.1, 0.2, ...]'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[0.1, 0.2, ...]'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auto-sync embeddings with pgai
&lt;/h3&gt;

&lt;p&gt;No more manual embedding pipelines. pgai regenerates embeddings automatically on every INSERT and UPDATE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_vectorizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'documents'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loading_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'text-embedding-3-small'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'1536'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row stays in sync. No batch jobs. No drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid Search: BM25 + Vectors in One Query
&lt;/h2&gt;

&lt;p&gt;This is where Postgres consolidation pays off immediately. Combining keyword search and semantic search in other stacks requires two API calls, result merging, failure handling, and double the latency. In Postgres, it's one query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple weighted hybrid
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database optimization'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bm25_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vector_distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'database optimization'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hybrid_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hybrid_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reciprocal Rank Fusion (for RAG applications)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query. One transaction. One result set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-Series (Replace InfluxDB)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;a href="https://github.com/timescale/timescaledb" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB&lt;/u&gt;&lt;/a&gt; (21K+ GitHub stars)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; InfluxDB (separate database, Flux or limited SQL), Prometheus (metrics only, not application data).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Automatic time-based partitioning, compression up to 95%, continuous aggregates for fast dashboards, and full SQL. Your time-series data lives alongside your relational data with &lt;code&gt;JOIN&lt;/code&gt;s and &lt;a href="https://www.tigerdata.com/learn/understanding-acid-compliance" rel="noopener noreferrer"&gt;&lt;u&gt;ACID guarantees&lt;/u&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;temperature&lt;/span&gt; &lt;span class="nb"&gt;DOUBLE&lt;/span&gt; &lt;span class="nb"&gt;PRECISION&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Convert to a hypertable (automatic time partitioning)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;create_hypertable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'metrics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query with time buckets&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;time_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lifecycle automation
&lt;/h3&gt;

&lt;p&gt;TimescaleDB handles retention and compression policies so you don't have to build cron jobs for data management.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Automatically drop data older than 30 days&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_retention_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'metrics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Compress data older than 7 days (up to 95% storage reduction)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timescaledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;add_compression_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'metrics'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Case study:&lt;/strong&gt; &lt;a href="https://www.tigerdata.com/blog/from-4-databases-to-1-how-plexigrid-replaced-influxdb-got-350x-faster-queries-tiger-data" rel="noopener noreferrer"&gt;&lt;u&gt;Plexigrid went from 4 databases to 1&lt;/u&gt;&lt;/a&gt; and got 350x faster queries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching (Replace Redis)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Feature:&lt;/strong&gt; &lt;code&gt;UNLOGGED&lt;/code&gt; tables + &lt;code&gt;JSONB&lt;/code&gt; (built into Postgres, no extension needed)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; Redis for simple key-value caching scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; In-memory-speed storage without WAL overhead. Good for session data, temporary lookups, and simple caches. No separate service to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to keep Redis:&lt;/strong&gt; If you need pub/sub, sorted sets, Lua scripting, or complex data structures, Redis is still the better tool for those specific jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- UNLOGGED = no WAL overhead, faster writes&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;UNLOGGED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Set with expiration&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'user:123'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "Alice"}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Get&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user:123'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- Schedule cleanup with pg_cron&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cache_cleanup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0 * * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Message Queues (Replace Kafka)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;a href="https://github.com/tembo-io/pgmq" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pgmq&lt;/code&gt;&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; Kafka or RabbitMQ for task queues and simple event processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; A lightweight message queue inside Postgres. Send, receive with visibility timeouts, and delete after processing. Transactional with the rest of your data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to keep Kafka:&lt;/strong&gt; If you need high-throughput event streaming across dozens of services, consumer groups, exactly-once semantics, or multi-datacenter replication, Kafka is purpose-built for that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_queue'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Send a message&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_queue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{"event": "signup", "user_id": 123}'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Receive (with 30-second visibility timeout)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_queue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Delete after processing&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pgmq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_queue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Alternative: SKIP LOCKED pattern (no extension needed)
&lt;/h3&gt;

&lt;p&gt;For simple job queues, Postgres has a built-in pattern using &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Worker claims a job atomically&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Documents (Replace MongoDB)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Feature:&lt;/strong&gt; Native &lt;code&gt;JSONB&lt;/code&gt; (built into Postgres since 2014)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; MongoDB for document storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Schemaless document storage with GIN indexing, plus everything Postgres gives you: ACID transactions, relational &lt;code&gt;JOIN&lt;/code&gt;s, and SQL. No separate database for your "document-shaped" data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert a nested document&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{
  "name": "Alice",
  "profile": {"bio": "Developer", "links": ["github.com/alice"]}
}'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Query nested fields&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'bio'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'profile'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'bio'&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Developer%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Index specific JSON fields for fast lookups&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_email&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Geospatial (Replace Specialized GIS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;a href="https://postgis.net/" rel="noopener noreferrer"&gt;&lt;u&gt;PostGIS&lt;/u&gt;&lt;/a&gt; (the industry standard since 2001)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; Nothing, really. PostGIS is what most specialized GIS tools are built on. It powers OpenStreetMap and has been in production for 24 years.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;postgis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;stores&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;GEOGRAPHY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;POINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Find stores within 5km&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ST_Distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ST_MakePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;122&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;stores&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_DWithin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ST_MakePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;122&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scheduled Jobs (Replace External Cron)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;a href="https://github.com/citusdata/pg_cron" rel="noopener noreferrer"&gt;&lt;u&gt;&lt;code&gt;pg_cron&lt;/code&gt;&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you're replacing:&lt;/strong&gt; External &lt;code&gt;cron&lt;/code&gt; jobs, Kubernetes CronJobs, or Lambda scheduled triggers for database maintenance tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you get:&lt;/strong&gt; Cron scheduling inside Postgres. Useful for cache cleanup, materialized view refreshes, data retention, and periodic aggregation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_cron&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Run cache cleanup every hour&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cleanup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0 * * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;cache&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Refresh a materialized view every night at 2 AM&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rollup'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0 2 * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;daily_stats&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fuzzy Search (Typo Tolerance)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Extension:&lt;/strong&gt; &lt;code&gt;pg_trgm&lt;/code&gt; (built into Postgres)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_trgm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_name_trgm&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;gin_trgm_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Finds "PostgreSQL" even when typed as "posgresql"&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="s1"&gt;'posgresql'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'posgresql'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;If you want the architectural argument for why consolidating on Postgres matters (especially in the AI era), read &lt;a&gt;&lt;u&gt;It's 2026, Just Use Postgres&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All of these extensions come pre-configured on &lt;a href="https://console.cloud.timescale.com" rel="noopener noreferrer"&gt;&lt;u&gt;Tiger Cloud&lt;/u&gt;&lt;/a&gt;. Create a free database and start building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.tigerdata.com/docs/use-timescale/latest/extensions/pg-textsearch" rel="noopener noreferrer"&gt;&lt;u&gt;pg_textsearch documentation&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/timescale/pgvectorscale" rel="noopener noreferrer"&gt;&lt;u&gt;pgvectorscale on GitHub&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tigerdata.com/docs/" rel="noopener noreferrer"&gt;&lt;u&gt;TimescaleDB documentation&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tembo-io/pgmq" rel="noopener noreferrer"&gt;&lt;u&gt;pgmq on GitHub&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://postgis.net/" rel="noopener noreferrer"&gt;&lt;u&gt;PostGIS&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tigerdata.com/blog/from-4-databases-to-1-how-plexigrid-replaced-influxdb-got-350x-faster-queries-tiger-data" rel="noopener noreferrer"&gt;&lt;u&gt;How Plexigrid replaced InfluxDB and got 350x faster queries&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>postgresqlextensions</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Why I'm Learning ROS 2 as a Database Person</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Fri, 01 May 2026 21:46:47 +0000</pubDate>
      <link>https://dev.to/mattstratton/why-im-learning-ros-2-as-a-database-person-3cce</link>
      <guid>https://dev.to/mattstratton/why-im-learning-ros-2-as-a-database-person-3cce</guid>
      <description>&lt;p&gt;There's a moment that happens in every robotics company that makes it to production. The pilot worked. The robot does the thing. Now there are twenty on the floor, then fifty, then a fleet, and somebody asks: "What did the sensors look like on unit 17 during those three anomalies last quarter?"&lt;/p&gt;

&lt;p&gt;That's when the data infrastructure question gets real. As robotics moves from lab to warehouse floor to factory line, that question is going to come up a lot more often. The default answers aren't really built for it.&lt;/p&gt;

&lt;p&gt;I'm not a robotics engineer. Never built a robot. Never written a &lt;a href="https://docs.ros.org/en/jazzy/" rel="noopener noreferrer"&gt;ROS 2&lt;/a&gt; node or had an argument about &lt;a href="https://mcap.dev" rel="noopener noreferrer"&gt;MCAP&lt;/a&gt; vs. SQLite3 (though I'm about to). What I am is someone who's spent 20+ years watching what happens when storage decisions get made before the questions are fully understood. Log files are the classic version: great for appending, bad for answering "show me the error pattern across 200 servers from three weeks ago." The data exists. It's just not queryable. That's the shape of regret I'm talking about.&lt;/p&gt;

&lt;p&gt;I work at &lt;a href="https://tigerdata.com" rel="noopener noreferrer"&gt;Tiger Data&lt;/a&gt; doing developer relations for &lt;a href="https://github.com/timescale/timescaledb" rel="noopener noreferrer"&gt;TimescaleDB&lt;/a&gt;. Time-series data is literally my job. When I started looking at how ROS 2 handles recorded telemetry at scale, I saw something familiar: high-frequency sequential writes, stable append rates, a recording format optimized for replay rather than analysis, no real query story for "compare behavior across 200 runs without loading everything into memory."&lt;/p&gt;

&lt;p&gt;Oof.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rosbag2&lt;/code&gt; is good at what it does. Record, replay, done. That's the right scope for it. If you've been running ROS 2 long enough to think about what happens &lt;em&gt;after&lt;/em&gt; the bag file, you've probably felt the edge of that scope. The problem is that "done recording" is the beginning of a different problem when you're trying to understand what's happening across a fleet, over time, at scale. There's &lt;a href="https://github.com/ros2/rosbag2/issues/1739" rel="noopener noreferrer"&gt;an open feature request in the rosbag2 repo&lt;/a&gt; that names TimescaleDB and InfluxDB by name as candidate options for exactly this. It's been open since July 2024. Zero comments.&lt;/p&gt;

&lt;p&gt;The community noticed the gap. Nobody filled it.&lt;/p&gt;

&lt;p&gt;So that's what I'm going to try to figure out, in public. I'm going to build a ROS 2 node that writes telemetry directly to TimescaleDB and actually query it. All of it in a &lt;a href="https://github.com/mattstratton/ros2-timescaledb-bridge" rel="noopener noreferrer"&gt;public GitHub repo&lt;/a&gt;, commit by commit, so the decisions are visible as they get made. Document everything that breaks. The database side I know. The ROS 2 side I'm learning from scratch, and I'll be honest about that difference in real time. And to be clear: I don't actually know what any of this looks like at fleet scale. That's not false modesty. That's the whole point.&lt;/p&gt;

&lt;p&gt;What's coming next: getting the local environment set up (&lt;a href="https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/" rel="noopener noreferrer"&gt;TurtleBot3&lt;/a&gt; is basically the hello world of ROS 2 simulation: it's what the official tutorials use, it publishes the standard topics I care about, and &lt;a href="https://gazebosim.org" rel="noopener noreferrer"&gt;Gazebo&lt;/a&gt; runs the whole thing without me needing actual hardware*) and a post on what &lt;code&gt;rosbag2&lt;/code&gt; actually stores and why the format question matters once you're past replay. After that, the actual build: schema decisions, type mapping headaches, and a Grafana dashboard over live robot telemetry. The full tutorial comes last, once I've learned enough to actually teach it.&lt;/p&gt;

&lt;p&gt;If you've ever wanted to weigh in on that &lt;a href="https://github.com/ros2/rosbag2/issues/1739" rel="noopener noreferrer"&gt;rosbag2 feature request&lt;/a&gt;, now's a good time. And if you're working on this problem (fleet telemetry pipelines, robot data at scale, the gap between recording and understanding), I want to hear what you're running into.&lt;/p&gt;




&lt;p&gt;* - that said, I am not opposed to asking my boss to buy me a robot&lt;/p&gt;

</description>
      <category>ros2</category>
    </item>
    <item>
      <title>How My Coworker Who Didn't Know 'cd' Shipped to Production</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:30:39 +0000</pubDate>
      <link>https://dev.to/mattstratton/how-my-coworker-who-didnt-know-cd-shipped-to-production-3j6j</link>
      <guid>https://dev.to/mattstratton/how-my-coworker-who-didnt-know-cd-shipped-to-production-3j6j</guid>
      <description>&lt;p&gt;This morning, our Design Lead asked me how to get her terminal into the right folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tanya [6:57 AM]
my ghost isnt in tiger den anymore! GAH do i do /tiger-den
or what was the command to make it work 
in tiger den

Matty [7:14 AM]
What what?

Tanya [7:14 AM]
sorry lol
you know when i go into terminal/or ghostie
and i need to type the thing to make it so im
working in tiger den

Matty [7:14 AM]
Yes type cd tiger-den

Tanya [7:15 AM]
YES
OK CD
thank you
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.linkedin.com/in/tanya-higgins/" rel="noopener noreferrer"&gt;Tanya&lt;/a&gt; has been here at &lt;a href="https://tigerdata.com" rel="noopener noreferrer"&gt;Tiger Data&lt;/a&gt; for about two weeks. In that time, she shipped a feature to our production Next.js app.&lt;/p&gt;

&lt;p&gt;I'm not going to pretend that's a normal sentence to write.&lt;/p&gt;

&lt;p&gt;Tanya is our Design Lead. She runs brand. She's the reason our designs look the way they do and our blog thumbnails don't look like a high school yearbook. She is not a software engineer. She had not, as far as I know, previously planned on becoming one.&lt;/p&gt;

&lt;p&gt;She didn't become one. She just started shipping.&lt;/p&gt;

&lt;p&gt;The feature she shipped is an internal brand hub: a searchable index of every Tiger Data brand asset (logos, typography, colors, whatever you need) plus an interactive image builder that works like Canva (if Canva had been written specifically for our team). You can upload your own images, pick from our logo library, layer in backgrounds and icons, and drop text on top. Drag everything around, resize it, line it up. Export a PNG. (The thumbnail on this post was made with it.)&lt;/p&gt;

&lt;p&gt;The downside of teaching a designer to use the terminal is that she will want hers to look like yours. Tanya saw my &lt;a href="https://ghostty.org/" rel="noopener noreferrer"&gt;Ghostty&lt;/a&gt; theme and my &lt;a href="https://nix.catppuccin.com/options/main/home/catppuccin.starship/" rel="noopener noreferrer"&gt;catppuccin Starship&lt;/a&gt; theme over a screen share and decided she wanted both. Her Claude Code statusline came next. That's an entire other post.&lt;/p&gt;

&lt;p&gt;Hold both of these facts in your brain at the same time: Tanya is asking me what &lt;code&gt;cd&lt;/code&gt; does this morning. Tanya shipped a real feature to a real codebase last week.&lt;/p&gt;

&lt;p&gt;If you read &lt;a href="https://dev.to/mattstratton/coding-agents-are-actually-good-at-this-one-thing-5dej"&gt;my last post on coding agents and internal tooling&lt;/a&gt;, you know where I ended it. The caveat was that I was a solo contributor to a real codebase, and the first comment on the post called out exactly the right thing. &lt;em&gt;Who owns this when you leave?&lt;/em&gt; That's a reasonable question. In March, my answer was "me, and that's a problem."&lt;/p&gt;

&lt;p&gt;My answer in April is "...and also Tanya."&lt;/p&gt;

&lt;p&gt;That's only true because of what got built alongside the agent. The agent is the interesting part of the demo. The &lt;em&gt;boring&lt;/em&gt; part is what made it safe to hand a Next.js codebase to a designer who's still learning &lt;code&gt;cd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's that boring part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent is not the hard part
&lt;/h2&gt;

&lt;p&gt;Coding agents are very good at a lot of things. Being trustworthy is...not one of them.&lt;/p&gt;

&lt;p&gt;I don't mean malicious. I mean if you ask an agent to fix a bug, it will often fix the bug. It will also sometimes decide the failing test is "probably flaky" and move on. Or notice a test it doesn't understand and quietly skip it. Or hit an error, try a second approach, fail, try a third approach, fail again, and by the time you look up it has deleted the function you were trying to fix. Or my favorite one: "These test failures are unrelated to my changes".&lt;/p&gt;

&lt;p&gt;This is catchable. I catch a lot of it. I've been doing ops since before the Seinfeld finale aired, and I have been running DevOpsDays since before most of the current DevOps job descriptions existed. I read every diff before it leaves my machine and I have strong opinions about what a normal-looking CI run is supposed to contain.&lt;/p&gt;

&lt;p&gt;I still miss things, everyone does. I rubber-stamp, I push tired, I get three levels deep on a task and stop reading carefully. Even with the paranoia cranked up, I am not reliably the last line of defense for my own work, let alone anyone else's.&lt;/p&gt;

&lt;p&gt;And the paranoia itself is not transferable. "Notice when an agent silently dropped a test case" is not a skill you pick up in two weeks. It is not a skill most of the &lt;em&gt;engineers&lt;/em&gt; I know pick up in two weeks, because most of them never had to. I cannot hand it to Tanya by writing a memo.&lt;/p&gt;

&lt;p&gt;So the question is not &lt;em&gt;how do I make Tanya as paranoid as I am&lt;/em&gt;. The question is: how do I make the system so paranoid that it doesn't matter how paranoid any of us are on a given Friday afternoon?&lt;/p&gt;

&lt;p&gt;You build the paranoia into the system. (If this &lt;a href="https://dev.to/mattstratton/shifting-left-securely-with-inspec-6fk"&gt;sounds familiar&lt;/a&gt;, it should.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules are a suggestion
&lt;/h2&gt;

&lt;p&gt;The first place everyone reaches is &lt;code&gt;CLAUDE.md&lt;/code&gt;. Some people call theirs &lt;code&gt;AGENTS.md&lt;/code&gt; if they want the file to be portable across agents that aren't Claude. Same idea. You write down the rules you want the agent to follow, drop it at the root of the repo, and the agent reads it every session.&lt;/p&gt;

&lt;p&gt;Ours has a section called "Golden Rules". A handful of the actual rules from the actual file:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Diagnose before fixing. Read errors, trace code, explain your hypothesis before writing any fix. No guess-and-deploy.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Never dismiss failures. Test failures, CI failures, lint warnings. Investigate every one. Never call them "pre-existing" or "not my problem."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Never destructive without permission. No DROP, TRUNCATE, DELETE without WHERE.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Always use &lt;code&gt;/pr&lt;/code&gt; to push. Never raw &lt;code&gt;git push&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These are not theoretical. Each one is on the list because the agent did the thing I didn't want it to do, often enough that I eventually wrote it down. (Sometimes from running &lt;a href="https://pasqualepillitteri.it/en/news/408/claude-code-insights-command-workflow" rel="noopener noreferrer"&gt;&lt;code&gt;/insights&lt;/code&gt;&lt;/a&gt; in Claude Code.)&lt;/p&gt;

&lt;p&gt;Does the agent follow them? Mostly. Does the agent ignore them when convenient? Also yes.&lt;/p&gt;

&lt;p&gt;The failure mode is an exchange you have probably already had:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Me: "Wait, you just pushed to main."&lt;/p&gt;

&lt;p&gt;Claude: "Oh shoot, you're right. The &lt;code&gt;CLAUDE.md&lt;/code&gt; says to use &lt;code&gt;/pr&lt;/code&gt;. I just... didn't."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It apologizes. It means it. It will still do it again on Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is training; it shapes the default behavior. It does &lt;em&gt;not&lt;/em&gt; enforce anything. Treat it like training and it is useful. Treat it like a wall and something is going to walk through the wall.&lt;/p&gt;

&lt;p&gt;That was the first thing I had to learn. It might have take a few times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills are a habit
&lt;/h2&gt;

&lt;p&gt;If rules are what you write down when you want an agent to &lt;em&gt;know&lt;/em&gt; something, skills are what you write down when you want the agent (or the human) to &lt;em&gt;do&lt;/em&gt; something the same way every time.&lt;/p&gt;

&lt;p&gt;A Claude Code skill is roughly: a folder with instructions, some scripts, and a slash command. Type &lt;code&gt;/setup&lt;/code&gt; and it walks through our dev environment setup. Type &lt;code&gt;/debug&lt;/code&gt; and it starts a structured investigation instead of letting the agent flail. Type &lt;code&gt;/release&lt;/code&gt; and it runs the release process in the right order.&lt;/p&gt;

&lt;p&gt;The one that matters most is &lt;code&gt;/pr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our repo has a preflight checklist you have to pass to open a pull request. It runs lint. It runs the typechecker. It runs the tests. It runs the build. It runs a code review subagent. If any changed files touch rendered output, it runs a UI verification step. If any touched docs, it runs a docs-drift check. If any touched security-sensitive paths, it runs a security review. Only then does it actually open the PR, and even then it opens it as a draft.&lt;/p&gt;

&lt;p&gt;All of that is one command. Tanya does not have to remember any of it. She types &lt;code&gt;/pr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is still not enforcement. Skills are suggestions. A motivated human (or a tired agent, or a confused agent, or an agent being nudged by a tired human) can skip &lt;code&gt;/pr&lt;/code&gt; and push directly, and nothing in the skill itself will stop them (note: the skill does try to tell the agent when it should try to run it, and it usually gets it right...but not always).&lt;/p&gt;

&lt;p&gt;The job of a skill is to make the right way the easy way. &lt;code&gt;/pr&lt;/code&gt; is one command. Running all of those checks by hand is eight. If typing &lt;code&gt;/pr&lt;/code&gt; is easier than doing the alternative, the alternative stops happening. That is most of the game.&lt;/p&gt;

&lt;p&gt;Tanya used &lt;code&gt;/pr&lt;/code&gt; for the brand hub. She did not run lint, typecheck, tests, and build in sequence. She would not have known to! She typed a slash and a word. Everything ran.&lt;/p&gt;

&lt;p&gt;The rest is between her and the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks are a wall
&lt;/h2&gt;

&lt;p&gt;The rule "never push to main" lives in &lt;code&gt;CLAUDE.md&lt;/code&gt;. The skill &lt;code&gt;/pr&lt;/code&gt; is easier to type than the alternative. Neither of those is enforcement.&lt;/p&gt;

&lt;p&gt;The enforcement is a pre-push &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" rel="noopener noreferrer"&gt;git hook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you (or a coding agent) run &lt;code&gt;git push&lt;/code&gt; from a machine with our repo checked out, the hook fires before the push leaves your laptop. If the branch you are on is &lt;code&gt;main&lt;/code&gt; or &lt;code&gt;production&lt;/code&gt;, the hook exits 1 and prints the error message I wrote specifically for the version of me that is about to do a dumb thing. The push does not happen.&lt;/p&gt;

&lt;p&gt;If the branch is anything else, the hook runs lint, typecheck, tests, and build locally. If any of them fail, the push does not happen.&lt;/p&gt;

&lt;p&gt;There is a flag to skip hooks, yes. If the agent tries to use it, a separate &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;&lt;code&gt;PreToolUse&lt;/code&gt; hook&lt;/a&gt; catches the command before it runs and blocks it on protected branches. And if a push somehow lands on the remote anyway, GitHub branch protection rejects it on the server side.&lt;/p&gt;

&lt;p&gt;I did not build all three layers on day one. I built them in the order I needed them, which is to say I built each one the first time something got past the layer before it.&lt;/p&gt;

&lt;p&gt;There is also a &lt;code&gt;SessionStart&lt;/code&gt; hook that runs before the agent does anything. It checks that the Node version is the one the app expects, that the environment file exists, that dependencies are up to date, that the database the session is about to touch is the development database and not production. If any of that is wrong, the session opens with a warning, and the agent is told to act on the warning before doing anything else.&lt;/p&gt;

&lt;p&gt;None of this requires the agent to cooperate. None of it requires the human to remember.&lt;/p&gt;

&lt;p&gt;CI is the outer wall. Six jobs, parallel, on every pull request. Lint. Typecheck. Frontend tests. Backend tests across three shards. Build. Doc drift check. If any job fails, the PR cannot merge. If the PR does not have a label, a seventh job fails. &lt;strong&gt;Zero warnings allowed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the only layer that does not care how tired anyone is, how confused the agent is, or what quiet thing got dropped on the way through. It just says no.&lt;/p&gt;

&lt;p&gt;Tanya has never pushed to &lt;code&gt;main&lt;/code&gt;. She could not push to &lt;code&gt;main&lt;/code&gt; if she tried. Neither can I. We have confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scaffolding is the product
&lt;/h2&gt;

&lt;p&gt;Here is the funny thing about all of this: none of it is new.&lt;/p&gt;

&lt;p&gt;Linters. CI. Pre-commit hooks. Code review. Required labels. Branch protection. Engineering teams have been writing these guardrails for the humans on the team for two decades. What changed is that the same guardrails now work on the agent. And because the agent does most of the typing, they work for the non-engineer driving the agent too.&lt;/p&gt;

&lt;p&gt;The three layers are just discipline given shape. Rules tell the agent what you want, skills make the right thing the easiest thing, hooks enforce the non-negotiables. That stack is not new. It is what engineering has been doing for twenty years, and it works.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;did&lt;/em&gt; change is the cost. A &lt;code&gt;CLAUDE.md&lt;/code&gt; is an afternoon. A skill is another afternoon. A git hook is an hour. A &lt;code&gt;SessionStart&lt;/code&gt; script is an hour. Most of them I wrote in a single week, when the alternative was cleaning up after a mistake the agent had already made twice.&lt;/p&gt;

&lt;p&gt;The agent isn't the product, the scaffolding is. The agent is really good at working inside it without stepping on anything important.&lt;/p&gt;

&lt;p&gt;There is one piece of scaffolding I have not been able to write.&lt;/p&gt;

&lt;p&gt;It is the call on my calendar every few days where Tanya and I hop on a screen share and work through something hand-in-hand. We have a name for these. It is "make Tanya an engineer." Sometimes we are making her Claude Code statusline look cool. Sometimes we are picking out a new text editor and making it hers. Sometimes we are walking through how the app is put together so we can figure out how to make it even more awesome.&lt;/p&gt;

&lt;p&gt;They are still happening. They will probably still be happening in six months. I am not trying to automate them away.&lt;/p&gt;

&lt;p&gt;There is something about a real person on the other side of a screen share that makes the work feel survivable. A &lt;code&gt;CLAUDE.md&lt;/code&gt; will not do that. A slash command will not do that. The hooks definitely will not do that.&lt;/p&gt;

&lt;p&gt;That is also part of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things I owe you
&lt;/h2&gt;

&lt;p&gt;First, a callback to &lt;a href="https://dev.to/theminimalcreator/comment/3544p"&gt;Guilherme&lt;/a&gt; on the last post. You were right that Retool is bus-factor insurance. You can also roll your own. It's called a &lt;code&gt;CLAUDE.md&lt;/code&gt;, a pre-push hook that exits 1, and an hour-long call called "make Tanya an engineer."&lt;/p&gt;

&lt;p&gt;Second, a correction on the opener. Tanya is not the only other person shipping code to Tiger Den. A few other folks have been committing too. I made her the hero because her two-week arc was the cleanest story. Apologies to the rest of the team for the dramatic license.&lt;/p&gt;

&lt;p&gt;Airtable, it's been real. Tiger Den is better. Tanya is proof.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was reviewed and approved by Tanya before publishing. Zero AI agent skills involved.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webd</category>
    </item>
    <item>
      <title>How Do PostgreSQL Indices Work, Anyways?</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Wed, 18 Mar 2026 14:35:21 +0000</pubDate>
      <link>https://dev.to/tigerdata/how-do-postgresql-indices-work-anyways-3jnn</link>
      <guid>https://dev.to/tigerdata/how-do-postgresql-indices-work-anyways-3jnn</guid>
      <description>&lt;p&gt;You've probably created a hundred indexes in your career. Maybe a thousand. You ran &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, saw "Index Scan" instead of "Seq Scan," pumped your fist, and moved on.&lt;/p&gt;

&lt;p&gt;But do you actually know what's happening underneath? Because once you do, a lot of things about PostgreSQL performance start to make a &lt;em&gt;lot&lt;/em&gt; more sense. And some of the pain points you've been fighting start to feel less like mysteries and more like, well, physics.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's a tree. Obviously.
&lt;/h2&gt;

&lt;p&gt;The default index type in PostgreSQL is a B-tree. You knew that. But let's talk about what that actually means for your data.&lt;/p&gt;

&lt;p&gt;When you create an index on, say, a &lt;code&gt;timestamp&lt;/code&gt; column, PostgreSQL builds a balanced tree structure where each node contains keys and pointers. The leaf nodes point to actual heap tuples (your rows on disk). The internal nodes just help you navigate. Think of it like a phone book. (Do people still know what phone books are? I'm aging myself.)&lt;/p&gt;

&lt;p&gt;The key thing to understand: the index is a &lt;em&gt;separate data structure&lt;/em&gt; from your table. It lives in its own pages on disk. When you insert a row, PostgreSQL doesn't just write your row. It also has to update every index on that table. Every. Single. One.&lt;/p&gt;

&lt;p&gt;So if you have a table with five indexes and you're doing 50,000 inserts per second, that's not 50K write operations. That's 250K+ B-tree insertions per second, plus the heap write. Oof.&lt;/p&gt;

&lt;p&gt;You can see exactly how much space each index is consuming with &lt;code&gt;\di+&lt;/code&gt; in psql:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="c1"&gt;-- Or if you want programmatic access:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;idx_scan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;times_used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;idx_tup_read&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;tuples_read&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that on your biggest table. If you see indexes measured in gigabytes that have &lt;code&gt;idx_scan = 0&lt;/code&gt;, those indexes are costing you writes and giving you nothing back. They're dead weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pages, not rows
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. PostgreSQL doesn't read individual rows from disk. It reads 8KB pages. Always. Even if you only want one tiny row, you're pulling in a full 8KB page.&lt;/p&gt;

&lt;p&gt;Your B-tree is also organized into 8KB pages. Each page holds as many index entries as it can fit. For a simple index on a &lt;code&gt;bigint&lt;/code&gt; column, you can fit a few hundred entries per page. For a compound index on &lt;code&gt;(tenant_id, event_type, created_at)&lt;/code&gt;, you're fitting fewer because each entry is wider.&lt;/p&gt;

&lt;p&gt;When PostgreSQL traverses your B-tree, it starts at the root page, reads it, follows a pointer to the right internal page, reads that, and eventually gets to a leaf page that tells it where your actual row lives on the heap. For a table with a million rows, that's maybe three or four page reads. For a billion rows, it might be five or six. Logarithmic scaling is your friend here.&lt;/p&gt;

&lt;p&gt;You can see this in action with &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Look for lines like:&lt;/span&gt;
&lt;span class="c1"&gt;--   Index Scan using events_created_at_idx on events&lt;/span&gt;
&lt;span class="c1"&gt;--     Buffers: shared hit=4 read=2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;shared hit&lt;/code&gt; count tells you how many pages came from the buffer cache. The &lt;code&gt;read&lt;/code&gt; count tells you how many had to come from disk. If you're seeing high &lt;code&gt;read&lt;/code&gt; values on a query you run frequently, your working set has outgrown your &lt;code&gt;shared_buffers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But. (There's always a but.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody thinks about
&lt;/h2&gt;

&lt;p&gt;Those leaf pages need to stay ordered. When you insert a new value that belongs in the middle of a page that's already full, PostgreSQL has to split that page. Page splits are expensive. They cause write amplification and can fragment your index over time.&lt;/p&gt;

&lt;p&gt;For time-series data (timestamps always increasing), you mostly dodge this problem because new values go to the rightmost leaf. That's nice. But it creates a different problem: hot-page contention. Every concurrent insert is fighting to write to the same leaf page at the end of the tree.&lt;/p&gt;

&lt;p&gt;And then there's the part that really gets you: MVCC overhead.&lt;/p&gt;

&lt;p&gt;PostgreSQL's multiversion concurrency control means that even your index has to deal with tuple visibility. Index entries don't get removed immediately when a row is deleted or updated. They stick around until &lt;code&gt;VACUUM&lt;/code&gt; cleans them up. So your index isn't just tracking live rows. It's tracking &lt;em&gt;all the versions&lt;/em&gt; of your rows until the cleanup crew gets around to it.&lt;/p&gt;

&lt;p&gt;For a high-churn table, your index can be significantly larger than you'd expect just from the row count. I've seen cases where the index is effectively 2-3x the "expected" size because of dead tuple bloat.&lt;/p&gt;

&lt;p&gt;Here's how to check if bloat is eating your indexes alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;n_dead_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;n_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;nullif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_live_tup&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dead_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;last_autovacuum&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n_dead_tup&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;dead_pct&lt;/code&gt; is climbing above 10-20% and &lt;code&gt;last_autovacuum&lt;/code&gt; was hours ago (or null), autovacuum is falling behind. That bloat isn't just wasting space. It's making every index scan touch more pages than it should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index-only scans (and why they're worth understanding)
&lt;/h2&gt;

&lt;p&gt;There's one more behavior worth knowing about, because it changes how you think about index design.&lt;/p&gt;

&lt;p&gt;Normally, PostgreSQL uses the index to find &lt;em&gt;where&lt;/em&gt; a row lives on the heap, then goes and reads the actual row. That's two separate lookups: the index, then the heap.&lt;/p&gt;

&lt;p&gt;But if every column your query needs is already &lt;em&gt;in&lt;/em&gt; the index, PostgreSQL can skip the heap entirely. That's an index-only scan, and it's significantly faster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This index covers both the WHERE clause and the SELECT list:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_covering&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Now this query never touches the heap:&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 hour'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Look for:&lt;/span&gt;
&lt;span class="c1"&gt;--   Index Only Scan using idx_events_covering on events&lt;/span&gt;
&lt;span class="c1"&gt;--     Heap Fetches: 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Heap Fetches: 0&lt;/code&gt; is what you want. That means PostgreSQL answered the entire query from the index alone.&lt;/p&gt;

&lt;p&gt;The catch: index-only scans only work well when the visibility map is up to date, which brings us right back to VACUUM. If VACUUM hasn't visited a page recently, PostgreSQL can't trust the index alone and has to check the heap anyway. So even this optimization depends on keeping autovacuum healthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partial indexes (less is more)
&lt;/h2&gt;

&lt;p&gt;One more tool that's underused: partial indexes. If you only query a subset of your data most of the time, you can index just that subset.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Instead of indexing every row:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_status&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Index only the rows that matter:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_events_active&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The partial index is smaller, faster to scan, and cheaper to maintain on writes. For high-churn tables where most queries filter to a small slice of data, this is free performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why does this matter?
&lt;/h2&gt;

&lt;p&gt;Understanding this stuff isn't just academic. It explains real problems you hit in production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why adding indexes slows down writes.&lt;/strong&gt; Every index is another B-tree that needs to be maintained on every insert. It's not free. It's never been free. The cost just hides until you're at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why your queries get slower over time even though nothing changed.&lt;/strong&gt; Index bloat from dead tuples. Pages that used to be tightly packed are now half-empty after splits and vacuuming. Your three-page-read query is now a six-page-read query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why VACUUM matters so much.&lt;/strong&gt; It's not just reclaiming table space. It's keeping your indexes healthy. If autovacuum can't keep up, your indexes degrade. And if you're inserting fast enough, autovacuum can fall behind. That's not a bug. That's just the architecture working as designed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why partitioning helps (and then stops helping).&lt;/strong&gt; Smaller partitions mean smaller indexes mean fewer tree levels. Great. But now your query planner has to evaluate all those partitions to figure out which ones to scan. And that planning cost scales linearly with partition count. You're trading one bottleneck for another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;I wrote about this cycle more extensively in a piece about &lt;a href="https://www.tigerdata.com/blog/postgres-optimization-treadmill?utm_source=devto&amp;amp;utm_medium=da-activity&amp;amp;utm_campaign=matty-digital" rel="noopener noreferrer"&gt;the PostgreSQL optimization treadmill&lt;/a&gt;. The short version: there's a pretty predictable progression that teams go through. Optimize indexes. Partition tables. Tune autovacuum. Scale vertically. Add read replicas. Each phase buys you a few months.&lt;/p&gt;

&lt;p&gt;That's not a criticism of PostgreSQL. Postgres is an incredible database. But it's a &lt;em&gt;general-purpose&lt;/em&gt; relational database, and its architecture reflects that. The heap storage model, MVCC, the query planner, B-trees. They're all designed to handle a wide range of workloads really well. The tradeoff is that for very specific access patterns (like time-series data at scale), those general-purpose design choices start working against you instead of for you.&lt;/p&gt;

&lt;p&gt;Understanding &lt;em&gt;how&lt;/em&gt; your indexes work is the first step to understanding &lt;em&gt;when&lt;/em&gt; they stop being enough. And knowing when you're fighting the architecture instead of optimizing within it can save you months of whack-a-mole performance tuning.&lt;/p&gt;

&lt;p&gt;But that's a topic for another day. For now, go run these queries on your biggest table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- How big are your indexes, really?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;idx_scan&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;scans&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your_table_here'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Are any of them unused?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;idx_scan&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;pg_stat_user_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;idx_scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt;  &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might be surprised.&lt;/p&gt;

</description>
      <category>post</category>
    </item>
    <item>
      <title>Coding Agents Are Actually Good at This One Thing</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Sun, 01 Mar 2026 20:31:28 +0000</pubDate>
      <link>https://dev.to/mattstratton/coding-agents-are-actually-good-at-this-one-thing-5dej</link>
      <guid>https://dev.to/mattstratton/coding-agents-are-actually-good-at-this-one-thing-5dej</guid>
      <description>&lt;p&gt;The discourse around AI coding tools tends to collapse into two camps: people who think they're going to replace developers, and people dunking on "vibe coding" demos that fall apart the moment you look at them sideways.&lt;/p&gt;

&lt;p&gt;Both camps are mostly arguing about the wrong thing.&lt;/p&gt;

&lt;p&gt;I've been using coding agents heavily for the past few months, and the use case where they've actually changed how I work isn't production applications or greenfield SaaS ideas. It's internal tooling. And that distinction matters more than people are giving it credit for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Airtable Problem
&lt;/h2&gt;

&lt;p&gt;For years, I built internal tools the way most of us did: I found something close enough and made it work. Airtable was great for this. I built conference talk trackers, content inventories, planning dashboards... all kinds of things. The spreadsheet-meets-database model was genuinely useful, and for a while, it was the right call.&lt;/p&gt;

&lt;p&gt;But you're always fighting two things with tools like Airtable or Notion or even Asana when you're bending them to a purpose they weren't built for. First, you fight the constraints of the tool itself: the data model it has, the views it supports, the automations it allows. Second, you fight the cost and friction of giving your whole team access to yet another SaaS platform.&lt;/p&gt;

&lt;p&gt;At some point the juice stops being worth the squeeze. You're not building what you actually need, you're building the closest approximation that fits inside someone else's product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Just Build the Thing
&lt;/h2&gt;

&lt;p&gt;What coding agents have unlocked for me is the ability to &lt;em&gt;just build the thing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The project I've been heads-down on is called Tiger Den: an internal content management system for our Marketing team at Tiger Data. It's a Next.js app, Drizzle ORM, tRPC, Postgres (running on &lt;a href="https://tigerdata.com" rel="noopener noreferrer"&gt;Tiger Data&lt;/a&gt;, natch). A real web app with a real database, exactly structured for how we actually work.&lt;/p&gt;

&lt;p&gt;Tiger Den manages our content library, tracks blog posts and videos, stores voice profiles for different authors so we can make sure our content matches our voices, generates UTM links, and surfaces the right content for the right workflow. It's not a product. It's not trying to be. It's a tool that fits our team's exact needs because we built it for our team's exact needs.&lt;/p&gt;

&lt;p&gt;A few folks on the team are starting to use it. Workflows that used to involve a bunch of spreadsheet wrangling and web search gymnastics are getting simpler. It's not fully baked (it's somewhere between "mostly works for me" and "the team is starting to play with it") but it's already more useful than what it replaced (a combination of "nothing" plus "not very updated spreadsheet"), and I've been iterating on it continuously.&lt;/p&gt;

&lt;p&gt;That iteration speed is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Internal Tooling Specifically
&lt;/h2&gt;

&lt;p&gt;The reason coding agents shine here, and why I keep coming back to this use case, is the risk profile.&lt;/p&gt;

&lt;p&gt;When you're building a public-facing application, you have to worry about edge cases at scale, security hardening, performance under load, what happens when a user does something unexpected, what happens when a thousand users do something unexpected. The cost of getting it wrong is high.&lt;/p&gt;

&lt;p&gt;Internal tools have a completely different calculus. The user population is small and known. The stakes for any individual bug are low. You can ship something that's 80% right and fix the other 20% next week. You can move fast and it's actually fine.&lt;/p&gt;

&lt;p&gt;This is the Microsoft Access model, honestly. In the 90s and early 2000s, people built remarkably functional internal tools in Access (forms, reports, relational data, real logic) because the barrier was low enough that you could actually do it. Then the web happened, the complexity went way up, and that era of "just build the thing" basically ended for most people.&lt;/p&gt;

&lt;p&gt;Coding agents are bringing it back. The barrier is low again. You can describe what you want, iterate in a tight loop, and end up with something that actually fits your use case instead of something that almost fits someone else's template.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About Retool?
&lt;/h2&gt;

&lt;p&gt;When I posted about this, someone brought up Retool — fair question. Isn't that exactly what Retool is for?&lt;/p&gt;

&lt;p&gt;Maybe. But my reaction was: isn't that just trading one set of constraints for another? Now I'm stuck on how Retool thinks about the world instead of how Airtable thinks about the world. I haven't dug deep into Retool, so I'm not going to trash it, but the pattern is the same: you're building inside someone else's model of what your tool should be.&lt;/p&gt;

&lt;p&gt;The thing I actually want is to build the thing I want. That's what "just use Next.js and a database" gives me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some Honest Caveats
&lt;/h2&gt;

&lt;p&gt;I want to be careful not to oversell this, because the context matters a lot.&lt;/p&gt;

&lt;p&gt;I'm a systems guy. Infrastructure, security, DevOps, that's my background. I could build Tiger Den without a coding agent; it would just take a lot longer because I'm coming at software development somewhat sideways. The agent collapses that time gap significantly. But I'm still making real architectural decisions, reviewing what gets generated, and understanding the codebase well enough to maintain it. This isn't "describe what you want and walk away."&lt;/p&gt;

&lt;p&gt;Someone also asked about maintenance (a reasonable concern!). My answer: for Tiger Den specifically, I'm only writing to my own database. I'm not doing write operations against external systems, I'm not touching production data, and the blast radius of any given bug is small. That's a deliberate choice, and it's part of why internal tooling is the right framing. If I were building something that talked to our customers' data or integrated deeply with critical external systems, I'd be a lot more careful.&lt;/p&gt;

&lt;p&gt;The low-stakes calculus stops applying the moment you have real users, real consequences for downtime, or data you can't afford to mess up. Keep that boundary clear and this approach works really well. Blur it and you're asking for trouble.&lt;/p&gt;

&lt;p&gt;But for "my team needs a thing and the existing tools don't quite fit"? This is legitimately transformative. I've wanted to be able to build internal tools this easily for a long time. Now I can.&lt;/p&gt;

&lt;p&gt;Airtable, it's been real. Tiger Den is better.&lt;/p&gt;

</description>
      <category>webd</category>
    </item>
    <item>
      <title>Product 101: Your Secret Weapon for Understanding the Business</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Tue, 04 Nov 2025 14:58:55 +0000</pubDate>
      <link>https://dev.to/mattstratton/product-101-your-secret-weapon-for-understanding-the-business-2m6j</link>
      <guid>https://dev.to/mattstratton/product-101-your-secret-weapon-for-understanding-the-business-2m6j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 5 of my 7-part series on business literacy for DevRel. &lt;a href="https://dev.to/mattstratton/why-business-literacy-matters-for-devrel-and-why-you-cant-skip-this-step-30p1"&gt;Start with Part 1 if you missed it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We've covered sales, marketing, and finance. But there's one group we haven't talked about yet - and they might be your most valuable partnership in the entire company: &lt;strong&gt;Product&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because product managers are inherently better than anyone else (though the good ones are pretty great). But because strategic product leaders sit at the intersection of everything. They see the business from a unique vantage point that can be incredibly valuable to DevRel.&lt;/p&gt;

&lt;p&gt;A good product partner is worth their weight in gold. They're your early warning system, your business intelligence source, and your reality check all rolled into one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Product Actually Does
&lt;/h2&gt;

&lt;p&gt;At the most basic level, product management is about figuring out what to build, why to build it, and making sure it actually gets built.&lt;/p&gt;

&lt;p&gt;But that simple description hides a ton of complexity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product managers (PMs) are responsible for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deciding what features/capabilities to prioritize&lt;/li&gt;
&lt;li&gt;Understanding user needs and market opportunities&lt;/li&gt;
&lt;li&gt;Working with engineering to build the right things&lt;/li&gt;
&lt;li&gt;Collaborating with go-to-market teams (sales, marketing) to position and launch features&lt;/li&gt;
&lt;li&gt;Measuring whether what they built is actually working&lt;/li&gt;
&lt;li&gt;Saying "no" to a lot of things (this is actually most of the job)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Product sits at the center of a bunch of competing pressures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales wants features that will close deals&lt;/li&gt;
&lt;li&gt;Marketing wants things that will generate buzz&lt;/li&gt;
&lt;li&gt;Engineering wants to build technically interesting things&lt;/li&gt;
&lt;li&gt;Finance wants efficient use of resources&lt;/li&gt;
&lt;li&gt;Customers want their specific problems solved&lt;/li&gt;
&lt;li&gt;Executives want the product to support business strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PM's job is to synthesize all of this and make decisions about what the product should become.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the Product Team: Roles You Need to Know
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chief Product Officer (CPO) / VP of Product&lt;/strong&gt;: Sets the overall product vision and strategy. They're thinking about where the product needs to be in 2-3 years, not just next quarter. They usually report directly to the CEO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Director of Product / Senior PM&lt;/strong&gt;: Usually owns a major product area or domain. They're thinking strategically about their area while managing other PMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Manager (PM)&lt;/strong&gt;: Owns a specific product or feature area. They work day-to-day with engineering teams to ship features. This is who you'll probably work with most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Product Manager (TPM)&lt;/strong&gt;: Like a PM but more technical. Often owns platform, API, or developer-facing products. If you work at a developer tools company, TPMs are your people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Marketing Manager (PMM)&lt;/strong&gt;: We talked about these folks in the marketing post, but they sit between product and marketing. They take what product builds and figure out how to position and message it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Operations / Product Ops&lt;/strong&gt;: Help PMs be more effective by managing tools, processes, and data. They're the behind-the-scenes folks making product teams run smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Product Thinks About the World
&lt;/h2&gt;

&lt;p&gt;Product managers live in a few key frameworks. Understanding these helps you speak their language:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Product Roadmap
&lt;/h3&gt;

&lt;p&gt;This is the plan for what gets built when. Roadmaps are usually organized by quarters or releases, and they're always wrong (because priorities change). &lt;/p&gt;

&lt;p&gt;Roadmaps are political documents as much as planning documents. What makes it onto the roadmap and what doesn't tells you a lot about what the business values.&lt;/p&gt;

&lt;h3&gt;
  
  
  User Stories and Jobs to Be Done
&lt;/h3&gt;

&lt;p&gt;PMs think about user needs in terms of "&lt;a href="https://www.atlassian.com/agile/project-management/user-stories" rel="noopener noreferrer"&gt;user stories&lt;/a&gt;" (as a [role], I want to [do something] so that [outcome]) or "&lt;a href="https://hbr.org/2016/09/know-your-customers-jobs-to-be-done" rel="noopener noreferrer"&gt;jobs to be done&lt;/a&gt;" (when [situation], I want to [motivation], so I can [expected outcome]).&lt;/p&gt;

&lt;p&gt;This is useful for DevRel because it's how you can frame developer feedback. Instead of "developers want feature X," try "when developers are trying to debug production issues, they want observability into what the system is doing, so they can identify root causes faster."&lt;/p&gt;

&lt;h3&gt;
  
  
  Prioritization Frameworks
&lt;/h3&gt;

&lt;p&gt;PMs are always prioritizing. Common frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.intercom.com/blog/rice-simple-prioritization-for-product-managers/" rel="noopener noreferrer"&gt;RICE&lt;/a&gt;&lt;/strong&gt;: Reach, Impact, Confidence, Effort&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value vs. Effort&lt;/strong&gt;: Classic 2x2 matrix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.productplan.com/glossary/kano-model/" rel="noopener noreferrer"&gt;Kano Model&lt;/a&gt;&lt;/strong&gt;: Basic needs vs. performance needs vs. delighters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding how your product team prioritizes helps you frame feedback effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Success Metrics
&lt;/h3&gt;

&lt;p&gt;Good PMs are all about metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Activation&lt;/strong&gt;: Did users do the key action that shows they "get" the product?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement&lt;/strong&gt;: Are users coming back? How often?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention&lt;/strong&gt;: Are users sticking around over time?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature adoption&lt;/strong&gt;: Are users actually using the new thing we built?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to value&lt;/strong&gt;: How quickly can users get value from the product?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice how some of these overlap with PLG metrics? That's not an accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Product Matters So Much to DevRel
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. Product is uniquely positioned to give you intelligence about the business that you can't get anywhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  They See Across Functions
&lt;/h3&gt;

&lt;p&gt;A strategic PM talks to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales (what's blocking deals?)&lt;/li&gt;
&lt;li&gt;Marketing (what's resonating in the market?)&lt;/li&gt;
&lt;li&gt;Customer success (what are customers struggling with?)&lt;/li&gt;
&lt;li&gt;Engineering (what's technically feasible?)&lt;/li&gt;
&lt;li&gt;Finance (what can we afford?)&lt;/li&gt;
&lt;li&gt;Executives (what's the strategy?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're synthesizing information from everywhere. If you have a good relationship with product leadership, they can give you context on what's really happening across the business that you'd never get from any single function.&lt;/p&gt;

&lt;h3&gt;
  
  
  They Know What's Coming
&lt;/h3&gt;

&lt;p&gt;Product knows what's on the roadmap before it's public. This is incredibly valuable for DevRel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can prep content in advance&lt;/li&gt;
&lt;li&gt;You can understand strategic direction&lt;/li&gt;
&lt;li&gt;You can give early feedback on how developers will react&lt;/li&gt;
&lt;li&gt;You can plan your activities around major releases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  They Need What You Have
&lt;/h3&gt;

&lt;p&gt;As a devrel, you're sitting on a goldmine of information that product desperately needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What developers are actually struggling with&lt;/li&gt;
&lt;li&gt;What features developers are asking for (and why)&lt;/li&gt;
&lt;li&gt;How developers talk about problems in their own language&lt;/li&gt;
&lt;li&gt;What the competition is doing (you see it at conferences and in communities)&lt;/li&gt;
&lt;li&gt;Which parts of your product confuse people&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates natural reciprocity. You help them, they help you.&lt;/p&gt;

&lt;h3&gt;
  
  
  They Fight Similar Battles
&lt;/h3&gt;

&lt;p&gt;Good PMs face the same challenge you do: they have to translate between technical reality and business objectives. They have to advocate for what's right while acknowledging business constraints.&lt;/p&gt;

&lt;p&gt;You're natural allies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product/DevRel Partnership
&lt;/h2&gt;

&lt;p&gt;When done right, the Product/DevRel partnership is incredibly powerful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DevRel provides Product:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raw, unfiltered user feedback from communities&lt;/li&gt;
&lt;li&gt;Market intelligence from conferences and events&lt;/li&gt;
&lt;li&gt;Early signals about what developers care about&lt;/li&gt;
&lt;li&gt;Reality checks on how features will be received&lt;/li&gt;
&lt;li&gt;Developer perspective on product decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Product provides DevRel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Early access to roadmap and upcoming features&lt;/li&gt;
&lt;li&gt;Context on why decisions were made&lt;/li&gt;
&lt;li&gt;Understanding of strategic priorities&lt;/li&gt;
&lt;li&gt;Cross-functional intelligence&lt;/li&gt;
&lt;li&gt;Air cover when you need to push back on requests&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Feedback Loop
&lt;/h3&gt;

&lt;p&gt;One of DevRel's most valuable contributions is being a structured feedback channel from developers to product. But this only works if you do it right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad feedback:&lt;/strong&gt; "Developers want dark mode"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Good feedback:&lt;/strong&gt; "I'm seeing consistent requests for dark mode from developers who code at night. The specific pain point is eye strain during long coding sessions. This came up in 15+ community conversations last month, and competitors X and Y both shipped this recently."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad feedback:&lt;/strong&gt; "This new feature is confusing"&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Good feedback:&lt;/strong&gt; "In office hours, 3 out of 5 developers got stuck at the same step in the new onboarding flow. The confusion is around [specific thing]. Here's a recording of where they got stuck."&lt;/p&gt;

&lt;p&gt;Strategic product leaders eat this stuff up because it's specific, actionable, and helps them make better decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not All PMs Are Created Equal
&lt;/h2&gt;

&lt;p&gt;That said, not every PM is strategic. Some are just feature factories, cranking out roadmap items without understanding the broader context.&lt;/p&gt;

&lt;p&gt;A comment that inspired this post mentioned this perfectly: not every product leader can "get their heads out of the sands of features/roadmap, nor are they all observant of business value themselves."&lt;/p&gt;

&lt;p&gt;Look for PMs who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand the business model, not just the feature list&lt;/li&gt;
&lt;li&gt;Can articulate why something is being built, not just what&lt;/li&gt;
&lt;li&gt;Talk to customers and users regularly&lt;/li&gt;
&lt;li&gt;Have opinions about strategy, not just execution&lt;/li&gt;
&lt;li&gt;Are curious about the market and competition&lt;/li&gt;
&lt;li&gt;Actually understand the technical implications of their decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the ones worth partnering closely with. The others... well, you can still work with them, but don't expect the strategic partnership.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Product Decisions Happen (And Where You Fit In)
&lt;/h2&gt;

&lt;p&gt;Understanding how product decisions get made helps you influence them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input gathering:&lt;/strong&gt; PMs collect input from sales, customers, community, market research, executives, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prioritization:&lt;/strong&gt; PMs use frameworks to decide what's most important&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roadmap planning:&lt;/strong&gt; Decisions get formalized into "we're building X in Q2"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution:&lt;/strong&gt; Engineering builds it&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch:&lt;/strong&gt; Product goes to market with help from marketing, sales, and (hopefully) DevRel&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measurement:&lt;/strong&gt; Did it work?&lt;/p&gt;

&lt;p&gt;DevRel can influence at almost every stage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; Share developer feedback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritization:&lt;/strong&gt; Provide market context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roadmap:&lt;/strong&gt; React to plans, suggest adjustments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution:&lt;/strong&gt; Give technical feedback during development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch:&lt;/strong&gt; Create content, drive awareness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurement:&lt;/strong&gt; Share qualitative feedback on how it's landing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you have to be in the conversation to have influence. Which means building relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevRel Seat at the Product Table
&lt;/h2&gt;

&lt;p&gt;Some DevRel teams are deeply integrated with product. Others are completely disconnected. If you're in the latter camp, here's how to change that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start small:&lt;/strong&gt; Ask to sit in on a product planning meeting as an observer&lt;br&gt;
&lt;strong&gt;Provide value first:&lt;/strong&gt; Share useful feedback before asking for anything&lt;br&gt;
&lt;strong&gt;Speak their language:&lt;/strong&gt; Frame things in terms of user problems and business impact&lt;br&gt;
&lt;strong&gt;Be consistent:&lt;/strong&gt; Show up regularly with valuable input&lt;br&gt;
&lt;strong&gt;Don't be precious:&lt;/strong&gt; Accept that you won't always get what you want&lt;br&gt;
&lt;strong&gt;Understand constraints:&lt;/strong&gt; Product is making trade-offs; understand what they are&lt;/p&gt;

&lt;h3&gt;
  
  
  Regular Touchpoints That Work
&lt;/h3&gt;

&lt;p&gt;Consider establishing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monthly sync with product leadership:&lt;/strong&gt; Share what you're hearing, get context on roadmap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback review sessions:&lt;/strong&gt; Present synthesized developer feedback quarterly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early access to betas:&lt;/strong&gt; Get hands-on with features before launch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch planning collaboration:&lt;/strong&gt; Work together on go-to-market for major features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roadmap input sessions:&lt;/strong&gt; Provide perspective on upcoming priorities&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Product Gives You Business Intelligence
&lt;/h2&gt;

&lt;p&gt;Here's something nobody talks about: a good product partner can be your best source of business intelligence.&lt;/p&gt;

&lt;p&gt;They'll tell you things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Sales is really struggling in the enterprise segment right now"&lt;/li&gt;
&lt;li&gt;"We're shifting strategy toward [market] next year"&lt;/li&gt;
&lt;li&gt;"The exec team is worried about [competitive threat]"&lt;/li&gt;
&lt;li&gt;"Marketing's campaigns aren't converting well"&lt;/li&gt;
&lt;li&gt;"There might be budget cuts coming"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why do they know this? Because they're in all the meetings. They see the cross-functional dynamics. They hear what's really going on.&lt;/p&gt;

&lt;p&gt;And if they trust you, they'll share it. This is incredibly valuable for planning your DevRel strategy and seeing what's coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential Product Terminology
&lt;/h2&gt;

&lt;p&gt;Let me give you the vocabulary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.agilealliance.org/glossary/mvp/" rel="noopener noreferrer"&gt;MVP (Minimum Viable Product)&lt;/a&gt;&lt;/strong&gt;: The smallest version of something that delivers value. Often used as an excuse to ship incomplete things, but the concept is sound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.lennysnewsletter.com/p/how-to-know-if-youve-got-productmarket" rel="noopener noreferrer"&gt;Product-Market Fit&lt;/a&gt;&lt;/strong&gt;: The magical moment when you've built something that a market actually wants. Most products never achieve this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature parity&lt;/strong&gt;: Having the same features as competitors. Often overvalued by sales, undervalued by product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical debt&lt;/strong&gt;: The accumulated cost of quick-and-dirty technical decisions. Always more than you think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dogfooding&lt;/strong&gt;: Using your own product internally. Good PMs do this religiously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beta / Alpha&lt;/strong&gt;: Early versions for testing. Alpha is usually internal, beta is usually external but limited.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GA (General Availability)&lt;/strong&gt;: The feature is fully launched and available to everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sunset / Deprecation&lt;/strong&gt;: Retiring a feature or product. Always painful, usually necessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.whatmatters.com/faqs/okr-meaning-definition-example/" rel="noopener noreferrer"&gt;OKRs (Objectives and Key Results)&lt;/a&gt;&lt;/strong&gt;: How many product teams set goals. Objectives are qualitative, key results are measurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means For Your Work
&lt;/h2&gt;

&lt;p&gt;Understanding product doesn't mean becoming a PM. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can provide product with valuable, actionable feedback&lt;/li&gt;
&lt;li&gt;You can anticipate what's coming and plan accordingly&lt;/li&gt;
&lt;li&gt;You can understand why certain decisions are made (even when you disagree)&lt;/li&gt;
&lt;li&gt;You can be a strategic partner instead of just a feedback conduit&lt;/li&gt;
&lt;li&gt;You have access to cross-functional intelligence that helps you navigate the business&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Product is your window into how the whole business is working - the headwinds, the tailwinds, the tensions, the priorities. Use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding Your Product Partner
&lt;/h2&gt;

&lt;p&gt;Not sure who to connect with in product? Start here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the PM who owns your developer-facing products&lt;/strong&gt; (API, SDK, CLI, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach out with value:&lt;/strong&gt; "Hey, I'm hearing a lot of feedback about [thing]. Want to chat?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be low-maintenance:&lt;/strong&gt; Respect their time, be organized, come prepared&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for strategic thinkers:&lt;/strong&gt; Find the PMs who see the bigger picture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the relationship over time:&lt;/strong&gt; This is a long game&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And if you find a great product partner? Hold onto them. They're rare and incredibly valuable.&lt;/p&gt;

&lt;p&gt;Next up: we'll talk about Product-Led Growth and how it changes everything about the relationship between product, DevRel, and go-to-market. Spoiler: in PLG companies, product and DevRel become deeply intertwined.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions about working with product? War stories about great (or terrible) product partnerships? Let's hear them in the comments!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Previously:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/finance-101-budgets-pls-and-the-language-of-money-3dp9"&gt;Part 4: Finance 101&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/product-led-growth-and-what-it-means-for-devrel-10mj"&gt;Product-Led Growth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devrel</category>
    </item>
    <item>
      <title>Putting It All Together: Speaking Business as a DevRel Professional</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Tue, 28 Oct 2025 18:54:34 +0000</pubDate>
      <link>https://dev.to/mattstratton/putting-it-all-together-speaking-business-as-a-devrel-professional-2kfj</link>
      <guid>https://dev.to/mattstratton/putting-it-all-together-speaking-business-as-a-devrel-professional-2kfj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 7 of my 7-part series on business literacy for DevRel. &lt;a href="https://dev.to/mattstratton/why-business-literacy-matters-for-devrel-and-why-you-cant-skip-this-step-30p1"&gt;Start with Part 1 if you missed it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We've covered a lot of ground in this series. Sales pipelines and forecast accuracy. Marketing funnels and MQLs. P&amp;amp;Ls and CapEx vs OpEx. Product-Led Growth and how it changes the game for developer tools companies. If your brain feels a little full right now, that's normal. This is a lot of new vocabulary and new ways of thinking.&lt;/p&gt;

&lt;p&gt;But you now have the foundation. You understand the basics of how the key parts of your business operate, what they care about, and what language they speak. You understand how modern go-to-market models work and where DevRel fits in.&lt;/p&gt;

&lt;p&gt;Now let's talk about what you actually &lt;em&gt;do&lt;/em&gt; with all this knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DevRel Sits in the Business Landscape
&lt;/h2&gt;

&lt;p&gt;If you think about your company as an ecosystem, DevRel occupies a unique position. You're not sales, but you influence deals. You're not marketing, but you create awareness and demand. You're not product, but you deeply understand user needs. You're not support, but you help developers be successful.&lt;/p&gt;

&lt;p&gt;I often say "DevRel rhymes with lots of different parts of the business"&lt;/p&gt;

&lt;p&gt;You're the bridge. You're the translator. You're uniquely positioned to connect the technical world of developers with the commercial world of business.&lt;/p&gt;

&lt;p&gt;And that's actually a superpower, not a weakness. The question is, &lt;strong&gt;are you using it?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevRel Translation Guide
&lt;/h2&gt;

&lt;p&gt;Let's get practical. Here's how you translate common DevRel activities into language that resonates with different stakeholders:&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: Conference Talks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What you do&lt;/strong&gt;: Give a technical talk at a conference about solving a real problem using your technology&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How different stakeholders hear it&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Sales&lt;/strong&gt;: "This is TOFU content that builds awareness and credibility in a target market. Prospects who attend or watch this talk are more likely to trust us when we're in their consideration set."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Marketing&lt;/strong&gt;: "This creates thought leadership content at the top of the funnel, reaching developers in our target persona. The recorded talk becomes an asset we can promote for months."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Finance&lt;/strong&gt;: "For a $5K investment (travel, event costs), we reach 200 in-person developers and potentially 10K+ via the recorded talk. Compared to paid advertising costs of $50-100 per qualified lead, this is highly efficient awareness generation."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Your CEO&lt;/strong&gt;: "This positions our company as a technical leader in [space], increases brand awareness with our target audience, and creates trust that accelerates the sales cycle."&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Office Hours Program
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What you do&lt;/strong&gt;: Run weekly office hours where developers can get help with technical challenges&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How different stakeholders hear it&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Sales&lt;/strong&gt;: "This is MOFU activity that helps prospects overcome technical blockers that might prevent deals from closing. Sales can invite prospects to office hours as a value-add during the evaluation phase."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Marketing&lt;/strong&gt;: "This creates ongoing touchpoints with developers in the consideration phase, moving them down the funnel. It also generates insights about common pain points we can address in our content."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Finance&lt;/strong&gt;: "This program costs approximately $X per quarter (staff time) and supports Y developers, reducing the load on our paid support team (saving $Z) while accelerating deal velocity."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Product&lt;/strong&gt;: "This is a direct line to user feedback. We're hearing real problems from real users, which informs our product roadmap and helps prioritize features."&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Documentation Improvements
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What you do&lt;/strong&gt;: Rewrite documentation to make it clearer and more accessible for developers&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How different stakeholders hear it&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Sales&lt;/strong&gt;: "Better docs reduce technical objections during the sales cycle. When prospects can self-serve and get quick answers, it reduces friction in the buying process."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Marketing&lt;/strong&gt;: "Great docs are a conversion tool. When someone is evaluating our product, comprehensive docs signal quality and reduce perceived risk."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Finance&lt;/strong&gt;: "Improved docs reduce support ticket volume by X%, which translates to $Y in reduced support costs. It also improves trial-to-paid conversion, impacting revenue."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Product&lt;/strong&gt;: "Quality documentation improves user experience and product adoption, which reduces churn and improves LTV."&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 4: Community Building
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What you do&lt;/strong&gt;: Build and nurture an active community where developers help each other&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How different stakeholders hear it&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Sales&lt;/strong&gt;: "An active community is social proof. When prospects see thousands of engaged developers, it reduces perceived risk and speeds up the sales cycle."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Marketing&lt;/strong&gt;: "Community creates network effects that reduce our CAC over time. Developers in the community become advocates who bring in new users organically."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Finance&lt;/strong&gt;: "The community provides peer-to-peer support that would otherwise require paid support resources. If 70% of questions are answered by community members, that's saving us $X in support costs annually."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Your CEO&lt;/strong&gt;: "Our community is a competitive moat. It's relationship capital that competitors can't easily replicate, and it creates switching costs that improve retention."&lt;/p&gt;

&lt;h2&gt;
  
  
  Having Better Conversations With Stakeholders
&lt;/h2&gt;

&lt;p&gt;The key to using your business literacy isn't just knowing the vocabulary. It's understanding what different stakeholders care about and framing your work in those terms.&lt;/p&gt;

&lt;h3&gt;
  
  
  With Sales
&lt;/h3&gt;

&lt;p&gt;Talk about: pipeline influence, deal velocity, technical credibility, competitive positioning, removing buying blockers&lt;/p&gt;

&lt;p&gt;Example: "I'd like to pilot a program where DevRel joins strategic deals for technical deep dives. Based on what I've seen, this could reduce time-to-close by 2-3 weeks by addressing technical concerns earlier in the cycle. Want to try this with three opportunities next quarter and measure the impact?"&lt;/p&gt;

&lt;h3&gt;
  
  
  With Marketing
&lt;/h3&gt;

&lt;p&gt;Talk about: funnel stages, campaign support, content performance, lead quality, brand awareness&lt;/p&gt;

&lt;p&gt;Example: "I noticed the marketing team is planning a campaign around [topic]. DevRel can support this by creating technical deep-dive content that serves the MOFU stage. We'll own the technical accuracy and authentic voice, which should improve conversion from awareness to consideration."&lt;/p&gt;

&lt;h3&gt;
  
  
  With Finance
&lt;/h3&gt;

&lt;p&gt;Talk about: budget allocation, ROI, cost efficiency, strategic investment, measurable outcomes&lt;/p&gt;

&lt;p&gt;Example: "I'm requesting $30K for three conference sponsorships next quarter. Based on last year's data, this typically generates 150 qualified conversations, with 10-15 entering our pipeline. At our average deal size, if just two deals close, that's $200K in revenue from a $30K investment."&lt;/p&gt;

&lt;h3&gt;
  
  
  With Executives
&lt;/h3&gt;

&lt;p&gt;Talk about: business objectives, market position, competitive advantage, growth levers, strategic priorities&lt;/p&gt;

&lt;p&gt;Example: "Our community strategy supports three key company objectives: it reduces CAC through organic growth, it creates customer stickiness that improves retention, and it provides product insights that help us build what developers actually need. This quarter we'll focus on [specific initiative] to accelerate growth in [target segment]."&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls to Avoid
&lt;/h2&gt;

&lt;p&gt;As you develop your business literacy and start speaking this language, watch out for these traps:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-rotating to business speak&lt;/strong&gt;: Don't become so focused on business outcomes that you lose sight of serving developers. If developers don't genuinely benefit from what you're doing, it's not sustainable DevRel work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claiming direct attribution when it's influence&lt;/strong&gt;: Be honest about what you can and can't directly attribute to DevRel. Influence is still valuable, but don't claim you "closed the deal" when you participated in one discovery call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using business literacy to justify work that doesn't serve developers&lt;/strong&gt;: Business literacy should help you advocate for developer-serving work, not rationalize doing things that only benefit the business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Becoming a "mini-marketer"&lt;/strong&gt;: You're not marketing. You can use marketing language to communicate, but don't let that change what you actually do or how you do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overpromising to get budget&lt;/strong&gt;: It's tempting to make big claims to secure resources, but if you can't deliver, you damage your credibility for future asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business Literacy ≠ Selling Out
&lt;/h2&gt;

&lt;p&gt;Let's address this head-on one more time: &lt;strong&gt;learning to speak business language doesn't mean you're selling out&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You're still serving developers first. You're still advocating for their needs. You're still building authentic relationships and creating genuine value.&lt;/p&gt;

&lt;p&gt;What you're doing is making sure the people who control resources understand why that work matters to the business. You're ensuring that the work you believe in gets funded. You're protecting your team and your programs by being able to articulate their value.&lt;/p&gt;

&lt;p&gt;The alternative - being "too pure" to speak business language - often ends with your team getting cut and your programs getting defunded. And then who does that serve?&lt;/p&gt;

&lt;p&gt;Some of the most authentic, developer-focused DevRel professionals I know are also fluent in business language. They use that fluency to advocate more effectively for developers. They're bilingual, not sell-outs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now You're Ready for the Next Level
&lt;/h2&gt;

&lt;p&gt;With the foundation of business literacy, you can now tackle the more advanced challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developing meaningful DevRel metrics that map to business objectives&lt;/li&gt;
&lt;li&gt;Building attribution models that give you credit for influence without overclaiming&lt;/li&gt;
&lt;li&gt;Creating business cases for new DevRel initiatives&lt;/li&gt;
&lt;li&gt;Demonstrating ROI in ways that are credible to finance teams&lt;/li&gt;
&lt;li&gt;Advocating for budget and headcount using compelling business logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These advanced topics deserve their own deep dives (and there are good resources out there on these topics). But without the foundation we've built in this series, those advanced topics don't make sense. You'd be trying to build a measurement framework without understanding what matters to the business. You'd be creating attribution models without understanding how marketing thinks about attribution. You'd be requesting budget without speaking the language of finance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work as Imagined vs Work as Done
&lt;/h2&gt;

&lt;p&gt;There's this concept from safety science: "work as imagined" vs "work as done." &lt;strong&gt;Work as imagined&lt;/strong&gt; is how things are supposed to work in theory. &lt;strong&gt;Work as done&lt;/strong&gt; is the messy reality of how things actually work.&lt;/p&gt;

&lt;p&gt;A lot of the DevRel discourse happens in "work as imagined" territory. In theory, we should just be able to show our impact through beautiful metrics. In theory, executives should just trust that DevRel is valuable. In theory, we shouldn't have to speak business language because our work should speak for itself.&lt;/p&gt;

&lt;p&gt;But that's not work as done. In reality, we operate in companies with finite resources, competing priorities, and stakeholders who think in different frameworks. In reality, business literacy is table stakes for being taken seriously.&lt;/p&gt;

&lt;p&gt;Focus on work as done. Learn the language. Have the conversations. Bridge the gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Network Is Vast: DevRel as BizDev
&lt;/h2&gt;

&lt;p&gt;Here's one more thing to consider: your network is one of your most valuable assets, both personally and for your company.&lt;/p&gt;

&lt;p&gt;You know people at other companies. You have relationships in the developer community. You're connected to potential partners, customers, and advocates.&lt;/p&gt;

&lt;p&gt;This makes you valuable in ways that go beyond traditional DevRel metrics. Want to explore a partnership with another company? You probably know someone there. Need to get into a strategic account? Your network can probably help with an introduction.&lt;/p&gt;

&lt;p&gt;I'm not saying you should become a business development person. But I am saying that leveraging your network thoughtfully (and with appropriate boundaries) is another way DevRel creates business value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show Value But Set Boundaries
&lt;/h2&gt;

&lt;p&gt;As you become more business-literate and better at articulating your value, you'll likely get more requests. Sales will want you on more calls. Marketing will want you to support more campaigns. Executives will want you to do more things that "require less imagination to show value."&lt;/p&gt;

&lt;p&gt;This is where boundaries matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I'll call in a favor from my network - once."&lt;/li&gt;
&lt;li&gt;"I'll share company news on social media, but in my own words and only if I genuinely think it's interesting."&lt;/li&gt;
&lt;li&gt;"I'll meet with prospects for technical discussions, but I won't do sales demos or pitch."&lt;/li&gt;
&lt;li&gt;"I'll write about new features after I've used them, and I'll share my honest impression."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your boundaries might be different. The key is being intentional about what you will and won't do. And remember: boundaries can be flexible and situational. But they should be conscious choices, not things that happen to you.&lt;/p&gt;

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

&lt;p&gt;Business literacy is power. It's the power to advocate effectively. The power to protect your team. The power to fund the programs that serve developers. The power to have a seat at the strategic table.&lt;/p&gt;

&lt;p&gt;You don't need to become a finance expert or a sales leader or a marketing strategist. But you need to understand enough about how these functions work to translate between their world and yours.&lt;/p&gt;

&lt;p&gt;The cognitive dissonance a lot of DevRel folks feel - between "DevRel is good and pure and unsullied by capitalism" and "the company has to generate revenue and DevRel is part of that" - that dissonance is real. But it's also false tension.&lt;/p&gt;

&lt;p&gt;You can serve developers AND contribute to business success. You can be authentic AND speak business language. You can maintain your integrity AND work effectively with sales and marketing.&lt;/p&gt;

&lt;p&gt;In fact, you kind of have to. Because if you're not connected in a measurable way to either building the thing or selling the thing, it's going to be really hard to stick around for the encore.&lt;/p&gt;

&lt;p&gt;Get over yourself. Learn the language. Bridge the gap. And go be awesome...together with the rest of your company.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources for Continued Learning
&lt;/h2&gt;

&lt;p&gt;Want to deepen your business literacy? Here are some places to start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take a basic business finance course (lots of free options on Coursera, edX)&lt;/li&gt;
&lt;li&gt;Read your company's financial statements (if public) to understand how they talk about the business&lt;/li&gt;
&lt;li&gt;Ask to shadow someone in sales/marketing/finance for a day&lt;/li&gt;
&lt;li&gt;Read business books focused on SaaS/tech companies (I recommend "Crossing the Chasm" by Geoffrey Moore as a starting point)&lt;/li&gt;
&lt;li&gt;Follow finance and business leaders on social media to absorb how they think&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And most importantly: ask questions. The folks in sales, marketing, and finance usually love explaining their world to people who are genuinely curious.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jmeiss.me/" rel="noopener noreferrer"&gt;Jeremy Meiss&lt;/a&gt; has written some great next step posts for you to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jmeiss.me/posts/devrel-and-the-customer-journey/" rel="noopener noreferrer"&gt;DevRel and the Customer Journey&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jmeiss.me/posts/positioning-devrel-as-a-resource/" rel="noopener noreferrer"&gt;Positioning DevRel as a Resource&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jmeiss.me/posts/measuring-devrel-impact-on-revenue/" rel="noopener noreferrer"&gt;Measuring the impact of Developer Relations on Revenue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next For You?
&lt;/h2&gt;

&lt;p&gt;So here's my question for you: what's one thing you're going to do differently based on this series?&lt;/p&gt;

&lt;p&gt;Maybe you're going to schedule coffee with someone from your sales team to understand their pipeline better. Maybe you're going to reframe how you talk about your work in your next stakeholder meeting. Maybe you're going to actually read through your company's budget process documentation (I know, thrilling stuff).&lt;/p&gt;

&lt;p&gt;Whatever it is, don't let this just be information you consumed. Make it actionable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you want to continue this conversation, I'm always available on &lt;a href="https://hachyderm.io/@mattstratton" rel="noopener noreferrer"&gt;Mastodon&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/mattstratton/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, and &lt;a href="https://bsky.app/profile/matty.wtf" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt;. Share your experiences, your questions, your disagreements. This is work in progress for all of us.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks for sticking with this series. Now go forth and speak business fluently while never forgetting who you're really here to serve: the developers.&lt;/p&gt;

&lt;p&gt;Build the thing. Sell the thing. But most importantly, make developers' lives better. That's the job.&lt;/p&gt;

</description>
      <category>devrel</category>
    </item>
    <item>
      <title>Product-Led Growth and What It Means for DevRel</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Tue, 28 Oct 2025 18:54:05 +0000</pubDate>
      <link>https://dev.to/mattstratton/product-led-growth-and-what-it-means-for-devrel-10mj</link>
      <guid>https://dev.to/mattstratton/product-led-growth-and-what-it-means-for-devrel-10mj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 5 of my 7-part series on business literacy for DevRel. &lt;a href="https://dev.to/mattstratton/why-business-literacy-matters-for-devrel-and-why-you-cant-skip-this-step-30p1"&gt;Start with Part 1 if you missed it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We've covered sales, marketing, and finance - the three core business functions you need to understand. But there's one more concept that ties everything together, and it's especially critical if you work at a developer tools company or any company with a self-serve product: &lt;strong&gt;Product-Led Growth&lt;/strong&gt; (PLG).&lt;/p&gt;

&lt;p&gt;If you've been around tech for the past five years, you've definitely heard this term thrown around. And if you're like most people, you might have a vague sense that it means "the product sells itself" but aren't totally clear on what that actually means or how DevRel fits in.&lt;/p&gt;

&lt;p&gt;This is the missing piece. Because understanding PLG fundamentally changes how you think about the relationship between DevRel, sales, marketing, and product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Product-Led Growth Actually Means
&lt;/h2&gt;

&lt;p&gt;Product-Led Growth is a go-to-market strategy where the product itself is the primary driver of customer acquisition, expansion, and retention. Instead of relying heavily on sales teams to convince people to buy, the product experience convinces them.&lt;/p&gt;

&lt;p&gt;Think about how you adopted tools like Slack, Figma, or GitHub. You probably didn't talk to a sales rep first. You or someone on your team just started using it. The product was good enough that you kept using it. Eventually you needed more features or more seats, so you paid for it. Maybe your whole company ended up adopting it, even though it started with one team.&lt;/p&gt;

&lt;p&gt;That's PLG in action.&lt;/p&gt;

&lt;p&gt;In a traditional sales-led model, the sequence is: Sales → Trial → Adoption → Payment&lt;br&gt;&lt;br&gt;
In a product-led model, it's: Product → Adoption → Payment → Sales (maybe)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PLG Matters (Especially For Developer Tools)
&lt;/h2&gt;

&lt;p&gt;Developers hate being sold to. You know this. I know this. We all know this.&lt;/p&gt;

&lt;p&gt;Developers want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try things before buying&lt;/li&gt;
&lt;li&gt;Evaluate on their own terms, not on a sales rep's calendar&lt;/li&gt;
&lt;li&gt;Get hands-on experience without talking to anyone&lt;/li&gt;
&lt;li&gt;Make technical decisions based on actual experience, not marketing claims&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why so many successful developer tools companies use PLG. It aligns with how developers actually want to evaluate and adopt tools.&lt;/p&gt;

&lt;p&gt;Companies like Stripe, Twilio, HashiCorp, MongoDB, and countless others have proven that PLG works incredibly well for developer products. And it's a model where DevRel can be absolutely critical to success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PLG Funnel (It's Different)
&lt;/h2&gt;

&lt;p&gt;In a traditional sales-led company, the funnel looks like:&lt;br&gt;&lt;br&gt;
Awareness → Lead → MQL → SQL → Opportunity → Customer&lt;/p&gt;

&lt;p&gt;In a PLG company, it's more like:&lt;br&gt;&lt;br&gt;
Awareness → Sign-up → Activation → Active Usage → Power User → Paying Customer → Expansion&lt;/p&gt;

&lt;p&gt;Let's break down these stages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign-up&lt;/strong&gt;: User creates an account. This is easy - usually just an email address. The barrier is intentionally low.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activation&lt;/strong&gt;: User completes the critical actions that demonstrate they "get" the product. For GitHub, this might be creating their first repository. For Stripe, it's making a test API call. This is often called the "aha moment."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active Usage&lt;/strong&gt;: User is regularly using the product and getting value from it. They've integrated it into their workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Power User&lt;/strong&gt;: User is deeply engaged, using advanced features, bringing in teammates. They're getting significant value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paying Customer&lt;/strong&gt;: User hits some limit (features, usage, seats) and converts to paid. Critically, this often happens without ever talking to sales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expansion&lt;/strong&gt;: Customer uses more features, adds more users, upgrades tiers. Again, often self-serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DevRel Fits in PLG
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting for you: DevRel is absolutely critical at almost every stage of the PLG funnel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Awareness Stage
&lt;/h3&gt;

&lt;p&gt;This is classic DevRel territory. Your conference talks, blog posts, podcasts, social media presence - all of this drives awareness. The difference in PLG is that you're not trying to generate leads for sales; you're trying to get people to sign up and try the product.&lt;/p&gt;

&lt;p&gt;Your call-to-action isn't "talk to sales" - it's "try it yourself right now."&lt;/p&gt;

&lt;h3&gt;
  
  
  Sign-up to Activation (The Critical Gap)
&lt;/h3&gt;

&lt;p&gt;This is where a LOT of PLG companies struggle. People sign up, poke around, don't understand the value, and leave. The activation rate might be 20-30%.&lt;/p&gt;

&lt;p&gt;DevRel can massively impact this through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Great documentation that gets people to their "aha moment" fast&lt;/li&gt;
&lt;li&gt;Quick-start guides and tutorials&lt;/li&gt;
&lt;li&gt;Sample projects and templates&lt;/li&gt;
&lt;li&gt;Video walkthroughs&lt;/li&gt;
&lt;li&gt;Office hours where people can get unstuck&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're essentially reducing time-to-value. The faster someone gets value, the more likely they activate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Active Usage to Power User
&lt;/h3&gt;

&lt;p&gt;Once someone is using the product, DevRel keeps them engaged and helps them go deeper:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced tutorials and use cases&lt;/li&gt;
&lt;li&gt;Community where they can learn from peers&lt;/li&gt;
&lt;li&gt;Content about best practices and patterns&lt;/li&gt;
&lt;li&gt;Events and workshops that teach advanced features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're helping users get more value from the product, which increases stickiness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power User to Paying Customer
&lt;/h3&gt;

&lt;p&gt;This conversion often happens naturally when users hit limits, but DevRel can influence it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Case studies showing how others use paid features&lt;/li&gt;
&lt;li&gt;Content that highlights the value of premium capabilities&lt;/li&gt;
&lt;li&gt;Community members sharing their experiences with paid tiers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're not selling, but you're helping people understand when and why to upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paying Customer to Expansion
&lt;/h3&gt;

&lt;p&gt;DevRel helps customers expand their usage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Training on enterprise features&lt;/li&gt;
&lt;li&gt;Best practices for scaling&lt;/li&gt;
&lt;li&gt;Community connections that show what's possible&lt;/li&gt;
&lt;li&gt;Advocacy that keeps them engaged and excited&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  PLG Metrics That Matter to DevRel
&lt;/h2&gt;

&lt;p&gt;In a PLG company, the metrics you care about shift a bit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign-ups&lt;/strong&gt;: How many people are creating accounts? Your awareness activities drive this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activation Rate&lt;/strong&gt;: What percentage of sign-ups reach the "aha moment"? Your onboarding content affects this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time to Value&lt;/strong&gt;: How quickly do users get value? Your documentation and tutorials impact this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Qualified Leads (PQLs)&lt;/strong&gt;: Users who have demonstrated high engagement and fit the profile for conversion to paid. This is PLG's version of an MQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free-to-Paid Conversion Rate&lt;/strong&gt;: What percentage of free users convert to paid? Your content about premium features influences this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net Revenue Retention (NRR)&lt;/strong&gt;: Are existing customers expanding their usage? Your community and advanced content affect this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community Engagement&lt;/strong&gt;: Active community members are more likely to convert and expand. Your community building directly impacts this.&lt;/p&gt;

&lt;p&gt;The beautiful thing about PLG is that DevRel activities have clearer, more direct metrics. You can often see: this piece of content led to X sign-ups, Y of whom activated, and Z of whom converted to paid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sales-Assist Model (PLG + Sales)
&lt;/h2&gt;

&lt;p&gt;Here's where it gets nuanced: most PLG companies still have sales teams. They just work differently.&lt;/p&gt;

&lt;p&gt;In a PLG company, sales typically does one of two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Sales-Assist for Expansion&lt;/strong&gt;: Free users become paid users on their own, but when they want to expand significantly (more seats, enterprise features), sales gets involved to help close bigger deals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Land-and-Expand&lt;/strong&gt;: Sales might not touch small deals at all, but once an account reaches a certain size or shows certain behaviors, sales reaches out to help them expand.&lt;/p&gt;

&lt;p&gt;This is sometimes called "Product-Led Sales" - sales that's triggered by product usage signals, not traditional lead gen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means for DevRel in a PLG Company
&lt;/h2&gt;

&lt;p&gt;If you work at a PLG company (or your company is moving toward PLG), your role is even more critical:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're a growth driver, not just brand building&lt;/strong&gt;: In traditional sales-led companies, DevRel is often seen as brand/awareness. In PLG companies, DevRel directly impacts the metrics that matter - sign-ups, activation, and conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation and onboarding are strategic&lt;/strong&gt;: These aren't afterthoughts; they're core to the business model. Investing in great docs isn't nice-to-have; it's mission-critical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community is a growth engine&lt;/strong&gt;: Active community members are more likely to convert, expand, and bring in new users. Community building isn't just feel-good work; it's a competitive advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your content directly impacts the funnel&lt;/strong&gt;: That tutorial you wrote? It's not just TOFU content - it might be what gets someone from sign-up to activation. That's directly impacting conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You work more closely with product&lt;/strong&gt;: In PLG, the line between product and go-to-market blurs. You need to work closely with product teams to ensure the product experience supports the PLG motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  PLG Changes the Business Literacy Conversation
&lt;/h2&gt;

&lt;p&gt;Remember all that stuff we talked about in earlier posts about explaining your value? In PLG companies, it's often easier because the connection is more direct:&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Sales&lt;/strong&gt;: "DevRel is filling the top of the funnel and helping users get to PQL status faster, which gives you higher-quality accounts to work with."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Marketing&lt;/strong&gt;: "DevRel is driving sign-ups and activation, which are our primary growth metrics. We're also reducing time-to-value, which improves our conversion rates."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Finance&lt;/strong&gt;: "DevRel investments directly correlate with sign-up growth and activation improvements. Here's the data showing how tutorial usage maps to activation rate."&lt;/p&gt;

&lt;p&gt;To &lt;strong&gt;Product&lt;/strong&gt;: "DevRel is hearing user feedback at scale and seeing where people get stuck in the onboarding flow. This intelligence helps you improve the product experience."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dark Side of PLG (That Nobody Talks About)
&lt;/h2&gt;

&lt;p&gt;PLG isn't magic. It has challenges:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Free User Problem&lt;/strong&gt;: In PLG, you have tons of free users who may never convert. Supporting them costs money. DevRel needs to help free users get enough value to convert, without providing so much support that it's not sustainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activation is Hard&lt;/strong&gt;: Most people who sign up for a product never activate. This can be demoralizing if your sign-up numbers look great but activation is terrible. DevRel can help, but can't fix a fundamentally confusing product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-Serve Isn't Always Better&lt;/strong&gt;: Some products are complex enough that they need human help. Forcing a pure PLG motion when the product isn't ready for it doesn't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support Burden&lt;/strong&gt;: When you have thousands of free users, the support and community management burden can be enormous. DevRel often ends up carrying this, and it needs resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional Sales-Led vs PLG: Not Either/Or
&lt;/h2&gt;

&lt;p&gt;Here's the thing: most companies aren't purely PLG or purely sales-led. They're hybrid.&lt;/p&gt;

&lt;p&gt;You might have a free tier and self-serve paid tier (PLG), but also have a sales team for enterprise deals (sales-led). This is really common.&lt;/p&gt;

&lt;p&gt;Or you might start PLG for small teams, but sales gets involved when an account grows to a certain size.&lt;/p&gt;

&lt;p&gt;Understanding both models - and how they work together - makes you more effective. You can support the PLG motion for individual developers and small teams while also supporting sales when they're working with larger accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Think About PLG as a DevRel Professional
&lt;/h2&gt;

&lt;p&gt;If your company is PLG or moving toward PLG:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embrace it&lt;/strong&gt;: PLG aligns really well with DevRel values. Letting the product speak for itself, empowering users to evaluate on their own terms, removing barriers - this is all good stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get closer to product data&lt;/strong&gt;: Work with your product team to understand metrics like activation rate, time-to-value, and conversion funnel. Ask for access to dashboards. Understand what users are actually doing in the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus on onboarding&lt;/strong&gt;: The period between sign-up and activation is critical. Invest heavily in making this smooth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build feedback loops&lt;/strong&gt;: PLG gives you tons of data about what's working and what's not. Use it to improve docs, content, and even influence product decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Think about the full journey&lt;/strong&gt;: Don't just focus on awareness. Think about how your work impacts every stage from sign-up to expansion.&lt;/p&gt;

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

&lt;p&gt;Product-Led Growth isn't a replacement for sales or marketing. It's a different go-to-market motion that changes how everything works, including DevRel.&lt;/p&gt;

&lt;p&gt;In PLG companies, DevRel isn't just "nice to have" - it's essential infrastructure. Great docs aren't a nice-to-have; they're what makes the PLG motion work. Active community isn't just engagement; it's what drives retention and expansion.&lt;/p&gt;

&lt;p&gt;Understanding PLG helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Position your work more strategically&lt;/li&gt;
&lt;li&gt;Focus on the right activities&lt;/li&gt;
&lt;li&gt;Measure impact more directly&lt;/li&gt;
&lt;li&gt;Collaborate better with product, marketing, and sales&lt;/li&gt;
&lt;li&gt;Advocate for resources more effectively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're at a PLG company and you haven't been thinking about your work through this lens, start now. And if you're at a traditional sales-led company but serve developers, consider whether some PLG principles could make your work more effective.&lt;/p&gt;

&lt;p&gt;Either way, understanding PLG is part of business literacy for modern DevRel. Add it to your toolkit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Go Deeper?
&lt;/h2&gt;

&lt;p&gt;Some resources on PLG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;&lt;a href="https://www.amazon.com/Product-Led-Growth-Build-Product-Itself/dp/1798434520" rel="noopener noreferrer"&gt;Product-Led Growth&lt;/a&gt;&lt;/em&gt; by Wes Bush (book)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openviewpartners.com/product-led-growth/" rel="noopener noreferrer"&gt;OpenView's PLG resources&lt;/a&gt; (they coined the term)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=cJY1Pabz6q8" rel="noopener noreferrer"&gt;Is Product Led Growth the DevOps of DevRel?&lt;/a&gt; (Daniel Bryant's talk at DevRelCon 2022)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up, we're bringing it all together in the final post - how to actually use this business literacy in your day-to-day DevRel work. How to translate what you do into language that resonates with stakeholders. How to speak business without losing your soul.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;As always, comments are open below if you want to discuss, debate, or share your PLG experiences.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Previously:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/product-101-your-secret-weapon-for-understanding-the-business-2m6j9"&gt;Part 5: Product 101&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/putting-it-all-together-speaking-business-as-a-devrel-professional-2kfj"&gt;Wrapping it all up&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devr</category>
    </item>
    <item>
      <title>Finance 101: Budgets, P&amp;Ls, and the Language of Money</title>
      <dc:creator>Matty Stratton</dc:creator>
      <pubDate>Tue, 28 Oct 2025 18:53:35 +0000</pubDate>
      <link>https://dev.to/mattstratton/finance-101-budgets-pls-and-the-language-of-money-3dp9</link>
      <guid>https://dev.to/mattstratton/finance-101-budgets-pls-and-the-language-of-money-3dp9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 4 of my 7-part series on business literacy for DevRel. &lt;a href="https://dev.to/mattstratton/why-business-literacy-matters-for-devrel-and-why-you-cant-skip-this-step-30p1"&gt;Start with Part 1 if you missed it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Time for everyone's favorite topic: money. Specifically, how companies think about money, track money, and make decisions about money.&lt;/p&gt;

&lt;p&gt;If your eyes are already glazing over, I get it. When I was first a sysadmin, I thought finance was just "the people who make it difficult to buy the things I need." As a DevRel professional, you might think finance is "the people who keep asking me to justify my conference budget."&lt;/p&gt;

&lt;p&gt;However, understanding finance might be the single most important business skill you develop. Because at the end of the day, no matter how amazing your DevRel work is, if you can't speak the language of money, you're going to struggle to advocate for your team, defend your budget, and demonstrate your value.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Finance Actually Does
&lt;/h2&gt;

&lt;p&gt;Finance manages the company's money. All of it. They track where money comes from (revenue) and where it goes (expenses). They plan for the future (budgeting). They make sure the company stays financially healthy. They report to investors or the board on the company's financial performance.&lt;/p&gt;

&lt;p&gt;But finance isn't just about counting beans (though there is some bean counting). Strategic finance helps the company make smart decisions about where to invest resources. They're asking questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Should we hire more sales people or invest in marketing?"&lt;/li&gt;
&lt;li&gt;"Can we afford to expand to a new market?"&lt;/li&gt;
&lt;li&gt;"Which initiatives are giving us the best return on investment?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a company to survive, revenue needs to exceed expenses. Finance's job is to make sure that happens (or at least make sure the company is on a path to make it happen, because early-stage companies often operate at a loss while they're growing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the Finance Team
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CFO (Chief Financial Officer)&lt;/strong&gt;: The top finance executive. They own the overall financial strategy and health of the company. They report to the CEO and often the board. When big financial decisions get made, the CFO is in the room.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VP Finance / Controller&lt;/strong&gt;: Usually manages day-to-day finance operations. They might oversee accounting, financial planning, and the finance team members.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FP&amp;amp;A (Financial Planning &amp;amp; Analysis)&lt;/strong&gt;: These folks do the budgeting, forecasting, and financial modeling. When you submit your budget request, FP&amp;amp;A is reviewing it. When the company is trying to figure out if they can afford something, FP&amp;amp;A is running the numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounting Team&lt;/strong&gt;: They track and record all financial transactions, make sure bills get paid, ensure compliance with financial regulations, and generally keep the books in order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How DevRel Interacts With Finance&lt;/strong&gt;: Mostly through budget requests, expense management, and ROI discussions. Unless your company is really small, you probably won't interact with the CFO regularly, but you might work with FP&amp;amp;A on planning and budgets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the P&amp;amp;L (Profit and Loss Statement)
&lt;/h2&gt;

&lt;p&gt;A P&amp;amp;L is basically a report card for how the company is doing financially. Here's the simple version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue&lt;/strong&gt; (money coming in)&lt;br&gt;&lt;br&gt;
minus &lt;strong&gt;Expenses&lt;/strong&gt; (money going out)&lt;br&gt;&lt;br&gt;
equals &lt;strong&gt;Profit or Loss&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If revenue is bigger than expenses, you have profit. If expenses are bigger than revenue, you have a loss.&lt;/p&gt;

&lt;p&gt;Now let's break down the expense side, because that's where you live:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost of Goods Sold (COGS)&lt;/strong&gt;: The direct costs of delivering your product or service. For SaaS companies, this is things like cloud infrastructure, hosting costs, and support. Generally, you want this to be relatively low compared to revenue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operating Expenses (OpEx)&lt;/strong&gt;: Everything else it costs to run the business:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sales &amp;amp; Marketing&lt;/strong&gt;: Sales team salaries, marketing programs, advertising, events, tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Research &amp;amp; Development (R&amp;amp;D)&lt;/strong&gt;: Engineering team, product team, technical infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General &amp;amp; Administrative (G&amp;amp;A)&lt;/strong&gt;: Finance, HR, legal, executive team, office costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DevRel usually shows up in Sales &amp;amp; Marketing or sometimes in R&amp;amp;D, depending on where you report. Either way, you're an operating expense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Center vs Profit Center
&lt;/h3&gt;

&lt;p&gt;Most DevRel teams are cost centers - meaning you cost money but don't directly generate revenue. Sales is usually a profit center because they directly bring in revenue (though they also cost money in salaries and commissions).&lt;/p&gt;

&lt;p&gt;Being a cost center isn't bad! Most of the company is cost centers. But it does mean that during budget cuts, cost centers are scrutinized more heavily. This is why being able to articulate your value in business terms matters so much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budgeting: The Annual Stress Fest
&lt;/h2&gt;

&lt;p&gt;Budgeting is the process of planning how much money each part of the company gets to spend. Most companies do annual budgets, often with quarterly reviews and adjustments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Budgeting Cycle&lt;/strong&gt; usually goes something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leadership sets revenue targets and high-level spending parameters&lt;/li&gt;
&lt;li&gt;Each department builds a budget showing what they need to spend to hit their goals&lt;/li&gt;
&lt;li&gt;Finance reviews all the requests (which always add up to more than the company can afford)&lt;/li&gt;
&lt;li&gt;Negotiation happens. Priorities get set. Cuts get made.&lt;/li&gt;
&lt;li&gt;Final budget gets approved&lt;/li&gt;
&lt;li&gt;Everyone complains it's not enough (it never is)&lt;/li&gt;
&lt;li&gt;Throughout the year, actual spending gets tracked against the budget&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Top-Down vs Bottom-Up Budgeting&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-down: Leadership says "Sales &amp;amp; Marketing gets $5M" and then that team figures out how to allocate it&lt;/li&gt;
&lt;li&gt;Bottom-up: Each team builds their ideal budget and submits it up the chain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most companies do some combination. You might be told "DevRel has a budget of $X" or you might be asked "What do you need for next year?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a DevRel Budget
&lt;/h3&gt;

&lt;p&gt;Your budget probably includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headcount&lt;/strong&gt;: Salaries and benefits for your team (usually your biggest expense by far)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Programs&lt;/strong&gt;: Events, sponsorships, community programs, swag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: Software subscriptions, community platforms, content creation tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Travel&lt;/strong&gt;: Flights, hotels, meals for conferences and events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content Production&lt;/strong&gt;: Video production, freelance writers, design services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you build a budget, you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tie it to goals&lt;/strong&gt;: "We need $X to do Y, which will result in Z outcome"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show your math&lt;/strong&gt;: "Three conferences at $10K each = $30K"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize&lt;/strong&gt;: What's must-have vs nice-to-have&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be realistic&lt;/strong&gt;: Under-budgeting doesn't help anyone&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And here's what nobody tells you: &lt;strong&gt;always expect cuts&lt;/strong&gt;. Budget a little high if you can, knowing you'll probably need to trim. But don't budget ridiculous things you don't actually need, because that damages your credibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Your Budget Throughout The Year
&lt;/h3&gt;

&lt;p&gt;Once you have a budget, you need to track it. Finance is definitely tracking it, and you should be too. Know at any given time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much you've spent&lt;/li&gt;
&lt;li&gt;How much you have left&lt;/li&gt;
&lt;li&gt;What big expenses are coming&lt;/li&gt;
&lt;li&gt;Whether you're on track or over/under budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're going to go over budget, flag it early. Finance hates surprises. If you're going to be way under budget, that's also worth flagging - sometimes you can reallocate or carry forward savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential Finance Terminology
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CapEx vs OpEx&lt;/strong&gt;: This is a big one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CapEx (Capital Expenditure)&lt;/strong&gt;: Large purchases that are assets - things like equipment, buildings, major infrastructure. These get depreciated over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpEx (Operating Expenditure)&lt;/strong&gt;: Day-to-day operating costs like salaries, subscriptions, travel, programs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most DevRel spending is OpEx. Why does this matter? Because CapEx and OpEx are treated differently for accounting and tax purposes, and companies often have different budgets for each.&lt;/p&gt;

&lt;p&gt;If you want to buy a $50K piece of video production equipment, that's CapEx and comes from a different bucket than your $50K event sponsorship (OpEx). Understanding this helps you navigate budget conversations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EBITDA&lt;/strong&gt;: Earnings Before Interest, Taxes, Depreciation, and Amortization. This is a measure of operational profitability. Basically, it's trying to show how much cash the core business operations are generating.&lt;/p&gt;

&lt;p&gt;Why should you care? Because companies that aren't profitable yet but are showing strong EBITDA growth are demonstrating they're on a path to profitability. This affects investment decisions and budget allocations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Burn Rate&lt;/strong&gt;: How quickly a company is spending money. Especially relevant for startups. If you're spending $1M/month and bringing in $500K/month, your burn rate is $500K/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runway&lt;/strong&gt;: How long the company can operate at current burn rate before running out of money. If you have $6M in the bank and a burn rate of $500K/month, you have 12 months of runway.&lt;/p&gt;

&lt;p&gt;Why this matters to you: when runway gets short, budget cuts happen. Understanding runway gives you early warning signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CAC (Customer Acquisition Cost)&lt;/strong&gt;: The total cost to acquire a new customer. Add up all your Sales &amp;amp; Marketing spend and divide by number of new customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LTV (Lifetime Value)&lt;/strong&gt;: The total revenue you expect from a customer over their entire relationship with you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LTV:CAC Ratio&lt;/strong&gt;: This is super important. If it costs you $10,000 to acquire a customer (CAC) and they generate $30,000 in revenue over their lifetime (LTV), your ratio is 3:1.&lt;/p&gt;

&lt;p&gt;Generally, you want this ratio to be 3:1 or better. Lower than 3:1 and you're spending too much on acquisition. Higher than 3:1 and you might not be spending enough (you could grow faster).&lt;/p&gt;

&lt;p&gt;Why DevRel should care: If you can show that DevRel activities reduce CAC or increase LTV, you're speaking directly to metrics that executives obsess about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gross Margin&lt;/strong&gt;: Revenue minus COGS, expressed as a percentage. If you have $100 in revenue and $30 in COGS, your gross margin is 70%. SaaS companies typically have gross margins of 70-80%+.&lt;/p&gt;

&lt;p&gt;Why this matters: High gross margin means you have more money available for operating expenses (like DevRel). If margins are shrinking, operating budgets get squeezed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How DevRel Costs Are Viewed
&lt;/h2&gt;

&lt;p&gt;Let's be honest about how finance sees DevRel:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headcount&lt;/strong&gt; is usually your biggest cost. For example, if you have a team of 5 people with an average total compensation (salary + benefits) of $150K, that's $750K/year just in people costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Programs&lt;/strong&gt; are visible and often questioned. That $50K conference sponsorship? Finance is asking "What do we get for that?" The more clearly you can connect program spend to outcomes, the easier budget conversations get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt; are usually easier to justify, especially if you can show ROI. A $10K/year community platform that supports 10,000 active developers is pretty easy math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Travel&lt;/strong&gt; is often the first thing to get cut in tough times. It's visible, feels expensive, and is easy to reduce. This is why many DevRel budgets got decimated during COVID.&lt;/p&gt;

&lt;p&gt;Finance asks questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What's the return on this investment?"&lt;/li&gt;
&lt;li&gt;"Could we get similar outcomes spending less?"&lt;/li&gt;
&lt;li&gt;"Is this must-have or nice-to-have?"&lt;/li&gt;
&lt;li&gt;"Can we measure the impact?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The better you can answer these questions, the easier your budget conversations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Truth About Budget Cuts
&lt;/h2&gt;

&lt;p&gt;When companies need to reduce costs, they look at what they can cut with the least immediate impact on revenue. Unfortunately, DevRel often falls into this category because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The connection to revenue isn't always direct or immediate&lt;/li&gt;
&lt;li&gt;The impact of cutting DevRel isn't felt right away&lt;/li&gt;
&lt;li&gt;It's easy to say "we'll just stop doing conferences for a year"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why business literacy matters. If you can articulate DevRel's impact in financial terms - "DevRel activities influenced $2M in pipeline" or "Our community reduces support costs by $X" - you're much better positioned to defend your budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making The Business Case
&lt;/h2&gt;

&lt;p&gt;When you need to advocate for budget, you need to speak finance's language:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect to revenue&lt;/strong&gt;: "This program helps accelerate deals by X days, which means we close Y more deals per quarter"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show efficiency&lt;/strong&gt;: "This investment of $X reduces costs in other areas by $Y"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demonstrate ROI&lt;/strong&gt;: "Last year we spent $X on this and it resulted in Y measurable outcome worth $Z"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frame in terms of risk&lt;/strong&gt;: "Not investing in this creates risk of A, B, C"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use benchmarks&lt;/strong&gt;: "Industry standard for companies our size is X, we're at Y"&lt;/p&gt;

&lt;p&gt;You don't have to have perfect data for all of this. But you need to be able to tell a credible story using numbers and business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Finance Wants From You
&lt;/h2&gt;

&lt;p&gt;Honestly? Finance wants you to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stay on budget&lt;/strong&gt; - Don't surprise them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track your spending&lt;/strong&gt; - Know your numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Justify requests&lt;/strong&gt; - Explain the why&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show impact&lt;/strong&gt; - Connect to business outcomes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be realistic&lt;/strong&gt; - Don't ask for things you can't defend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finance people aren't the enemy. They're trying to make sure the company doesn't run out of money. Help them understand why DevRel is a good investment, and they'll often advocate for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Means For Your Work
&lt;/h2&gt;

&lt;p&gt;Understanding finance doesn't mean becoming an accountant. It means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can build and defend a budget&lt;/li&gt;
&lt;li&gt;You can have informed conversations with finance leadership&lt;/li&gt;
&lt;li&gt;You can speak credibly about financial trade-offs&lt;/li&gt;
&lt;li&gt;You can frame DevRel's value in financial terms&lt;/li&gt;
&lt;li&gt;You can see early warning signals when budget cuts might be coming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look, finance isn't the hippest part of business literacy. But it might be the most important. Because at the end of the day, companies are resource-constrained, and the teams that can effectively advocate for resources in financial terms are the ones that thrive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Questions about budgets? Confused about the difference between CapEx and OpEx? Want to share your budget horror stories? Comments are open.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Previously:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/marketing-101-funnels-campaigns-and-what-marketing-actually-means-4j81"&gt;Part 3: Marketing 101&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up:&lt;/strong&gt; &lt;a href="https://dev.to/mattstratton/product-101-your-secret-weapon-for-understanding-the-business-2m6j"&gt;Product 101&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devrel</category>
    </item>
  </channel>
</rss>
