<?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: Mujib</title>
    <description>The latest articles on DEV Community by Mujib (@mujib77).</description>
    <link>https://dev.to/mujib77</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%2F3921321%2F6034e2d1-e80b-44fa-ace6-25a959985cf1.png</url>
      <title>DEV Community: Mujib</title>
      <link>https://dev.to/mujib77</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mujib77"/>
    <language>en</language>
    <item>
      <title>Building a Kafka-free CDC Pipeline in Go (and the DDL bug that almost broke my brain)</title>
      <dc:creator>Mujib</dc:creator>
      <pubDate>Sat, 18 Jul 2026 05:52:43 +0000</pubDate>
      <link>https://dev.to/mujib77/building-a-kafka-free-cdc-pipeline-in-go-and-the-ddl-bug-that-almost-broke-my-brain-203</link>
      <guid>https://dev.to/mujib77/building-a-kafka-free-cdc-pipeline-in-go-and-the-ddl-bug-that-almost-broke-my-brain-203</guid>
      <description>&lt;p&gt;So I've been obsessed with Postgres internals for a while now - WAL, replication slots, MVCC, all of it. I built pgstream a while back just to read the WAL and stream changes in real time, and it kind of took off (34 stars, got mentioned on Golang News Twitter which honestly made my whole week). But pgstream was just a reader. It didn't actually do anything with the data besides print it or forward it raw.&lt;/p&gt;

&lt;p&gt;So I started building Rift. The idea was simple: I wanted a single Go binary that reads Postgres WAL changes and ships them to wherever you want - a webhook, another Postgres database, Redis - without needing Kafka, Zookeeper, Debezium, or any of that JVM-based infra that honestly feels like overkill for 90% of side projects and even a lot of production use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Why not just use Debezium?&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look, Debezium is great. It's battle-tested, it's what actual companies run But spinning up Kafka Connect just to sync a couple tables between two Postgres instances feels like using a shipping container to move a backpack. I wanted something you could go install and run in like 30 seconds, with one yaml file.&lt;/p&gt;

&lt;p&gt;So that's what Rift is. Here's roughly what it does:&lt;/p&gt;

&lt;p&gt;~&lt;em&gt;Reads the WAL using logical replication (pglogrepl + pgx, no C bindings, no cgo headaches)&lt;/em&gt;&lt;br&gt;
~&lt;em&gt;Decodes INSERT/UPDATE/DELETE events&lt;/em&gt;&lt;br&gt;
~&lt;em&gt;Optionally filters/transforms them with a JS script (using goja, so no need to shell out to Node)&lt;/em&gt;&lt;br&gt;
~&lt;em&gt;Ships them to one or more destinations - webhook, Postgres, Redis&lt;/em&gt;&lt;br&gt;
~&lt;em&gt;If a destination is down, events get queued to disk (BoltDB) instead of getting dropped or blocking everything&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last part matters a lot more than I expected once I actually started testing it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The DDL bug that ate my weekend
&lt;/h2&gt;

&lt;p&gt;Once the basic INSERT/UPDATE/DELETE pipeline was working, I wanted to go further — track schema changes too. Like if someone runs ALTER TABLE users ADD COLUMN city TEXT on the source db, Rift should know about it, not just silently break next time it tries to sync a row with a new column it's never seen.&lt;/p&gt;

&lt;p&gt;The way I did this was with a Postgres event trigger. Basically:&lt;/p&gt;

&lt;p&gt;Rift installs a function + event trigger in the source database&lt;br&gt;
Any DDL command (CREATE TABLE, ALTER TABLE, CREATE INDEX, whatever) fires the trigger&lt;br&gt;
The trigger writes a row into an internal table called rift_ddl_log&lt;br&gt;
Since rift_ddl_log is a normal table, its INSERTs show up in the WAL too&lt;br&gt;
Rift picks those up and forwards them to destinations as DDL events&lt;/p&gt;

