<?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: Carl-Gerhard Lindesvärd</title>
    <description>The latest articles on DEV Community by Carl-Gerhard Lindesvärd (@lindesvard).</description>
    <link>https://dev.to/lindesvard</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1429354%2Fb92c5f12-cf6b-48b0-bbbb-3ba18d75c858.jpeg</url>
      <title>DEV Community: Carl-Gerhard Lindesvärd</title>
      <link>https://dev.to/lindesvard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lindesvard"/>
    <language>en</language>
    <item>
      <title>How Queues Saved Our API from Meltdown</title>
      <dc:creator>Carl-Gerhard Lindesvärd</dc:creator>
      <pubDate>Mon, 08 Dec 2025 19:58:02 +0000</pubDate>
      <link>https://dev.to/lindesvard/how-queues-saved-our-api-from-meltdown-55jb</link>
      <guid>https://dev.to/lindesvard/how-queues-saved-our-api-from-meltdown-55jb</guid>
      <description>&lt;p&gt;Most developers don't think about queues until the day their API catches fire. You build an endpoint, it works fine, then someone sends a spike of traffic and suddenly everything slows down. Requests hang, CPU spikes, retries add even more load. It turns into chaos really fast.&lt;/p&gt;

&lt;p&gt;This was us at OpenPanel. Our ingestion endpoint handles a lot of traffic. It grew slowly at first, then a few customers started sending big bursts and everything fell apart. Not gradually. It went from perfectly fine to completely overwhelmed in minutes. We didn't scale the API. We offloaded it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queues are not over-engineering
&lt;/h3&gt;

&lt;p&gt;People like to say queues are only for giant enterprise systems. I don't really buy that. A queue is basically an async function that runs later instead of right now. That's all it is. You don't need microservices or some huge distributed setup to use one. You just need a spot where you push work so your API doesn't do everything inside the request lifecycle. It is simple, and it can completely change how your system behaves under load.&lt;/p&gt;

&lt;h3&gt;
  
  
  When queues actually help
&lt;/h3&gt;

&lt;p&gt;There are a lot of good use cases, but these are the clearest ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy background tasks.&lt;/strong&gt; Sending emails, notifications, PDF generation. Any slow thing the user doesn't need to wait for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recurring jobs.&lt;/strong&gt; Queues are a great way to run cron logic inside your own code instead of managing some external service. You get more control over retries and timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better user experience.&lt;/strong&gt; This one feels small but it is amazing in practice. Imagine a user writes a comment. Without a queue you would save the comment, then check who needs a notification, then send every single one. The user sits there waiting for all of this to finish. With a queue you save the comment and push a job. The API returns right away. Notifications get sent in the background. Huge UX win for basically no effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingestion endpoints.&lt;/strong&gt; This is the big one for us. Our tracking endpoint used to validate events, transform them, fetch extra data, and insert everything. All inside one request. That works until someone sends 20 times the normal traffic. Then it stops working pretty fast.&lt;/p&gt;

&lt;p&gt;Now the endpoint does one thing. It pushes the event into a queue and returns. Done. The user only cares that we received the request. They do not care what we do after that. This change made the whole ingestion layer stable even when we get huge spikes. If the workers fall behind it just means a delay. We do not lose data. The API stays fast. The queue absorbs the blow instead of the server falling over.&lt;/p&gt;

&lt;h3&gt;
  
  
  A simple BullMQ example
&lt;/h3&gt;

&lt;p&gt;Here is a bare bones setup you can add to any Node API.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bullmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs&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;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/track&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;track&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can even run the worker in the same process while prototyping. When you go to production you normally split them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using queues in a serverless world
&lt;/h3&gt;

&lt;p&gt;Serverless makes this trickier because you can't keep workers alive. But there are solutions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger.dev&lt;/strong&gt; gives you background jobs and retries without needing long running processes. It works with a bunch of runtimes and solves the exact problem that queues solve for traditional servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel's Workflow API&lt;/strong&gt; is also interesting. You write background workflows as simple functions. They run outside the request flow and persist on their own. If you are already using Vercel this feels like cheating. You get a lot of the benefits of a queue without running Redis or workers yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why queues mattered for us
&lt;/h3&gt;

