<?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: Osman Ahmadzai</title>
    <description>The latest articles on DEV Community by Osman Ahmadzai (@osmanahmadxai).</description>
    <link>https://dev.to/osmanahmadxai</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%2F4044307%2F6e23ffb9-d120-4644-ac97-87dbc2647c10.png</url>
      <title>DEV Community: Osman Ahmadzai</title>
      <link>https://dev.to/osmanahmadxai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/osmanahmadxai"/>
    <language>en</language>
    <item>
      <title>I made AI coding agents race each other, and the fast one wasn't the one I expected</title>
      <dc:creator>Osman Ahmadzai</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:26:37 +0000</pubDate>
      <link>https://dev.to/osmanahmadxai/i-made-ai-coding-agents-race-each-other-and-the-fast-one-wasnt-the-one-i-expected-bn8</link>
      <guid>https://dev.to/osmanahmadxai/i-made-ai-coding-agents-race-each-other-and-the-fast-one-wasnt-the-one-i-expected-bn8</guid>
      <description>&lt;p&gt;Every few days someone tells me which coding agent is "obviously the best". They always have a benchmark to back it up. And the benchmark is always run on code that has nothing to do with mine.&lt;/p&gt;

&lt;p&gt;I got tired of nodding along, so I built a small thing to settle it the dumb way: give them all the same task at the same time and watch.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;Agent Derby&lt;/strong&gt;, and this is what a race looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx7yj1i9cmus6qxijrvs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx7yj1i9cmus6qxijrvs.gif" alt="Three Claude models racing to build a snake game, sped up" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first race
&lt;/h2&gt;

&lt;p&gt;I typed "build a playable snake game in the browser", picked three Claude models, and hit start.&lt;/p&gt;

&lt;p&gt;Sonnet 5.5 was finished in &lt;strong&gt;17 seconds&lt;/strong&gt;. I hadn't even finished reading what the other two were doing.&lt;/p&gt;

&lt;p&gt;Haiku 4.5 came in at 31 seconds.&lt;/p&gt;

&lt;p&gt;Opus 5.5 took &lt;strong&gt;two and a half minutes&lt;/strong&gt; and cost about ten times as much as either of them.&lt;/p&gt;

&lt;p&gt;So Opus lost, right? Well. Then I played the games.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66trep02zbu3q8rjr819.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66trep02zbu3q8rjr819.png" alt="The three finished games side by side" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sonnet's game works, and that is all it does. A square, a dot, a score.&lt;/p&gt;

&lt;p&gt;Opus built a start screen, sound effects, a pause button and a mute key. It wrote about six times as much code. Nobody asked for any of that, and it's also clearly the one you'd want to ship.&lt;/p&gt;

&lt;p&gt;That's the thing a score table can't tell you. "Fastest" and "best" were two different models, and I only found out because I could actually play all three.&lt;/p&gt;

&lt;p&gt;It's one task. It proves nothing in general. But it changed how I think about picking a model, which is more than any leaderboard has done for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;You type a task and choose who races. Everyone gets the exact same prompt at the exact same moment.&lt;/p&gt;

&lt;p&gt;Each agent works in its own sandbox, a private copy of the project. They can't see each other's work and they can't write to your real code. I was nervous about letting three agents loose with every permission switched on, so on macOS and Linux the operating system itself blocks them from writing anywhere else.&lt;/p&gt;

&lt;p&gt;While they run, you see every lane at once: what each one is reading, editing or running, with the clock, tokens and cost ticking.&lt;/p&gt;

&lt;p&gt;When one finishes, whatever it built just starts up in its lane. Web apps open in a frame. Terminal programs get a real terminal. No copying commands, no "now run npm install".&lt;/p&gt;

&lt;p&gt;Then you get the numbers lined up next to each other.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feek2y8b2gud1upwvd6k0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feek2y8b2gud1upwvd6k0.png" alt="Results screen with ranking and comparison table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like one result, you can keep it as a new branch or a folder. It never merges anything for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule I was stubborn about
&lt;/h2&gt;

&lt;p&gt;If an agent's CLI doesn't report a number, the app says &lt;strong&gt;"not reported"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not zero. Not a guess. Not a dash that looks like zero.&lt;/p&gt;