&lt;p&gt;Sounds clean right? Except I completely missed something obvious: the INSERT into rift_ddl_log is itself a WAL event, and Rift was treating it exactly like any other table's insert - meaning it tried to sync rift_ddl_log rows into my destination databases too. Which obviously don't have a rift_ddl_log table. So I'd get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: relation "rift_ddl_log" does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over and over. Every 5 seconds. Forever. Because the failed event kept getting shoved into the disk queue and retried.&lt;/p&gt;

&lt;p&gt;My first fix attempt was dumb - just checking if event.Table == "rift_ddl_log" and skipping it. Didn't work. Turns out the table name coming through was schema-qualified in some paths (public.rift_ddl_log) and not in others, so a strict equality check missed half the cases. Took me embarrassingly long to figure that out because I was staring at the filter logic instead of actually logging what event.Table looked like at the point it failed.&lt;/p&gt;

&lt;p&gt;Fix ended up being a small isInternalTable() helper that checks both exact match and suffix match, so it doesn't matter if the schema prefix is there or not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;isInternalTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;internal&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"rift_ddl_log"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"public.rift_ddl_log"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;internal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EqualFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that was in, rift_ddl_log inserts got filtered out cleanly ([FILTERED] INSERT rift_ddl_log in the logs now, instead of an infinite error loop), while actual schema changes like CREATE TABLE, ALTER TABLE, CREATE INDEX still got captured and forwarded correctly.&lt;/p&gt;

&lt;p&gt;Honestly this was a good reminder that "just add a string check" bugs are rarely actually about the check - they're about not knowing exactly what shape your data is in at that point in the pipeline. Should've logged the raw value first instead of guessing.&lt;/p&gt;

&lt;p&gt;The other thing I learned: DDL isn't the same as schema replication&lt;/p&gt;

&lt;p&gt;While testing this, I hit something that made me realize a gap in the whole design. I inserted a row into a table Rift didn't know about yet, and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error sending to analytics-db: ERROR: relation "users" does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first I thought "wait, didn't the DDL tracking just handle this?" But no — DDL tracking detects and forwards schema changes as metadata (like "hey, someone ran CREATE TABLE users"), it doesn't automatically apply that DDL to the destination database. Those are two very different things. Rift knows the schema changed, but it doesn't (yet) go run CREATE TABLE users (...) on the destination for you.&lt;/p&gt;

&lt;p&gt;Right now that's a real limitation I'm noting honestly in the release notes instead of pretending it's not there. Auto-applying DDL to Postgres destinations is genuinely tricky - you have to worry about column type differences, existing data conflicts, permissions, and probably a dozen edge cases I haven't thought of yet. It's on my list for a future version, but I'd rather ship something honest than something that pretends to do more than it does.&lt;/p&gt;

&lt;p&gt;Making it feel like an actual tool&lt;/p&gt;

&lt;p&gt;For the first while I was just running everything with go run main.go, which is fine for testing but feels pretty amateur if you're trying to get someone else to actually use your project. So I rebuilt the CLI using Cobra — now it's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rift run
rift run &lt;span class="nt"&gt;-c&lt;/span&gt; custom-config.yaml
rift version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of a raw script. go install github.com/mujib77/rift@latest and you're done - no cloning, no build step, it just becomes a command on your machine like git or docker. Small change but it genuinely changes how the project feels, both to use and honestly just to work on.&lt;/p&gt;

&lt;p&gt;Where it's at now&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Rift can currently:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;-Stream INSERT/UPDATE/DELETE from Postgres to webhook, Postgres, or Redis destinations&lt;br&gt;
-Filter/transform events with a JS filter script&lt;br&gt;
-Track DDL changes via event triggers (with the filtering bug above, finally, actually fixed)&lt;br&gt;
-Survive destination downtime using an embedded disk queue, no external broker needed&lt;br&gt;
-Run as a proper installed CLI binary&lt;/p&gt;

&lt;p&gt;It's still early - 1 star as of writing this, so if you're reading this and it sounds useful, go kick the tires and tell me what breaks. That's genuinely the fastest way I learn what's actually wrong with it versus what I assume is wrong with it.&lt;/p&gt;