&lt;p&gt;Before we added queues, any traffic spike would make our ingestion API slow or unstable. After we added queues it became boring. And boring is exactly what you want in production.&lt;/p&gt;

&lt;p&gt;Queues let us keep the API tiny and predictable. Workers do the heavy work. Traffic spikes turn into a backlog instead of downtime. This one change made OpenPanel a lot more reliable for everyone using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A quick note about OpenPanel
&lt;/h3&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%2F4htv6ub962p7n5ubw5jc.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%2F4htv6ub962p7n5ubw5jc.png" alt=" " width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Queues have become a core part of how we keep OpenPanel fast. Our ingestion API gets a lot of traffic every day and queues turned it from something fragile into something we don't need to think about anymore. We use them for event ingestion, cron tasks, cleanup jobs, and anything that should not block the user. It is one of the reasons OpenPanel can handle heavy load without falling over.&lt;/p&gt;

&lt;p&gt;If you want an open source analytics platform that can survive traffic without feeling fragile, take a look at it. It is fully self hosted and built for real world workloads.&lt;/p&gt;

&lt;p&gt;In the next article I will talk about &lt;strong&gt;GroupMQ&lt;/strong&gt;, our custom built queue library. It is a drop in alternative to BullMQ with first class support for grouped queues and ordered processing. These features saved us more than once, and I think a lot of teams will find them useful.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>ClickHouse: The Good, The Bad, and The Ugly</title>
      <dc:creator>Carl-Gerhard Lindesvärd</dc:creator>
      <pubDate>Mon, 17 Nov 2025 15:02:11 +0000</pubDate>
      <link>https://dev.to/lindesvard/clickhouse-the-good-the-bad-and-the-ugly-2pi7</link>
      <guid>https://dev.to/lindesvard/clickhouse-the-good-the-bad-and-the-ugly-2pi7</guid>
      <description>&lt;p&gt;ClickHouse is one of those databases that everyone gets excited about after their first benchmark. It’s absurdly fast, column-oriented, and built for analytics at scale.&lt;/p&gt;

&lt;p&gt;It’s also surprisingly easy to plug into an existing stack. You can stream data from Postgres, Mongo, S3, or pretty much anywhere. That’s what makes it so appealing. You hit the point where your Postgres queries start to struggle, you don’t want to rebuild everything, so you drop in ClickHouse and suddenly your dashboard loads in milliseconds.&lt;/p&gt;

&lt;p&gt;It’s like putting a turbocharger on your reporting.&lt;/p&gt;

&lt;p&gt;ClickHouse is also improving at a crazy pace. Every month they roll out new features, bug fixes and faster queries.&lt;/p&gt;

&lt;p&gt;But with speed comes responsibility. ClickHouse is a beast. It’ll reward you when you treat it well, but it’ll bite you if you cut corners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud vs self-hosting
&lt;/h3&gt;

&lt;p&gt;Your first big decision is whether to self-host or go with a managed provider.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;th&gt;Self-Hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Days/weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost at scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$$$&lt;/td&gt;
&lt;td&gt;$ + engineer time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backup/HA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Headaches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A few but not hosting related&lt;/td&gt;
&lt;td&gt;Many&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Good for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most use cases&lt;/td&gt;
&lt;td&gt;Cost optimization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cloud route is painless. You get uptime, automatic scaling, and zero headaches. The trade-off is cost. ClickHouse Cloud, Altinity Cloud, Tinybird, they all work great, but the bill can sting hard once you start pushing data around. You don’t get server issues, but you pay for the peace of mind.&lt;/p&gt;

&lt;p&gt;In Clickhouse Cloud you also don't need to worry about replication, this is handled under the hood so you don't need to create replicated and distributed tables.&lt;/p&gt;

&lt;p&gt;Self-hosting looks easy at first, but it’s not.&lt;/p&gt;

&lt;p&gt;You spin up a single node, everything flies, and then one day something fails, stops merging data, corrupt data or what ever. This is just the top of the iceberg.&lt;/p&gt;