&lt;p&gt;It sounds like a small thing, but comparison tools get untrustworthy fast when they quietly fill in blanks. If the cost is an estimate, it's labelled as an estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me while building it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Some models take a long time to say anything.&lt;/strong&gt; One model sat silent for minutes between steps and timed out twice. I assumed my sandbox had broken it. It hadn't. It was just slow that day. The other lanes carried on without noticing, which is exactly what I wanted to be true, but it was a nervous ten minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents do unexpected things when left alone.&lt;/strong&gt; One of them, asked to build a game, decided to start a web server and go looking for a browser to test it in. Fair enough, honestly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Is it playable?" is harder to check than it sounds.&lt;/strong&gt; My first automated check pressed arrow keys and declared one game broken. It wasn't broken. It had a Start button.&lt;/p&gt;

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

&lt;p&gt;You need Node and git.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx https://github.com/osmanahmadxai/agent-derby/releases/latest/download/agent-derby.tgz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It uses the agent CLIs you're already logged in to, so there are no API keys involved. If you don't have any agent installed, there are built-in demo agents that run a scripted race for free, so you can see how it works first.&lt;/p&gt;

&lt;p&gt;There's also a terminal version that splits your screen into one pane per agent, and a desktop app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not finished
&lt;/h2&gt;

&lt;p&gt;I'd rather tell you than have you find out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I've only properly tested &lt;strong&gt;Claude Code&lt;/strong&gt;. Codex CLI and Gemini CLI are wired up, but I haven't had a real logged-in run with either yet. They may break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; has no sandbox yet and is basically untested.&lt;/li&gt;
&lt;li&gt;The desktop builds aren't code-signed, so your OS will complain the first time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for full disclosure: I built this with Claude Code. Which means an AI coding agent helped write the tool that judges AI coding agents. Make of that what you will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;It's MIT licensed and all of it is here: &lt;strong&gt;&lt;a href="https://github.com/osmanahmadxai/agent-derby" rel="noopener noreferrer"&gt;github.com/osmanahmadxai/agent-derby&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you race something, tell me what happened. I'm especially curious whether the ranking flips on a harder task than snake. My guess is yes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Syncle: keep any two databases in sync, live and across engines</title>
      <dc:creator>Osman Ahmadzai</dc:creator>
      <pubDate>Thu, 23 Jul 2026 18:07:39 +0000</pubDate>
      <link>https://dev.to/osmanahmadxai/syncle-keep-any-two-databases-in-sync-live-and-across-engines-3656</link>
      <guid>https://dev.to/osmanahmadxai/syncle-keep-any-two-databases-in-sync-live-and-across-engines-3656</guid>
      <description>&lt;p&gt;You have a row in Postgres. You want that same row in MongoDB — not tonight in a batch job, but the &lt;em&gt;instant&lt;/em&gt; it changes. And next month you'll want it in Redis too, and maybe POSTed to some webhook.&lt;/p&gt;

&lt;p&gt;Today that's a Kafka cluster, a Debezium connector, a sink connector, a schema registry, and a weekend. For a job that is, at its heart, one sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a source → one or more destinations → kept in sync.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Syncle&lt;/strong&gt; is an open-source tool that does exactly that sentence, and nothing you didn't ask for. Connect your databases, draw a &lt;strong&gt;bridge&lt;/strong&gt; from a source to one or more destinations, and the moment a row changes in the source it's written to every destination you linked. Any engine to any engine — &lt;strong&gt;PostgreSQL · MySQL/MariaDB · SQLite · MongoDB · Redis&lt;/strong&gt; — plus HTTP endpoints when you need them.&lt;/p&gt;

&lt;p&gt;This post is a tour of what it does and, for the curious, how it's built.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea: a bridge
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;bridge&lt;/strong&gt; reads rows from a source and writes each one to its &lt;strong&gt;destinations&lt;/strong&gt;. A destination is either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;another database&lt;/strong&gt; — the headline feature. Postgres → MongoDB, MySQL → SQLite, MongoDB → Redis. One bridge can fan out to several databases at once, and bridges can chain (A → B → C).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;an HTTP endpoint&lt;/strong&gt; — POST/PUT/PATCH each row to a URL with a payload you design, for feeding a service instead of a database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part isn't that it copies data — plenty of things copy data. It's the guarantees around &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  No duplicates, ever
&lt;/h3&gt;