&lt;p&gt;Repo's here: &lt;a href="https://dev.torift"&gt;https://github.com/mujib77/rift&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've built something similar or have thoughts on how you'd handle the DDL-to-destination sync problem, I'd love to hear it in the comments - still figuring that part out.&lt;a href="https://dev.tourl"&gt;https://github.com/mujib77&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>go</category>
      <category>postgres</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I spent a week researching why devs hate Supabase, LangChain, PostHog &amp; Neon (and what I found)</title>
      <dc:creator>Mujib</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:16:02 +0000</pubDate>
      <link>https://dev.to/mujib77/i-spent-a-week-researching-why-devs-hate-supabase-langchain-posthog-neon-and-what-i-found-6f7</link>
      <guid>https://dev.to/mujib77/i-spent-a-week-researching-why-devs-hate-supabase-langchain-posthog-neon-and-what-i-found-6f7</guid>
      <description>&lt;p&gt;If you hang out on Reddit or Hacker News long enough, you’ll notice a pattern. Every few days someone posts:&lt;/p&gt;

&lt;p&gt;“Supabase RLS is driving me insane”&lt;br&gt;
“LangChain is death by a thousand abstractions”&lt;br&gt;
“Neon cold starts are killing my API response times”&lt;br&gt;
“PostHog autocapture is just noise”&lt;/p&gt;

&lt;p&gt;I’ve used all four tools. And yeah, I’ve felt the pain. But I wanted to know: how bad is it really? Not just vibes, but actual hours lost. So I did something stupid for a student with free time.&lt;/p&gt;

&lt;p&gt;I researched it Properly.&lt;/p&gt;

&lt;p&gt;I read through GitHub issues, Reddit threads, Hacker News comments, and Discord rants. I even used AI to help me aggregate and categorize the complaints. (Yes, AI helped - I’m not hiding that I’m a student, not a journalistic purist.)&lt;/p&gt;

&lt;p&gt;Here’s what I learned -&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

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

&lt;p&gt;Add it up for a team using all four? That’s 40+ hours a week of non-feature work. Basically one engineer just fighting tools.&lt;/p&gt;

&lt;p&gt;And here’s the kicker: a lot of these problems already have solutions. People just don’t know about them.&lt;/p&gt;

&lt;p&gt;Supabase RLS — the biggest trap&lt;br&gt;
Supabase markets itself as “build in a weekend” But then you hit Row Level Security.&lt;/p&gt;

&lt;p&gt;“I know I should learn Postgres better, but I feel misled.” — some Reddit user, speaking for thousands.&lt;/p&gt;

&lt;p&gt;RLS policies aren't just access rules. They're PostgreSQL expressions that run inline with every query. When they fail, you get a generic “permission denied” and zero clue why.&lt;/p&gt;

&lt;p&gt;One engineer said debugging RLS means opening nine different log tabs and manually correlating timestamps. Nine tabs For one bug.&lt;/p&gt;

&lt;p&gt;LangChain — why I’m glad I didn’t build my AI agent with it&lt;br&gt;
I almost used LangChain for Kommit (my AI commit CLI). Thank god I didn’t.&lt;/p&gt;

&lt;p&gt;People describe debugging LangChain as “peeling an onion of abstractions until you cry” A simple API call becomes: PromptTemplate → OutputParser → Chain → AgentExecutor → LCEL pipe operators.&lt;/p&gt;

&lt;p&gt;One team removed LangChain entirely after a year. They said: “We spent as much time understanding LangChain internals as building features”&lt;/p&gt;

&lt;p&gt;The worst part? Hidden LLM calls. One developer reported an agent that called the wrong tool 14 times in a loop and burned $40 of API credits before returning gibberish.&lt;/p&gt;

&lt;p&gt;My take: Use the raw OpenAI/Anthropic SDK unless you really need agent orchestration. LangGraph is better but still complex.&lt;/p&gt;