&lt;p&gt;To handle real production traffic you’ll end up with replicated and distributed tables. You’ll need to decide between vertical scaling, horizontal scaling, or both. Then you start worrying about corrupted parts, cluster topology, and backups.&lt;/p&gt;

&lt;p&gt;Running ClickHouse yourself works fine for smaller setups. Once you grow, it’s a full-time job unless you use something like the &lt;strong&gt;Altinity ClickHouse Operator&lt;/strong&gt; on Kubernetes. That operator makes things bearable. You define clusters in YAML, it handles replication, zookeeper (Clickhouse Keeper), and have great backup strategies. If you ever plan to self-host long-term, start there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dark side of joins
&lt;/h3&gt;

&lt;p&gt;Joins in ClickHouse are not the joins you’re used to. They work, but they’re not “free.”&lt;/p&gt;

&lt;p&gt;ClickHouse doesn’t have a full query optimizer like Postgres or MySQL. That means it doesn’t plan your joins intelligently. If you join two big tables, it’ll happily try and load everything in memory and die trying.&lt;/p&gt;

&lt;p&gt;You have to think ahead. Filter first, join later.&lt;/p&gt;

&lt;p&gt;A few ways to survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;CTEs&lt;/strong&gt; or sub-queries to narrow down the joined dataset before the join actually happens.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;dictionaries&lt;/strong&gt; (in-memory lookup tables) for small reference data. They’re insanely fast, but they have to fit in memory.&lt;/li&gt;
&lt;li&gt;Know your &lt;strong&gt;sorting keys&lt;/strong&gt;. ClickHouse relies on them for efficient reads. Bad keys make joins worse.&lt;/li&gt;
&lt;li&gt;Always join the smaller table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll notice when you have a bad join since it will take ages or die trying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updates, deletes, and the reality of immutability
&lt;/h3&gt;

&lt;p&gt;ClickHouse was never designed for frequent updates or deletes. It’s a write-once, append-forever kind of database. You can’t just run &lt;code&gt;UPDATE users SET ...&lt;/code&gt; like in Postgres.&lt;/p&gt;

&lt;p&gt;To their credit, the ClickHouse team has made big progress here. They’ve added &lt;strong&gt;lightweight deletes and updates&lt;/strong&gt;, and there are new table engines like &lt;code&gt;ReplacingMergeTree&lt;/code&gt; and &lt;code&gt;VersionedCollapsingMergeTree&lt;/code&gt; that can simulate mutable data. But it still takes extra thought.&lt;/p&gt;

&lt;p&gt;You need to design your tables knowing that changing data later is harder. That’s fine for analytics workloads, but painful if you expect relational behavior.&lt;/p&gt;

&lt;p&gt;I still run into these kind of issues today. Hoping the lightweight updates which is in beta now will make my life easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inserting data the right way
&lt;/h3&gt;

&lt;p&gt;Here’s the biggest beginner trap.&lt;/p&gt;

&lt;p&gt;ClickHouse loves big inserts. It hates small ones.&lt;/p&gt;

&lt;p&gt;Every insert triggers background merges, index updates, compression, and part creation. Do that one row at a time and you’ll drown it. Batch your inserts into chunks, ideally thousands of rows at a time. You’ll instantly see CPU drop and throughput skyrocket.&lt;/p&gt;

&lt;p&gt;If you’re ingesting data continuously, throw it into a queue and batch it there. That’s what we do at &lt;a href="https://openpanel.dev" rel="noopener noreferrer"&gt;OpenPanel.dev&lt;/a&gt;. It smooths out traffic spikes and keeps our ingestion fast and predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replication and sharding
&lt;/h3&gt;

&lt;p&gt;This isn’t a bad thing about ClickHouse. In fact, it’s one of its best features.&lt;br&gt;
But I still want to cover a few parts that confused me when I first started using it.&lt;/p&gt;