&lt;p&gt;Every database write is an &lt;strong&gt;idempotent upsert&lt;/strong&gt;, keyed by columns you choose. So replays, retries, and at-least-once redeliveries never double-write. Under the hood each engine does it with its own native atomic operation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Upsert&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL / SQLite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON CONFLICT&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateOne(filter, ..., { upsert: true })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inserts, updates, &lt;strong&gt;and deletes&lt;/strong&gt; all propagate — a delete routes to a keyed delete on each target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing table? It builds it
&lt;/h3&gt;

&lt;p&gt;If the destination table or collection doesn't exist, Syncle creates it from the source's shape, translating types across the engine boundary (a Postgres &lt;code&gt;timestamptz&lt;/code&gt; becomes something sensible in SQLite, a document in Mongo, a hash in Redis). Or you map columns yourself: &lt;em&gt;write this column into that column over there&lt;/em&gt;, rename, drop, pick keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  You pick how it fires
&lt;/h3&gt;

&lt;p&gt;Three trigger modes, same everything-else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replay&lt;/strong&gt; — a one-shot job. Stream all (or selected) rows once, then finish. Perfect for the initial backfill or a migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch&lt;/strong&gt; — poll the source on a cursor (auto-increment id, an &lt;code&gt;updated_at&lt;/code&gt; column, or a primary-key diff) and sync new rows as they appear. Works on every engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDC&lt;/strong&gt; — true change-data-capture straight from the database's change log, in real time, no polling: Postgres logical replication, MySQL binlog, MongoDB change streams, Redis keyspace notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You build the whole thing visually — browse the source table, toggle columns, pick destinations, and watch a live preview of exactly what will be written before you commit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Watch it happen
&lt;/h2&gt;

&lt;p&gt;Reliability you can't see isn't reassuring, so every delivery shows up on a live timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 green — synced&lt;/li&gt;
&lt;li&gt;🔴 red — failed&lt;/li&gt;
&lt;li&gt;🟡 amber — skipped&lt;/li&gt;
&lt;li&gt;⬜ slate — queued&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click any cell to see the exact row written, the result, timing, and any error. Runs survive restarts, resume where they stopped, and can be cancelled. You can skip rows by range, or retry only the failed ones in place — and watch the failed cells flip green.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it in one command
&lt;/h2&gt;

&lt;p&gt;A fresh machine needs only Docker. Everything else — Node, Postgres, Redis — runs in containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/osmanahmadxai/SYNCLE/main/install.sh | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That installs a native &lt;code&gt;syncle&lt;/code&gt; command, builds the stack, and opens the GUI at &lt;code&gt;http://localhost:3002&lt;/code&gt;. After that it's just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;syncle up      &lt;span class="c"&gt;# start everything, open the GUI&lt;/span&gt;
syncle down    &lt;span class="c"&gt;# stop it&lt;/span&gt;
syncle logs    &lt;span class="c"&gt;# follow the logs&lt;/span&gt;
syncle update  &lt;span class="c"&gt;# pull latest + rebuild&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer to run from source?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;                  &lt;span class="c"&gt;# frontend + backend&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;          &lt;span class="c"&gt;# postgres (metadata) + redis (job queue)&lt;/span&gt;
pnpm start                    &lt;span class="c"&gt;# env files + migrations + run the whole app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;Syncle is a pnpm monorepo with a strict one-way dependency flow: &lt;code&gt;web → api → core&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;syncle/
├─ packages/
│  └─ core/            @syncle/core — framework-agnostic domain (pure TS)
│     ├─ adapters/       DatabaseAdapter interface + one file per engine
│     │                  (raw drivers: pg, mysql2, better-sqlite3, mongodb, ioredis)
│     └─ hooks/          column mapping + cross-engine translation, payload
│                        transform, shared bridge schemas (Zod)
├─ apps/
│  ├─ api/             @syncle/api — NestJS backend
│  │  ├─ hooks/          bridge store · run processor · CDC providers ·
│  │  │                  sink router → database sink + HTTP delivery
│  │  ├─ connections/    Prisma-backed store · live adapter pool
│  │  └─ common/         crypto · Zod validation · exception filter
│  └─ web/             @syncle/web — Next.js 15 + shadcn/ui + TanStack
└─ docker-compose.yml  Postgres (metadata) + Redis (run queue)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design decisions did most of the work of keeping it small.&lt;/p&gt;