&lt;p&gt;PostHog — the data cleanup tax&lt;br&gt;
PostHog’s autocapture is both a blessing and a curse It tracks everything Clicks, rageclicks, form submissions, you name it.&lt;/p&gt;

&lt;p&gt;But then you spend hours filtering out the noise&lt;/p&gt;

&lt;p&gt;One GitHub issue I found: a developer was trying to stop $rageclick events from firing when users click up/down arrows on a date picker. Normal behavior Not a rageclick&lt;/p&gt;

&lt;p&gt;PostHog has a “drop events” transformation, but that’s reactive. You’re cleaning up after the mess&lt;/p&gt;

&lt;p&gt;What works: Disable autocapture from day one and implement explicit event tracking with a schema. Boring, but saves weekends&lt;/p&gt;

&lt;p&gt;Neon — serverless is cool until it sleeps&lt;br&gt;
Neon’s scale-to-zero saves money. But when your database wakes up, you get a 300-700ms cold start. On the free tier, it’s even worse.&lt;/p&gt;

&lt;p&gt;And the connection pooling? It uses PgBouncer in transaction mode. That means SET search_path and prepared statements break. Your app works locally (direct connection) but fails in production (pooled connection). Fun times.&lt;/p&gt;

&lt;p&gt;Fix: Set a minimum compute size to keep it warm or accept the cold start. Also maintain two connection strings - one pooled, one direct for migrations.&lt;/p&gt;

&lt;p&gt;The pattern I didn’t expect&lt;br&gt;
Almost every problem falls into three buckets:&lt;/p&gt;

&lt;p&gt;Leaky abstractions - the tool hides complexity until it breaks, then you need to understand the internals anyway.&lt;/p&gt;

&lt;p&gt;Configuration fatigue - too many env vars, YAML files, and dashboard settings that interact in weird ways.&lt;/p&gt;

&lt;p&gt;Data/schema drift - changes made in dashboards instead of migrations, or events accumulating without governance.&lt;/p&gt;

&lt;p&gt;None of this is new. Joel Spolsky wrote about leaky abstractions in 2002. But somehow every new tool repeats the same mistakes.&lt;/p&gt;

&lt;p&gt;What I’m doing with this&lt;br&gt;
I’m not writing this to bash the tools. They’re all well-engineered and solve real problems.&lt;/p&gt;

&lt;p&gt;But as a student looking for an internship, I wanted to understand: what do engineers actually hate? Because if I can build something that fixes a small piece of that, maybe I can get paid to build it instead of fighting it.&lt;/p&gt;

&lt;p&gt;I already have a few ideas. But first, I’m turning this research into a small series on Dev.to. Next post will be: “Three unsolved problems I found in the research (and what I’m building next).”&lt;/p&gt;

&lt;p&gt;One last thing&lt;br&gt;
If you’re using Supabase, LangChain, PostHog, or Neon - what’s your biggest pain point? I’m genuinely curious. I might try to build a fix for one of them.&lt;/p&gt;

&lt;p&gt;Drop a comment or Find me on &lt;a href="https://github.com/mujib77" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://dev.to/mujib77"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;– Mujib, sophomore who spends too much time in the terminal.&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>ai</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How I Built a Real-Time PostgreSQL WAL Reader in GO</title>
      <dc:creator>Mujib</dc:creator>
      <pubDate>Sat, 09 May 2026 08:41:40 +0000</pubDate>
      <link>https://dev.to/mujib77/how-i-built-a-real-time-postgresql-wal-reader-in-go-4568</link>
      <guid>https://dev.to/mujib77/how-i-built-a-real-time-postgresql-wal-reader-in-go-4568</guid>
      <description>&lt;p&gt;Most developers use PostgreSQL every day without knowing &lt;br&gt;
what happens under the hood when they run an INSERT. I &lt;br&gt;
wanted to find out. So I built pgstream — a tool that &lt;br&gt;
reads every database change in real time directly from &lt;br&gt;
PostgreSQL's internal log.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is the WAL?
&lt;/h2&gt;