&lt;p&gt;There are three kinds of tables you’ll deal with when setting up replication or sharding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your existing table (usually &lt;code&gt;MergeTree&lt;/code&gt; or something similar)&lt;/li&gt;
&lt;li&gt;A replicated table (&lt;code&gt;ReplicatedMergeTree&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A distributed table (&lt;code&gt;Distributed&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these plays a different role in your cluster, and it’s worth understanding them before you begin.&lt;/p&gt;



&lt;p&gt;First, decide how many replicas you want. Most setups use three replicas for high availability and to get replication working, you’ll replace your existing table with a replicated one.&lt;/p&gt;

&lt;p&gt;You can do that by creating a new table and swapping &lt;code&gt;MergeTree&lt;/code&gt; for &lt;code&gt;ReplicatedMergeTree&lt;/code&gt; in the engine section.&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;events_replicated&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReplicatedMergeTree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'/clickhouse/{installation}/{cluster}/tables/{shard}/openpanel/v1/{table}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{replica}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;toYYYYMM&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="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that’s done, any data written to one node will be replicated to the others. ZooKeeper or ClickHouse Keeper handles the synchronization automatically.&lt;/p&gt;

&lt;p&gt;If you want to move your data from your existing table to the replicated table you can use &lt;code&gt;INSERT SELECT&lt;/code&gt; to do this.&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;events_replicated&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Now let’s look at where sharding and &lt;code&gt;Distributed&lt;/code&gt; tables come in.&lt;br&gt;
Sharding is how you scale horizontally by splitting data into smaller chunks and spreading them across nodes. That said, it’s usually better to scale vertically first, because ClickHouse handles vertical scaling surprisingly well.&lt;/p&gt;

&lt;p&gt;If you decide to shard, you’ll need to create a distributed table. A distributed table knows where your data lives and redirects queries to the right node.&lt;/p&gt;

&lt;p&gt;When creating one, you define how data should be split across nodes. In the example below, the data is sharded using &lt;code&gt;cityHash64(project_id)&lt;/code&gt;, which spreads rows evenly based on the &lt;code&gt;project_id&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;events_distributed&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CLUSTER&lt;/span&gt; &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;events_replicated&lt;/span&gt;
&lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Distributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'{cluster}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;currentDatabase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;events_replicated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cityHash64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&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;Now you can query data from any node, and ClickHouse will automatically route the request to where the data actually sits.&lt;/p&gt;

&lt;p&gt;If you want to dig deeper, check out the official docs on &lt;a href="https://clickhouse.com/docs/engines/table-engines/special/distributed" rel="noopener noreferrer"&gt;Distributed tables&lt;/a&gt; and &lt;a href="https://clickhouse.com/docs/architecture/replication" rel="noopener noreferrer"&gt;Replication&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  So why stick with it?
&lt;/h3&gt;

&lt;p&gt;Because when it works, it’s magic.&lt;/p&gt;

&lt;p&gt;At OpenPanel we hit all these issues. Slow inserts, bad joins, tricky replication, and we still use ClickHouse every single day. Once you set it up correctly, nothing else compares. It’s unbelievably fast and scales far beyond what most relational databases can handle.&lt;/p&gt;

&lt;p&gt;You just have to respect it. Treat it like a Ferrari, not a Corolla.&lt;/p&gt;

&lt;p&gt;If you want me to go deeper into how we deploy and manage our own cluster on Kubernetes using the Altinity operator, let me know in the comments. I can show exactly how we keep it stable and cost-efficient.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>How I Accidentally Built My Own Mixpanel Alternative</title>
      <dc:creator>Carl-Gerhard Lindesvärd</dc:creator>
      <pubDate>Tue, 11 Nov 2025 14:38:01 +0000</pubDate>
      <link>https://dev.to/lindesvard/how-i-accidentally-built-my-own-mixpanel-alternative-45i6</link>
      <guid>https://dev.to/lindesvard/how-i-accidentally-built-my-own-mixpanel-alternative-45i6</guid>
      <description>&lt;p&gt;Two years ago, around September 2023, I was working on one of my side projects. It wasn’t making money, but I wanted to understand how people were using it, so I used Mixpanel.&lt;/p&gt;

&lt;p&gt;At first it was fine. We were part of Mixpanel’s startup program, which made it free to use. That app had maybe 250–500k events a month, not much. Then the program ended. Suddenly the bill jumped to $300–400 a month.&lt;/p&gt;

&lt;p&gt;That was a bit painful for a project that had 10k active users a month&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I didn’t just switch tools
&lt;/h3&gt;

&lt;p&gt;I looked around. PostHog looked great, but it felt too advanced and heavy for what I needed. Plausible and Umami were cool for web analytics, but they didn’t support real product analytics, and custom events were behind enterprise plans.&lt;/p&gt;

&lt;p&gt;What I really wanted was something in between.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The simplicity of Plausible.&lt;/li&gt;
&lt;li&gt;The depth of Mixpanel.&lt;/li&gt;
&lt;li&gt;And the freedom of open source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination didn’t exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  The moment it clicked
&lt;/h3&gt;

&lt;p&gt;One evening I pulled data from my Postgres database and made a small chart just for fun.&lt;br&gt;
When I saw that chart update with live data, I realized I could actually build the kind of reporting tool Mixpanel had.&lt;/p&gt;

&lt;p&gt;That was the moment OpenPanel started.&lt;/p&gt;

&lt;p&gt;I figured if I was already logging events, why not build something that gives me full control? At first it was just an experiment. But after a while, I saw the bigger picture. If I wanted proper product analytics, I’d also need to track web data. So I started combining the two.&lt;/p&gt;

&lt;p&gt;Product analytics plus web analytics in one self-hosted stack.&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%2Ftu7poo47i1vl1jdfxqrx.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%2Ftu7poo47i1vl1jdfxqrx.png" alt="Today's product analytics" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The early stack
&lt;/h3&gt;

&lt;p&gt;The first version was a mix of &lt;strong&gt;Next.js, Node, and Postgres&lt;/strong&gt;. It wasn’t fancy, but it worked. I could track events, view them, and draw basic charts.&lt;/p&gt;

&lt;p&gt;Postgres handled things fine until the event count grew. Once it hit millions, queries started slowing down. That’s when I moved to ClickHouse and kept Postgres for application data.&lt;/p&gt;

&lt;p&gt;It was the first time OpenPanel started to feel like a real analytics product.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hard parts
&lt;/h3&gt;

&lt;p&gt;I’ve honestly given up on this project at least a hundred times.&lt;/p&gt;

&lt;p&gt;Building an open source platform that handles real data is hard. You fight with performance, storage, schemas, queues, and migrations. Every change has ripple effects.&lt;/p&gt;

&lt;p&gt;And then there’s the self-hosting part. You can’t just ship code. You have to think about Docker setups, upgrade paths, environment variables, and all the edge cases that don’t show up in your own environment.&lt;/p&gt;

&lt;p&gt;It’s a lot. But every time I get a message from someone who deployed it successfully or a paying customer saying “this saved us thousands,” it’s worth it. That feeling keeps me going.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it’s going now
&lt;/h3&gt;

&lt;p&gt;Fast forward to today — OpenPanel handles around &lt;strong&gt;100 million events per month&lt;/strong&gt; and around 200 self-hosted instance (what we know about).&lt;/p&gt;

&lt;p&gt;Some of our bigger customers have actually moved to self-hosting to save costs, which is exactly what I wanted. Open source analytics should give you control and options, not lock you in.&lt;/p&gt;

&lt;p&gt;The product has come a long way since that first chart I created.&lt;/p&gt;

&lt;p&gt;It’s now a complete analytics suite that combines product analytics and web analytics in one place, with no cookies, no user tracking, and full self-hosting support.&lt;/p&gt;

&lt;p&gt;You can think of it as a &lt;strong&gt;Mixpanel alternative&lt;/strong&gt; that’s simple like Plausible but powerful enough for real apps.&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%2F7evpdh6ltd132d80jie7.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%2F7evpdh6ltd132d80jie7.png" alt="The epic real time view" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I’m still building it
&lt;/h3&gt;

&lt;p&gt;OpenPanel started as a personal project because I couldn’t find what I wanted. Now it’s turning into something bigger.&lt;/p&gt;

&lt;p&gt;The goal hasn’t changed though. I want to make analytics tools that are simple, transparent, and don’t punish you for growing.&lt;/p&gt;

&lt;p&gt;If you’ve ever been frustrated by pricing tiers, missing features, or vendor lock-in, you’ll get it.&lt;/p&gt;

&lt;p&gt;That’s why I built OpenPanel, mainly for myself but now its available for anyone. Want to find out more, visit our website &lt;a href="https://openpanel.dev" rel="noopener noreferrer"&gt;https://openpanel.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>softwaredevelopment</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why We Ditched Next.js for TanStack Start</title>
      <dc:creator>Carl-Gerhard Lindesvärd</dc:creator>
      <pubDate>Thu, 06 Nov 2025 14:10:25 +0000</pubDate>
      <link>https://dev.to/lindesvard/why-we-ditched-nextjs-for-tanstack-start-4bp7</link>
      <guid>https://dev.to/lindesvard/why-we-ditched-nextjs-for-tanstack-start-4bp7</guid>
      <description>&lt;p&gt;I run &lt;a href="https://openpanel.dev" rel="noopener noreferrer"&gt;OpenPanel.dev&lt;/a&gt;, an open source, privacy friendly analytics tool. So our own dashboard kind of has to feel fast. If your analytics app is slow, nobody will use it.&lt;/p&gt;

&lt;p&gt;For a long time that dashboard ran on Next.js. I have used Next since the early days. It made SSR feel easy. You wrote some React, added &lt;code&gt;getServerSideProps&lt;/code&gt;, shipped it, and it worked. No deep framework knowledge needed.&lt;/p&gt;

&lt;p&gt;That version of Next was nice.&lt;/p&gt;

&lt;p&gt;Then the &lt;strong&gt;App Router&lt;/strong&gt; arrived. Then &lt;strong&gt;React Server Components&lt;/strong&gt;. And everything started to feel... wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Next.js went from "nice" to "what is going on"
&lt;/h3&gt;

&lt;p&gt;The idea behind RSC is clever. In practice it turned our app into a puzzle.&lt;/p&gt;

&lt;p&gt;You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;components that are secretly server only or client only&lt;/li&gt;
&lt;li&gt;promises and Suspense sprinkled in places that are hard to follow&lt;/li&gt;
&lt;li&gt;caching that acts differently depending on which file you put the code in&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;middleware.ts&lt;/code&gt; that does not really behave like middleware in the classic sense&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The old "just works" feeling was gone. Every bug felt like a framework behavior you had not learned yet.&lt;/p&gt;

&lt;p&gt;On top of that, Next.js became the default choice in the React world. Every big voice on X talks about it, tutorials use it by default, companies hire for it. So you feel a bit stupid even for questioning it.&lt;/p&gt;

&lt;p&gt;But here is the problem. Nobody asks a basic question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What kind of app are you building?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are building a marketing site or a mostly static e-commerce store, Next.js is amazing. SEO, images, static pages, all good.&lt;/p&gt;

&lt;p&gt;If you are building an interactive website, that is different. You want instant navigation, predictable state, and simple data loading. Instead you get server round trips on every navigation, cache layers that act like a black box, and stack traces that go from client to server to client again.&lt;/p&gt;

&lt;p&gt;It is not that Next.js is broken. It is just overloaded with ideas that look great in a conference talk but hurt once you have real users.&lt;/p&gt;

&lt;h3&gt;
  
  
  What hurt in practice at OpenPanel
&lt;/h3&gt;

&lt;p&gt;Here is what it felt like day to day while building OpenPanel on Next 14.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Navigation was slow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Switching between pages in the dashboard felt heavier than it should. Even with prefetching. You could feel the server in every click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Caching bugs out of nowhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you fetch data inside an RSC, Next.js decides when it should be revalidated. Sometimes it updates right away. Sometimes it doesn’t. You start throwing &lt;code&gt;revalidatePath&lt;/code&gt; or &lt;code&gt;no-store&lt;/code&gt; everywhere just to make it behave.&lt;/p&gt;

&lt;p&gt;Half the time we couldn’t even tell what was cached. Was it our fetch call? Or the framework holding on to something behind the scenes?&lt;/p&gt;

&lt;p&gt;That might be fine for a static site, but not for a live analytics dashboard...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Development was painful&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dev server startup: 25 to 30 seconds.&lt;br&gt;
Change route: wait a few seconds for the compiler. Repeat. All day.&lt;/p&gt;

&lt;p&gt;You want to tweak a chart, hit save, see it instantly. Instead you get to watch the progress bar and think about your life choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It felt like we were tied to Vercel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, you can self host Next. I know. But the whole thing is clearly designed with Vercel in mind. The DX, the docs, the examples. It all nudges you in that direction.&lt;/p&gt;

&lt;p&gt;We run on Hetzner and lean heavily into the self hosted story, so that feeling of "this framework really wants you on Vercel" did not sit well.&lt;/p&gt;

&lt;p&gt;At some point we realised we were fighting the tool on multiple fronts. Performance, caching, hosting. That is when we decided to try something else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enter TanStack Start
&lt;/h3&gt;

&lt;p&gt;We picked TanStack Start because it promised something boring that we actually wanted.&lt;/p&gt;

&lt;p&gt;Simple routing. Clear data loading. No hidden magic.&lt;/p&gt;

&lt;p&gt;It feels like React from before everything tried to be clever. You get file based routing, nested layouts, data loaders and type safety, without any mystery layers that silently cache or run on the server unless you tell them not to.&lt;/p&gt;

&lt;p&gt;And it is fast.&lt;/p&gt;

&lt;p&gt;Same project, new stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dev server spins up in about 2 seconds&lt;/li&gt;
&lt;li&gt;page changes in development feel instant&lt;/li&gt;
&lt;li&gt;navigation in production is snappy, like a proper SPA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We did the migration in a couple of weeks. Not full time, just squeezing it in while shipping other stuff. Most of the work was not even TanStack itself. We used the chance to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgrade a pile of outdated packages&lt;/li&gt;
&lt;li&gt;move everything from CommonJS to ESM&lt;/li&gt;
&lt;li&gt;clean up some old layout and routing hacks from the Next days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moving the actual pages and routes was surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;Now the OpenPanel dashboard loads way faster and, more important, it feels predictable again. When something is slow or broken, it is our code, not some invisible cache layer or server boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  So when does this actually matter for you
&lt;/h3&gt;

&lt;p&gt;If you are building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a blog&lt;/li&gt;
&lt;li&gt;a marketing page&lt;/li&gt;
&lt;li&gt;a brochure style site&lt;/li&gt;
&lt;li&gt;a simple shop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay on Next.js. Seriously. It is great for those things. You get SEO, image optimisation, static exports, all the goodies.&lt;/p&gt;

&lt;p&gt;If you are building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a dashboard&lt;/li&gt;
&lt;li&gt;a complex tool&lt;/li&gt;
&lt;li&gt;something where users click around a lot and expect instant feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then at least try TanStack Start on a side project. Feel the difference.&lt;/p&gt;

&lt;p&gt;TanStack is getting a lot of attention lately, and for good reason. The team moves fast, the community is growing, and it already has solid backing from sponsors. But it still feels grounded. You can actually read the code and understand what’s happening.&lt;/p&gt;

&lt;p&gt;What you get is a simple mental model, strong TypeScript support, and performance that feels like old school React apps where you just shipped a bundle and called it a day.&lt;/p&gt;

&lt;p&gt;That is why we switched. Not to be trendy. Not to be contrarian. Just to make OpenPanel nicer to work in and nicer to use.&lt;/p&gt;

&lt;p&gt;If Next.js currently feels like a puzzle you never signed up for, you are not alone. There is a simpler option. And for us, TanStack Start has been exactly that.&lt;/p&gt;

&lt;p&gt;If you have any questions, don't be shy, I'll try to answer them the best I can.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>discuss</category>
      <category>react</category>
    </item>
  </channel>
</rss>