&lt;h3&gt;
  
  
  One sink, two destination kinds
&lt;/h3&gt;

&lt;p&gt;Every trigger — replay, watch, CDC — funnels rows through a &lt;strong&gt;single sink router&lt;/strong&gt;. It dispatches to the &lt;strong&gt;database sink&lt;/strong&gt; (map columns → auto-create if needed → native upsert or keyed delete) or to &lt;strong&gt;HTTP delivery&lt;/strong&gt; (template render → POST with retries). The runner, the timeline, and the exactly-once accounting don't care which. So adding a new &lt;em&gt;kind&lt;/em&gt; of destination is one module, not a rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDC behind one interface
&lt;/h3&gt;

&lt;p&gt;Each engine captures change its own way — logical replication, binlog, change streams, keyspace notifications — but they all implement the same small contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CdcProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;readiness&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Readiness&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// is the source configured for capture?&lt;/span&gt;
  &lt;span class="nf"&gt;provision&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// create slot/publication if needed&lt;/span&gt;
  &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;AsyncIterable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Change&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// insert/update/delete, each tagged with its op&lt;/span&gt;
  &lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Cursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// where we are, for resume&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service around them owns the run lifecycle and the shared &lt;strong&gt;dedupe → map → write → record → checkpoint&lt;/strong&gt; pipeline. Adding a new engine's CDC is a single file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two data layers, two right tools
&lt;/h3&gt;

&lt;p&gt;This is my favourite tension in the codebase. The databases you connect &lt;em&gt;to&lt;/em&gt; have unknown, runtime-discovered schemas — so those adapters use &lt;strong&gt;raw drivers with fully parameterized queries&lt;/strong&gt;. An ORM literally can't introspect an arbitrary schema it's never seen.&lt;/p&gt;

&lt;p&gt;But Syncle's &lt;em&gt;own&lt;/em&gt; store — saved connections, bridges, runs, deliveries — has a fixed schema we control. So that uses &lt;strong&gt;Prisma with migrations&lt;/strong&gt;. Same app, opposite tools, each because of what it actually knows about the schema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durable runs
&lt;/h3&gt;

&lt;p&gt;A replay run is one BullMQ job (&lt;code&gt;jobId = runId&lt;/code&gt;). It streams the source a page at a time with keyset pagination (millions of rows, flat memory), syncs sequentially for natural backpressure, and checkpoints progress. A crash auto-resumes from where it left off.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The north star: &lt;strong&gt;adding an engine = implement &lt;code&gt;DatabaseAdapter&lt;/code&gt; and register it.&lt;/strong&gt; The connection form, schema browser, and feature gating all derive from that one registration.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A note on security
&lt;/h2&gt;

&lt;p&gt;Syncle handles credentials, so a few things are non-negotiable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection passwords and hook secrets are &lt;strong&gt;encrypted at rest (AES-256-GCM)&lt;/strong&gt; and only ever returned to the browser redacted.&lt;/li&gt;
&lt;li&gt;All user values are &lt;strong&gt;bound parameters&lt;/strong&gt;; identifiers are dialect-quoted.&lt;/li&gt;
&lt;li&gt;HTTP payloads are built by &lt;strong&gt;structured token substitution&lt;/strong&gt; (&lt;code&gt;{{column}}&lt;/code&gt;, &lt;code&gt;{{$row}}&lt;/code&gt;, &lt;code&gt;{{$op}}&lt;/code&gt;, …) — no string injection, no code execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs locally with no auth layer by default. Add authentication and restrict which destinations a bridge may write to before exposing it to an untrusted network.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;NestJS · BullMQ + Redis · Prisma + PostgreSQL · Next.js 15 · React 19 · TypeScript · Tailwind · shadcn/ui · TanStack Query &amp;amp; Table · Monaco · React Flow · Zod · Vitest.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/osmanahmadxai/SYNCLE" rel="noopener noreferrer"&gt;https://github.com/osmanahmadxai/SYNCLE&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-liner:&lt;/strong&gt; &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/osmanahmadxai/SYNCLE/main/install.sh | sh -s -- up&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever wanted "just keep these two databases in sync" without standing up a streaming platform to do it, I'd love for you to try it and tell me where it breaks. Issues and PRs welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What's the messiest cross-database sync you've had to build by hand? Tell me in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