&lt;p&gt;Every time you insert, update, or delete a row, Postgres &lt;br&gt;
doesn't immediately write to the actual data files. It &lt;br&gt;
first writes the change to a file called the WAL — Write &lt;br&gt;
Ahead Log. This is how Postgres guarantees nothing gets &lt;br&gt;
lost if the server crashes mid-write.&lt;/p&gt;

&lt;p&gt;I had read about WAL in docs before but never really &lt;br&gt;
understood why it existed until I started building this. &lt;br&gt;
It clicked when I realized — the WAL is basically a &lt;br&gt;
journal. Postgres writes every intention down first, &lt;br&gt;
then acts on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Logical Replication and pgoutput
&lt;/h2&gt;

&lt;p&gt;WAL records are stored in raw binary. You can't just &lt;br&gt;
read them like a text file. Postgres has a built-in &lt;br&gt;
plugin called pgoutput that decodes that binary into &lt;br&gt;
readable messages — INSERT, UPDATE, DELETE — with the &lt;br&gt;
actual row data included.&lt;/p&gt;

&lt;p&gt;To read from the WAL stream your program creates a &lt;br&gt;
replication slot. Think of it as a bookmark. Postgres &lt;br&gt;
tracks your position in the stream and holds WAL records &lt;br&gt;
until your reader has consumed them. This means you can &lt;br&gt;
stop and restart your reader without missing any changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building pgstream
&lt;/h2&gt;

&lt;p&gt;I built pgstream in Go using the pglogrepl library which &lt;br&gt;
handles the low level replication protocol. The project &lt;br&gt;
has four main parts:&lt;/p&gt;

&lt;p&gt;Connector — opens a replication connection to Postgres &lt;br&gt;
and creates the slot.&lt;/p&gt;

&lt;p&gt;Decoder — this was the hardest part. It reads raw bytes &lt;br&gt;
coming from Postgres and figures out what each message &lt;br&gt;
means. Postgres sends different message types — Relation &lt;br&gt;
messages that describe table schemas, Insert messages, &lt;br&gt;
Update messages, Delete messages, and keepalive &lt;br&gt;
heartbeats. The decoder has to handle all of them &lt;br&gt;
correctly.&lt;/p&gt;

&lt;p&gt;The tricky part was the Relation message. Before Postgres &lt;br&gt;
sends an INSERT event it sends a Relation message &lt;br&gt;
describing the table structure — column names, types. &lt;br&gt;
You have to store that and reference it later when &lt;br&gt;
decoding the actual row data. Without this you get raw &lt;br&gt;
bytes with no idea which column is which.&lt;/p&gt;

&lt;p&gt;Handler — takes the decoded event and prints it cleanly &lt;br&gt;
to the terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;What surprised me most was how fast it is. The moment &lt;br&gt;
you run an INSERT in pgAdmin the change appears in the &lt;br&gt;
terminal almost instantly. There is basically no delay. &lt;/p&gt;

&lt;p&gt;I knew replication was fast in theory but seeing it &lt;br&gt;
happen live made it real. This is the same mechanism &lt;br&gt;
companies use to sync databases across regions in &lt;br&gt;
real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;I understood Postgres as a database before this. Now I &lt;br&gt;
understand it as a system. WAL, logical decoding, &lt;br&gt;
replication slots, LSN — these are the internals that &lt;br&gt;
make Postgres reliable at scale.&lt;/p&gt;

&lt;p&gt;Go was also new to me coming from JavaScript and C. &lt;br&gt;
The concurrency model with goroutines and channels &lt;br&gt;
is genuinely different from anything I had used before.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;pgstream is open source.&lt;br&gt;
github.com/mujib77/pgstream&lt;/p&gt;

&lt;p&gt;If you want to understand what's happening inside your &lt;br&gt;
Postgres database — clone it, run it, and watch your &lt;br&gt;
changes stream in real time.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>go</category>
      <category>opensource</category>
      <category>sql</category>
    </item>
  </channel>
</rss>
