<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Instant Essays</title>
    <link>https://instantdb.com/essays</link>
    <description>Essays from the Instateam</description>
    <language>en-us</language>
    <lastBuildDate>Thu, 26 Mar 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://instantdb.com/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>A backend for AI-coded apps</title>
      <link>https://instantdb.com/essays/architecture</link>
      <guid isPermaLink="true">https://instantdb.com/essays/architecture</guid>
      <description>&lt;p&gt;After 4 years, we’re releasing Instant 1.0!&lt;/p&gt;
&lt;p&gt;Instant turns your favorite coding agent into a full-stack app builder. And we’re fully open source. &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Our claim is that Instant is the best backend you could use for AI-coded apps.&lt;/p&gt;
&lt;p&gt;In this post we’ll do two things. First we’ll show you a series of &lt;a href=&quot;#demos&quot;&gt;demos&lt;/a&gt;, so you can judge for yourself. Second, we’ll cover the &lt;a href=&quot;#architecture&quot;&gt;architecture&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The constraints behind a real-time, relational, and multi-tenant backend pushed us towards some interesting design choices. We built a multi-tenant database on top of Postgres, and a sync engine in Clojure. We’ll cover how all this works and what we’ve learned so far.&lt;/p&gt;
&lt;p&gt;Let’s get into it.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;demos&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Demos&lt;/h1&gt;
&lt;p&gt;When you choose Instant you get three benefits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You can make unlimited apps and they’re never frozen.&lt;/li&gt;
&lt;li&gt;You get a sync engine, so your apps work offline, are real-time, and feel fast.&lt;/li&gt;
&lt;li&gt;And when you need more features you have built-in services: auth, file storage, presence, and streams.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To get a sense of what we mean, I’ll dive into each point and show you how they look.&lt;/p&gt;
&lt;h2&gt;Unlimited Apps&lt;/h2&gt;
&lt;p&gt;Traditionally, when you want to host apps online you either pay for VMs, or you’re limited. Many services cap how many free apps you can make, and freeze them when they’re idle. Unfreezing can often take more than 30 seconds and sometimes a few whole minutes.&lt;/p&gt;
&lt;p&gt;We thought this sucked. So with Instant, you can spin up as many projects as you like and we’ll never freeze them.&lt;/p&gt;
&lt;p&gt;We can do this because Instant is designed to be multi-tenant. When you create a new project, we don’t spin up a VM. We just insert a few database rows in a multi-tenant instance.&lt;/p&gt;
&lt;p&gt;If your app is inactive, there are no compute or memory costs at all. And when it is active, it’s only a few kilobytes of extra RAM in overhead — as opposed to the many hundreds of megabytes required for VMs.&lt;/p&gt;
&lt;p&gt;This means you can truly create unlimited apps. In fact, the process is so efficient that we can create an app for you right inside this essay. No sign up required.&lt;/p&gt;
&lt;p&gt;If you click the button, you’ll get an isolated backend:&lt;/p&gt;
&lt;p&gt;&lt;architecture-demo demo=&quot;create-app&quot;&gt;&lt;/architecture-demo&gt;&lt;/p&gt;
&lt;p&gt;And with that we have our backend. Including the round-trip to your computer, the whole process takes a few hundred milliseconds. Actual time: &lt;architecture-demo demo=&quot;creation-time&quot;&gt;&lt;/architecture-demo&gt;&lt;/p&gt;
&lt;p&gt;You get a public App ID to identify your backend, and a private Admin Token that lets you make privileged changes. This gives you a relational database, sync engine, and the additional services we mentioned, like auth and storage.&lt;/p&gt;
&lt;p&gt;Combine limitless apps with agents, and you’ll start building differently. Today you can already use agents to make lots of apps. With Instant you’ll never be blocked from pushing them to production.&lt;/p&gt;
&lt;h2&gt;Sync Engine&lt;/h2&gt;
&lt;p&gt;But once you create an app, how do you make it good?&lt;/p&gt;
&lt;p&gt;It’s easy to build a traditional CRUD app. Just get an agent to wire up some database migrations, backend endpoints, and client-side stores. But it’s hard to make these apps &lt;em&gt;delightful&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Compare a traditional CRUD app to modern apps like Linear, Notion, and Figma. Modern apps are multiplayer, they work offline, and they feel fast. If you change a todo in Linear, it changes everywhere. If you go offline in Notion, you can still mark up your docs. When you color a shape in Figma, it doesn’t wait for a server, you just see it.&lt;/p&gt;
&lt;p&gt;These kinds of apps need custom infrastructure. For real-time you add stateful websocket servers. For offline mode you store caches in IndexedDB. And for optimistic updates, you figure out how to apply and undo mutations in the client.&lt;/p&gt;
&lt;p&gt;Linear, Notion, and Figma all built custom infra to handle this. As an industry we’ve called their infra sync engines &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;. Developers write UIs and query their data as though it was locally available. The sync engine handles all the data management under the hood.&lt;/p&gt;
&lt;p&gt;If modern apps need sync engines, then you shouldn’t have to build them from scratch each time.&lt;/p&gt;
&lt;p&gt;So we built a generalized sync engine in Instant. Every app comes with multiplayer, offline mode, and optimistic updates by default.&lt;/p&gt;
&lt;p&gt;You can try it yourself. Since we’ve created our isolated backend, let’s go ahead and use it:&lt;/p&gt;
&lt;p&gt;&lt;architecture-demo demo=&quot;todo-iframe&quot;&gt;&lt;/architecture-demo&gt;&lt;/p&gt;
&lt;p&gt;What you’re seeing are two iframes that render a todo app. They’re powered by the backend you just created (we passed the iframes your App ID).&lt;/p&gt;
&lt;p&gt;Now if you add a todo in one iframe, it will show up in the other. If you go offline, you can make changes and they will sync together. You can try degrading your network, and changes will still feel fast.&lt;/p&gt;
&lt;p&gt;And here’s what the todo app’s backend code is like:&lt;/p&gt;
&lt;p&gt;&lt;architecture-demo demo=&quot;todo-code&quot;&gt;&lt;/architecture-demo&gt;&lt;/p&gt;
&lt;p&gt;That’s about &lt;architecture-demo demo=&quot;todo-code-line-count&quot;&gt;&lt;/architecture-demo&gt; lines. This is even more concise than if you had built a traditional CRUD app. You would have needed to write backend endpoints and frontend stores. Instead you just make queries and transactions directly in your frontend.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db.useQuery&lt;/code&gt; lets you write relational queries and they stay in sync. &lt;code&gt;db.transact&lt;/code&gt; lets you make changes and it works offline.&lt;/p&gt;
&lt;p&gt;This is better for you as a builder: the code is understandable and it’s easy to maintain. It’s better for your users: they get a delightful app. And it’s better for your agents. Sync engines are a tight abstraction &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;, so agents can use them to write more concise code with fewer tokens and fewer mistakes.&lt;/p&gt;
&lt;h2&gt;Additional Services&lt;/h2&gt;
&lt;p&gt;You saw data sync, but it doesn’t stop there. Apps often need more than data sync.&lt;/p&gt;
&lt;p&gt;For example, right now every person who opens our demo app sees the same set of todos. What if we want to add auth or permissions? We may also want to support file uploads, or a “who’s online” section. Or heck maybe we add an AI assistant, and would need infra to stream tokens to the client.&lt;/p&gt;
&lt;p&gt;These are common features that most apps need. But often we have to string together different services to get them. Not only is that annoying, but it introduces a new level of complexity. When you manage multiple services, you manage multiple sources of truth.&lt;/p&gt;
&lt;p&gt;So to make it easier to enhance your apps, we baked in a bunch of common services inside Instant. Each service is built to work together as a single, integrated system.&lt;/p&gt;
&lt;p&gt;To get a sense of these services, let’s look at our todo app again, but this time we’ll add support for file uploads:&lt;/p&gt;
&lt;p&gt;&lt;architecture-demo demo=&quot;file-upload&quot;&gt;&lt;/architecture-demo&gt;&lt;/p&gt;
&lt;p&gt;What would be the traditional way to do this? We would first create a &lt;code&gt;files&lt;/code&gt; table in our transactional database, and link it to &lt;code&gt;todos&lt;/code&gt;. But then we would need to store the actual file blobs, so we’d probably add S3.&lt;/p&gt;
&lt;p&gt;Once we add S3, we have multiple sources of truth to deal with. If we delete a todo for example, we’d need to run a background worker to get rid of the corresponding blob in S3.&lt;/p&gt;
&lt;p&gt;With Instant, all of this is a non-issue.&lt;/p&gt;
&lt;p&gt;You get File Storage by default, and file objects are just rows in your database. They’re just like any other entity: you can create them, link them to other data, and run real-time queries against them.&lt;/p&gt;
&lt;p&gt;This means you can even create CASCADE delete rules, so you can say “when you delete todos, delete files”. There’s no need for background workers. Instead of multiple sources of truth, you get one integrated database. The shared infra handles all the edge cases under the hood &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;And this is just Instant Storage. You also get Auth. You can use Magic Codes, OAuth, and Guest Auth out of the box. Plus when your users sign up, they’re just rows in your database too.&lt;/p&gt;
&lt;p&gt;If you want to share cursors, typing indicators, or ‘who’s online’ markers, you can use Instant Presence.&lt;/p&gt;
&lt;p&gt;And if you need to share durable streams, you get, well, Instant Streams.&lt;/p&gt;
&lt;p&gt;If you’re curious, we have a bunch of real examples you can play with in the &lt;a href=&quot;/recipes&quot;&gt;recipes&lt;/a&gt; page. You’ll notice that most of these services require little setup and little code. Both you and your agents can move faster and make your apps feature-rich. You don’t have to scour for different providers and deal with bi-directional data sync.&lt;/p&gt;
&lt;h2&gt;Bonus: What you can do, your agent can do&lt;/h2&gt;
&lt;p&gt;Throughout this essay, you may have wondered, how do all these demos work?&lt;/p&gt;
&lt;p&gt;Well, Instant is completely programmatic. You can create apps, push schemas and update permissions either through an API or a CLI. This essay uses the API, but likely your agents will use the CLI.&lt;/p&gt;
&lt;p&gt;Most of the time you don’t have to click any dashboards. Your agents can just take actions on your behalf.&lt;/p&gt;
&lt;p&gt;At this point, we hope you’re excited enough to &lt;a href=&quot;/dashboard&quot;&gt;sign up&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You technically don’t even need to sign up to play around, but we do notice that if you do, you’re more likely to stick around. So we really encourage you to!&lt;/p&gt;
&lt;p&gt;And if you want to get your agents playing with Instant right away, here are a few things you can do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# This scaffolds a new starter for you in either NextJS, Tanstack, Bun, Vite, or Expo
# Your agent will have everything it needs to build
npx create-instant-app

# If you have an existing app, you can also add our handy skill and tell your agent
# to make some new features
npx skills add instantdb/skills
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, we can dive into the architecture that powers all of this.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;architecture&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Architecture&lt;/h1&gt;
&lt;p&gt;There are three unique things about how Instant works. We have the Client SDK, the Clojure Backend, and the Multi-Tenant Database.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775735518061_CleanShot+2026-04-09+at+04.51.412x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Your app sends queries and transactions directly to the Client SDK. It’s responsible for resolving your queries offline, and for applying transactions as soon as you make them.&lt;/p&gt;
&lt;p&gt;The Client SDK then talks to The Clojure Backend. The Clojure Backend keeps queries real-time. It takes transactions and figures out which clients need to know about them. It also implements all the additional services: permissions, auth, presence, storage, and streams.&lt;/p&gt;
&lt;p&gt;Finally, The Clojure Backend sends queries and transactions to a single Postgres Instance. We treat Postgres as a multi-tenant Triple store, and logically separate every database by App ID.&lt;/p&gt;
&lt;p&gt;That’s the sketch of our system. Now let’s get deeper.&lt;/p&gt;
&lt;h1&gt;The Client SDK&lt;/h1&gt;
&lt;p&gt;The design behind the Client SDK is motivated by two constraints: we need a system that works offline, and we need it to support optimistic updates.&lt;/p&gt;
&lt;p&gt;Here’s roughly where we ended up:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775710460513_image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;IndexedDB&lt;/h2&gt;
&lt;p&gt;Let’s start with the most obvious box. If we want to show the app offline, we need a place for data to live across refreshes.&lt;/p&gt;
&lt;p&gt;For the web you don’t have too many choices. IndexedDB is the best candidate. You can store many megabytes of data, and you even have some limited querying capabilities.&lt;/p&gt;
&lt;p&gt;So we chose IndexedDB &lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;. The next question was, what kind of data would we store there?&lt;/p&gt;
&lt;h2&gt;Triple store&lt;/h2&gt;
&lt;p&gt;Consider a query like “Show me all the open todos and their attachments”. This is how you would write it in Instant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
  todos: {
    $: { where: { done: false } },
    attachments: { },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we just wanted a read-only cache, we could store whatever the server returns to us. But we don’t just want a read-only cache.&lt;/p&gt;
&lt;p&gt;We need the client to respond to actions before the server acknowledges them. If a user adds a new todo for example, our query should just update right away.&lt;/p&gt;
&lt;p&gt;That means the client needs to understand queries. So then what our client really needs is a database itself. A database that can handle where clauses (i.e., ‘done is false’), and relations (‘todos &lt;em&gt;and&lt;/em&gt; their attachments’).&lt;/p&gt;
&lt;p&gt;One option would have been to use SQLite. We could store normalized tables there — like &lt;code&gt;todos&lt;/code&gt;, and &lt;code&gt;files&lt;/code&gt; — and run SQL over them. But this was too heavy. SQLite is about 300 KB gzipped. For most apps it wouldn’t make sense to add such a heavy dependency.&lt;/p&gt;
&lt;p&gt;After some sleuthing though we discovered Triple stores and Datalog.&lt;/p&gt;
&lt;p&gt;Triple stores let you store data as &lt;code&gt;[entity, attribute, value]&lt;/code&gt; tuples. Here’s what todos would look like inside a Triple store:&lt;/p&gt;
&lt;p&gt;&lt;triple-demo&gt;&lt;/triple-demo&gt;&lt;/p&gt;
&lt;p&gt;This uniform structure can model both attributes and relationships. Once data is stored in this way, you can use Datalog to make queries against it.&lt;/p&gt;
&lt;p&gt;Datalog is a logic-based query engine. Here’s what that looks like:&lt;/p&gt;
&lt;p&gt;&lt;datalog-demo&gt;&lt;/datalog-demo&gt;&lt;/p&gt;
&lt;p&gt;The syntax looks weird, but Datalog is powerful. It can support where clauses and relations just as well as SQL. And it’s simple to implement. In fact, you can write a basic Datalog engine in less than a hundred lines of code &lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So we built a Triple store and a Datalog engine. This lets us evaluate queries completely in the client, without having to wait for the server.&lt;/p&gt;
&lt;p&gt;If a user creates a new todo, we have what we need to re-run the query and observe the change right away. Well, almost. We need a way to apply changes to our query.&lt;/p&gt;
&lt;h2&gt;Pending Queue&lt;/h2&gt;
&lt;p&gt;We can’t just mutate the result in place. We have to be mindful of the server too.&lt;/p&gt;
&lt;p&gt;For example, what would happen if the server rejects our transaction? If we mutated the query result, there would be no way for us to undo the change. &lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;That’s where the Pending Queue comes in. When a user makes a change, we don’t apply it directly to the Triple store. Instead we track the change in a separate queue.&lt;/p&gt;
&lt;p&gt;To satisfy any query, we can apply pending changes to our triple store, and see the result:&lt;/p&gt;
&lt;p&gt;&lt;pending-queue-demo&gt;&lt;/pending-queue-demo&gt;&lt;/p&gt;
&lt;p&gt;This choice pushes us to make our Triple store immutable. This way we can apply the change and produce a new Triple store, rather than mutating the committed one. To make this work, we wrap the transact API with mutative, a library for immutable changes in Javascript &lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;With that we have undo. If the server returns a failure, we simply remove the change from the pending queue and undo works out of the box.&lt;/p&gt;
&lt;h2&gt;Bonus: InstaQL&lt;/h2&gt;
&lt;p&gt;You may have noticed that Instant queries don’t look like Datalog though. Instead they’re written in a language we call InstaQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;{
  todos: {
    $: { where: { done: false } },
    attachments: { },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We made this because we thought that the most ergonomic way for apps to query for data was to describe the shape of the response they were looking for.&lt;/p&gt;
&lt;p&gt;This idea was heavily inspired by GraphQL. The main difference with our implementation is syntax sugar. Instead of introducing a specific grammar, InstaQL is built on top of plain javascript objects. This choice lets users skip a build step, and it lets them generate queries programmatically &lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;Reactor&lt;/h2&gt;
&lt;p&gt;With that, we have a somewhat full view of the Client SDK!&lt;/p&gt;
&lt;p&gt;Users write InstaQL queries, which get turned into Datalog. Those queries are satisfied by Triple stores, which combine changes from a pending queue. Data gets cached to IndexedDB.&lt;/p&gt;
&lt;p&gt;That’s a lot of interesting choices generated from just two constraints!&lt;/p&gt;
&lt;p&gt;The final question on the client is this: how do all these boxes tie together?&lt;/p&gt;
&lt;p&gt;That’s where the Reactor comes in. It’s the main state machine that coordinates all these different processes. When an app wants a query, the Reactor is responsible for looking at IndexedDB, and for communicating with the server. It handles when the internet goes offline or pending changes fail.&lt;/p&gt;
&lt;p&gt;The Reactor communicates to the server through websockets. It sends requests for queries and transactions, and the server sends results and novelty from the database.&lt;/p&gt;
&lt;p&gt;Which brings us to the server.&lt;/p&gt;
&lt;h1&gt;Clojure Backend&lt;/h1&gt;
&lt;p&gt;The design behind the backend is motivated by two constraints: we need to make queries reactive, and we need to be fair about multi-tenant resources.&lt;/p&gt;
&lt;p&gt;Here’s roughly how the system looks:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775737048834_image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Query Store&lt;/h2&gt;
&lt;p&gt;Let’s start by thinking through what happens when a user asks for a query.&lt;/p&gt;
&lt;p&gt;First the server can go ahead and ask the database. In a stateless system that would be just about the end of the story. We could return our response and call it a day.&lt;/p&gt;
&lt;p&gt;But remember, our queries have to be reactive. For that we need a place to store &lt;em&gt;which&lt;/em&gt; users have made &lt;em&gt;which&lt;/em&gt; queries. That’s what the Query Store is for:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775739554767_CleanShot+2026-04-09+at+05.59.052x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If we were to track just the queries and the socket connections that asked for them, in principle we would have what we need to make an app reactive. For example we could tail every transaction and refresh every query. That would work, but our database would get hammered with lots of spam.&lt;/p&gt;
&lt;p&gt;Ideally, we should only change queries that &lt;em&gt;need&lt;/em&gt; to be changed.&lt;/p&gt;
&lt;h2&gt;Topics&lt;/h2&gt;
&lt;p&gt;We scoured around for ideas, and found the architecture behind Asana’s Luna &lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt; and Figma&amp;#39;s LiveGraph &lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt; very promising. Asana wrote about how they turn queries into sets of “topics”. Roughly, a topic describes the part of the index that the query in question cares about.&lt;/p&gt;
&lt;p&gt;For something like “Give me all todos”, you could imagine a topic that says: “Track all updates to the TodosIndex”.&lt;/p&gt;
&lt;p&gt;We adapted this idea into our system. When we run queries, we also generate a set of topics that it cares for:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775739641762_CleanShot+2026-04-09+at+06.00.272x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here’s our topic for “Watch all todos”:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775744562817_CleanShot+2026-04-09+at+07.21.592x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now we have a data structure we can use to describe the dependencies for a query. The next step is to track transactions and find these affected queries.&lt;/p&gt;
&lt;h2&gt;Invalidator&lt;/h2&gt;
&lt;p&gt;That’s where the invalidator comes in. The invalidator tracks Postgres’ WAL (Write-Ahead Log).&lt;/p&gt;
&lt;p&gt;We can take WAL entries and generate topics from them too. For example, if we had an update like “Set todo.done = false for id = 42’”, we could transform it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775743743474_CleanShot+2026-04-09+at+07.08.572x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This gets us the exact same kind of topic structure that our queries make. Now we can match them together, and discover what’s stale:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775744091203_CleanShot+2026-04-09+at+07.14.462x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Our version zero for this algorithm was very inefficient. We would effectively do an N^2 comparison from every transaction topic to every query topic. But you can intuit how these topic vectors are amenable to indexes. We now keep them in a tree-like structure. We only compare subsets and we prune early. &lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;With that we can take a WAL entry and refresh queries based on them. The next step is to parallelize.&lt;/p&gt;
&lt;h2&gt;Grouped Queues&lt;/h2&gt;
&lt;p&gt;Since our database is multi-tenant, our WAL includes updates from multiple apps.&lt;/p&gt;
&lt;p&gt;In order for the invalidation algorithm to work, transactions &lt;em&gt;within&lt;/em&gt; a single app have to be processed serially and in order. But, we can certainly parallelize invalidations across &lt;em&gt;different&lt;/em&gt; apps.&lt;/p&gt;
&lt;p&gt;We needed some way to guarantee order within a single app and parallelize across apps. We also needed to make sure that one high-traffic app didn’t hog all resources.&lt;/p&gt;
&lt;p&gt;This is where the Grouped Queue abstraction comes in:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775746073426_CleanShot+2026-04-09+at+07.47.442x.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Each app gets its own subqueue. This guarantees that all items for a particular app are handled serially.&lt;/p&gt;
&lt;p&gt;Workers however can take from multiple different subqueues. This lets us parallelize invalidations across apps.&lt;/p&gt;
&lt;p&gt;When we push a WAL entry into the grouped queue, it gets added to the app’s subqueue, but the global order of the subqueue does not change. This makes it so even if one app is adding thousands of items per second, other apps still get an equal chance to get picked up by an invalidator.&lt;/p&gt;
&lt;p&gt;This data structure has turned out to be very useful for us, and has seeped all across the code base, including the Session Manager.&lt;/p&gt;
&lt;h2&gt;The Session Manager, and Praise for Clojure and the JVM&lt;/h2&gt;
&lt;p&gt;Which brings us to the main coordinator inside the system. When the Client SDK opens up a websocket connection, it’s the session manager that picks up the messages:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775747390276_image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Session Manager’s job is to glue everything together. It makes reactive queries, it runs permissions, and it passes along requests to the other services.&lt;/p&gt;
&lt;p&gt;Notice the Grouped Queue abstraction makes an appearance here too. If different clients start bombarding the backend, the Grouped Queue makes sure to both parallelize as much as possible, and to prevent one bad socket from hogging all the resources.&lt;/p&gt;
&lt;p&gt;And with this it may be the right place to pause and praise Clojure and the JVM. They’ve been a huge win for us in building this infrastructure.&lt;/p&gt;
&lt;p&gt;First, Clojure comes with great concurrency primitives and has real threads. This lets us scale further with bigger machines and helped us avoid splitting the system up too early. The abstractions are also really simple and easy to compose. Our grouped queue for example is only 215 lines of code &lt;sup id=&quot;user-content-fnref-13&quot;&gt;&lt;a href=&quot;#user-content-fn-13&quot;&gt;[13]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Second, the JVM has a thriving ecosystem and we really enjoy the libraries. For example, we needed a way for users to define permissions inside Instant. We wanted a language that would be fast and easy to sandbox. After some searching, we discovered Google’s CEL. Thankfully CEL Java was available, and we could just pick it off the shelf.&lt;/p&gt;
&lt;p&gt;And third, Clojure is great for DSLs and for experimental programming. When we started building Instant we had to discover a lot of these abstractions, and playing with them in the REPL was instrumental.&lt;/p&gt;
&lt;p&gt;Many folks deride DSLs but I think we couldn’t have built Instant without them. Case in point: multi-tenant queries. We needed to make our database multi-tenant. To do that we would need to write some pretty complex SQL. Rather than do this by hand, we made a DSL that both made it easy to reason about, and guaranteed that you could pass in an App ID.&lt;/p&gt;
&lt;p&gt;And this brings us to the Multi-Tenant Database.&lt;/p&gt;
&lt;h1&gt;The Multi-Tenant Database&lt;/h1&gt;
&lt;p&gt;Our database was also motivated by two constraints: we needed a way to spin new databases cheaply, and we needed it to be relational.&lt;/p&gt;
&lt;p&gt;Here’s where we ended up:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://paper-attachments.dropboxusercontent.com/s_331134A1AB81F48C9BB3AF9F0C08F3485C408CA845F0A79093D4B651B8B202E3_1775751529695_image.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The Triples Table&lt;/h2&gt;
&lt;p&gt;Let’s start with the question: how can we let users create lots of different databases?&lt;/p&gt;
&lt;p&gt;The most straight forward path would have been to spin up Postgres VMs. But as we mentioned, VMs come with lots of overhead in RAM. There’s no sustainable way to support unlimited apps if you’re spinning up VMs.&lt;/p&gt;
&lt;p&gt;Another option would have been to use Postgres schemas. We could have created different tables for different apps, and then kept a mapping of who can see what. This would work, but Postgres wasn’t designed to scale well with tables. From our research we saw that after about 6000 tables, Postgres starts having issues: you get problems with how many files get created on disk, and pg_dump and autovacuum starts failing.&lt;/p&gt;
&lt;p&gt;This makes sense. The average Postgres app has a few big tables, not many small tables, which means big tables get optimized. Well, if big tables work, what if we reframed this problem into a giant table?&lt;/p&gt;
&lt;p&gt;And this brings us back to…Triple stores!&lt;/p&gt;
&lt;p&gt;They worked well on the client because they’re a simple DB that supports relational queries. We thought this could work well for us in Postgres too. So we added a &lt;code&gt;triples&lt;/code&gt; table:&lt;/p&gt;
&lt;p&gt;&lt;multi-tenant-demo&gt;&lt;/multi-tenant-demo&gt;&lt;/p&gt;
&lt;p&gt;All the data lives in a single &lt;code&gt;triples&lt;/code&gt; table, and they’re logically isolated by an &lt;code&gt;app_id&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If we wanted to get &lt;code&gt;post_1&lt;/code&gt; from the app &lt;code&gt;blog&lt;/code&gt; for example, we could generate a SQL query that looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select *
from triples
where app_id = &amp;#39;blog&amp;#39; and entity_id = &amp;#39;post_1&amp;#39; and attr_id in (posts/id, posts/title)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that, creating a new database is effectively free. Just as we mentioned in the demos, it’s a few rows in the database.&lt;/p&gt;
&lt;h3&gt;Surprising benefits&lt;/h3&gt;
&lt;p&gt;Our choice came with some surprising benefits too.&lt;/p&gt;
&lt;p&gt;Since we manage columns ourselves, we were able to optimize the developer experience.&lt;/p&gt;
&lt;p&gt;For example, Postgres locks the table when you create a column. Since we implemented columns ourselves, we could make them lock-free.&lt;/p&gt;
&lt;p&gt;When you delete a column in Postgres, the data is gone. But we thought this was way too dangerous in the world of agents. So we implemented soft deletes at the column level. Even if a rogue agent deletes your columns, you can undo it and get all your data back in milliseconds.&lt;/p&gt;
&lt;p&gt;These were the benefits, but of course there were costs too.&lt;/p&gt;
&lt;h2&gt;Partial Indexes&lt;/h2&gt;
&lt;p&gt;Consider a user who says, “I want my posts to have a unique ‘slug’”. In Postgres it’s easy to create unique columns. But since we’re implementing our own columns, we have to do this ourselves.&lt;/p&gt;
&lt;p&gt;This is where partial indexes came to the rescue. We could add boolean markers to our &lt;code&gt;triples&lt;/code&gt; table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;table_name: triples
app_id | entity_id | attr_id | value | column_unique | ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have that, we can create a partial index for the whole table, flipped on by the marker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create unique index unique_columns
  on triples(app_id, column, value) where column_unique
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if a user tries to insert two posts with the same slug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;app_id  | object_id | column | value   | column_unique
&amp;#39;blog&amp;#39; | 1         | &amp;#39;slug&amp;#39; | &amp;#39;hello&amp;#39; | true
&amp;#39;blog&amp;#39; | 2         | &amp;#39;slug&amp;#39; | &amp;#39;hello&amp;#39; | true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;unique_columns&lt;/code&gt; index triggers and prevents it!&lt;/p&gt;
&lt;p&gt;And this same trick makes our queries more efficient. If we want to find posts with the slug ‘hello’ for example, we can generate this query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select entity_id
from triples
where app_id = &amp;#39;blog&amp;#39; and attr_id = &amp;#39;slug&amp;#39; and value = &amp;#39;hello&amp;#39; and column_unique;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we can extend this pattern to a whole range of queries: unique columns, indexes, dates, references, and so on.&lt;/p&gt;
&lt;p&gt;Just using partial indexes and relying on Postgres to make the right queries worked great for us for a while. But after we reached a few hundred million tuples in scale, Postgres started having troubles.&lt;/p&gt;
&lt;h2&gt;Count-Min Sketches&lt;/h2&gt;
&lt;p&gt;If you are a Postgres expert reading this, you may have taken a pause looking at that triples table. In Postgres circles this is called the EAV pattern, and is generally discouraged.&lt;/p&gt;
&lt;p&gt;It’s discouraged because Postgres relies on tables and columns for statistics.&lt;/p&gt;
&lt;p&gt;Those statistics are what let the query planner decide which indexes are most efficient and which joins to do in what order.&lt;/p&gt;
&lt;p&gt;Once you keep all data in one table, Postgres loses information about the underlying frequencies in the dataset. It can&amp;#39;t tell the difference between a column with 10 distinct values and one with 10 million.&lt;/p&gt;
&lt;p&gt;To solve for this, we started keeping track of our statistics. We use a data structure called count-min sketches, which help us estimate frequencies for columns. If you’re curious about how that works, we wrote an essay about it &lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;We could give those statistics to our query engine, and make those queries efficient again.&lt;/p&gt;
&lt;h2&gt;The Query Engine&lt;/h2&gt;
&lt;p&gt;Which brings us to the query engine.&lt;/p&gt;
&lt;p&gt;So far I’ve been showing you SQL queries that are simple and easy to understand. But imagine translating more complicated InstaQL queries. Even a query with one where clause will start to have CTEs in them. And then you’ll want to use those statistics to decide which indexes to turn on.&lt;/p&gt;
&lt;p&gt;That’s what the query engine does. It takes InstaQL queries as well as the count-min sketches, and generates SQL query plans:&lt;/p&gt;
&lt;p&gt;&lt;sql-demo&gt;&lt;/sql-demo&gt;&lt;/p&gt;
&lt;p&gt;This engine is written in the Clojure backend. We took a lot of inspiration from Postgres’ own query engine. Sometimes these queries can look scarily long, but we have been so darn surprised with how well Postgres can handle them. We pass in some hints with pg_hint_plan, and Postgres just churns away and produces results.&lt;/p&gt;
&lt;h1&gt;Four Years in the Making&lt;/h1&gt;
&lt;p&gt;And that covers the database, which covers our whole system!&lt;/p&gt;
&lt;p&gt;We hope you found this fun! This has been a labor of love. We’ve built Instant because we want to power the next generation of builders. Any product we build, we built with Instant, and thousands of developers have trusted to run their core infrastructure.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building with agents, I think you will love using us.&lt;/p&gt;
&lt;p&gt;We hope you give us a &lt;a href=&quot;/dashboard&quot;&gt;try&lt;/a&gt;, and join us on &lt;a href=&quot;/discord&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: Every single line of code behind the company lives on GitHub, including this &lt;a href=&quot;https://github.com/instantdb/instant/blob/main/client/www/_posts/architecture.md&quot;&gt;post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;: Nikita wrote a great blog post about this &lt;a href=&quot;https://www.instantdb.com/essays/sync_future&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-3&quot; href=&quot;#user-content-fnref-3&quot;&gt;[3]&lt;/a&gt;  LLMs have already learned about Instant in their training data, but there really isn’t that much to learn. Queries and transactions have a predictable DSL.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-4&quot; href=&quot;#user-content-fnref-4&quot;&gt;[4]&lt;/a&gt;  Fun fact, your files are still stored in S3. Since both services are built together though, the system can handle bi-directional data sync on your behalf!&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-5&quot; href=&quot;#user-content-fnref-5&quot;&gt;[5]&lt;/a&gt;  On React Native we use react-native-async-storage, because it&amp;#39;s available on Expo Go. The API for storage is pluggable though, so you can replace this pretty easily.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;: Check out &lt;a href=&quot;https://www.instantdb.com/essays/datalogjs&quot;&gt;Datalog in Javascript&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;: There would be a lot more problems too. Check out the &lt;a href=&quot;https://www.instantdb.com/product/sync&quot;&gt;sync engine&lt;/a&gt; page, especially the conflict resolution demo.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://mutative.js.org/&quot;&gt;https://mutative.js.org/&lt;/a&gt; -- it&amp;#39;s a great library!&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-9&quot; href=&quot;#user-content-fnref-9&quot;&gt;[9]&lt;/a&gt;  This came very handy in our Explorer page. You can switch around a bunch of filters, and we&amp;#39;ll dynamically generate the query for it.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;: See this &lt;a href=&quot;https://blog.asana.com/2020/09/worldstore-distributed-caching-reactivity-part-2/&quot;&gt;post&lt;/a&gt; to get started on the rabbit hole.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt;: See this great &lt;a href=&quot;https://www.figma.com/blog/livegraph-real-time-data-fetching-at-figma/&quot;&gt;essay&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-12&quot; href=&quot;#user-content-fnref-12&quot;&gt;[12]&lt;/a&gt;  We do some even more cool things. For example we take where clauses and transform them into little programs for additional filtering.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-13&quot;&gt;&lt;a href=&quot;#user-content-fn-13&quot;&gt;[13]&lt;/a&gt;&lt;/sup&gt;: Check out the &lt;a href=&quot;https://github.com/instantdb/instant/blob/main/server/src/instant/grouped_queue.clj&quot;&gt;source&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt;: Check out &lt;a href=&quot;https://www.instantdb.com/essays/count_min_sketch&quot;&gt;Count-Min Sketches in JS&lt;/a&gt;&lt;/p&gt;
</description>
      <pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate>
      <author>Joe Averbukh, Stepan Parunashvili, Daniel Woelfel, Drew Harris</author>
    </item>
    <item>
      <title>Counter-Strike Bench: GPT 5.3 Codex vs Claude Opus 4.6</title>
      <link>https://instantdb.com/essays/codex_53_opus_46_cs_bench</link>
      <guid isPermaLink="true">https://instantdb.com/essays/codex_53_opus_46_cs_bench</guid>
      <description>&lt;p&gt;&lt;em&gt;We&amp;#39;re Instant. We give you and your agents unlimited databases and backends. Build whatever you like, from your next startup to an alternative Counter-Strike. &lt;a href=&quot;https://instantdb.com/dash&quot;&gt;Sign up&lt;/a&gt; and build your first app in minutes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GPT 5.3 Codex and Claude Opus 4.6 shipped within minutes of each other. How well do they compare building a multiplayer Counter-Strike?&lt;/p&gt;
&lt;p&gt;We tried and found out. Here&amp;#39;s how the gameplay feels:&lt;/p&gt;
&lt;iframe
  src=&quot;https://player.mux.com/w8hs9hQH902Vd01GU7zluTWVidil02BuV5opvzIz1XWc600?metadata-video-title=cs_bench_feb&amp;video-title=cs_bench_feb&amp;thumbnail-time=9&quot;
  style=&quot;width: 100%; border: none; aspect-ratio: 167/108;&quot;
  allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
  allowfullscreen
&gt;&lt;/iframe&gt;

&lt;p&gt;And if you&amp;#39;re curious, you can play it yourself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GPT 5.3 Codex&amp;#39;s attempt:&lt;/strong&gt; &lt;a href=&quot;https://53strike.vercel.app/&quot;&gt;https://53strike.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Opus 4.6&amp;#39;s attempt&lt;/strong&gt; &lt;a href=&quot;https://46strike.vercel.app/&quot;&gt;https://46strike.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We have a full recording of us building it &lt;a href=&quot;https://youtu.be/Kyef-cUUB0Q&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what surprised us:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Both models were a leap over any previous generation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can compare the results with our &lt;a href=&quot;/essays/agents_building_counterstrike&quot;&gt;last&lt;/a&gt; benchmark. These models made much more realistic maps on the first try. Their weapons were better. And they got much more right on the first shot. Codex had some issues with accounting for HP under respawns, and Claude has some issues spawning inside obstacles. But a simple paste got them fixing it. At no point did they get stuck and require guidance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPT 5.3 Codex was much faster than Claude&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In just about every prompt GPT 5.3 Codex finished in about half the time. This could be because of the harness: We noticed Claude Code did much more upfront research than Codex.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Opus 4.6 performed better on 5/6 prompts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But perhaps the upfront research came to good use, because Claude Opus 4.6 beat out GPT 5.3 Codex on all prompts but one (and the last one was a tie).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;GPT 5.3 Codex&lt;/th&gt;
&lt;th&gt;Claude Opus 4.6&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boxes + Physics&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gun + Creativity&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sounds + Animations&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiplayer&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bonus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🤝&lt;/td&gt;
&lt;td&gt;🤝&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Claude drew more interesting maps. Claude made a nicer weapon. The gameplay UI was much nicer on Claude&amp;#39;s first try.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Both models struggled a bit with physics&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At this point, neither model had issues drawing out the UI, setting up the backend, or getting caught up in bugs from three.js. The frontier now seems to be about physics.&lt;/p&gt;
&lt;p&gt;For example, Claude generated maps where players could end up stuck. Here&amp;#39;s Claude&amp;#39;s &amp;quot;inferno valley&amp;quot; and &amp;quot;nuke zone&amp;quot; produced 4-wall obstacles in the center:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/cs_feb/maps.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There would be no way for users to leave. Codex also had trouble with direction. The enemy&amp;#39;s &amp;quot;point of view&amp;quot; was coming out from the back of their had, rather than the front.&lt;/p&gt;
&lt;p&gt;With both models you could shoot through obstacles. Claude Opus 4.6 at least made it so you couldn&amp;#39;t walk through the obstacles -- but with Codex you could.&lt;/p&gt;
&lt;p&gt;With either one, it was fun to build and play!&lt;/p&gt;
</description>
      <pubDate>Thu, 05 Feb 2026 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Team features are free through the end of Februrary</title>
      <link>https://instantdb.com/essays/free_teams_through_february</link>
      <guid isPermaLink="true">https://instantdb.com/essays/free_teams_through_february</guid>
      <description>&lt;p&gt;February is all about celebrating the people you care for, so we&amp;#39;re making team features free through the end of February. You can invite as many members as you like to your free Instant apps and orgs as long as you add them before the end of February.&lt;/p&gt;
&lt;p&gt;At the end of of February, the members you added will still be able to access your app. You just won&amp;#39;t be able to add new members until you convert to a paid app.&lt;/p&gt;
&lt;p&gt;This is an experiment for us. We&amp;#39;re hoping that free teams will encourage people to add their team members to their Instant apps earlier. It takes a lot to convince your coworkers to use a new database. If we make it easy to bring them in earlier, before you&amp;#39;re required to put down a credit card, will companies be more likely to adopt Instant for their projects? We&amp;#39;re going to find out!&lt;/p&gt;
</description>
      <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
      <author>Instant</author>
    </item>
    <item>
      <title>GPT 5.2 on the Counter-Strike Benchmark</title>
      <link>https://instantdb.com/essays/gpt_52_on_the_counterstrike_benchmark</link>
      <guid isPermaLink="true">https://instantdb.com/essays/gpt_52_on_the_counterstrike_benchmark</guid>
      <description>&lt;p&gt;About 2 weeks ago we asked Codex 5.1 Max, Claude 4.5 Opus, and Gemini 3 Pro to &lt;a href=&quot;/essays/agents_building_counterstrike&quot;&gt;build Counter Strike&lt;/a&gt;. It had to be a 3D UI, and it had to be multiplayer.&lt;/p&gt;
&lt;p&gt;How good of a job does GPT 5.2 do at this task?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here&amp;#39;s the TL:DR:&lt;/strong&gt; Even though GPT 5.2 is not a coding model, it did better than Codex 5.1 Max on almost every prompt. GPT 5.2 was still behind Claude on frontend changes, but it began to go toe-to-toe with Gemini on the backend.&lt;/p&gt;
&lt;p&gt;You can try out the version that GPT 5.2 built here:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;GPT 5.2&amp;#39;s first attempt&lt;/strong&gt;: &lt;a href=&quot;https://codex52strike.vercel.app/&quot;&gt;https://codex52strike.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A second version with a map reminiscient of de dust 2&lt;/strong&gt;: &lt;a href=&quot;https://codex52dust.vercel.app/&quot;&gt;https://codex52dust.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&amp;#39;s a full &lt;a href=&quot;https://www.youtube.com/watch?v=MeKBO9QOUFA&quot;&gt;video&lt;/a&gt; of us going through the build, but for those who prefer text you get this post.&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;We evaluated GPT 5 on the Codex CLI set to medium. &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;. All prompts were the same as our last benchmark post. Take a look at how the leaderboard &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt; changed with GPT 5.2:&lt;/p&gt;
&lt;p&gt;&lt;gpt52-leaderboard&gt;&lt;/gpt52-leaderboard&gt;&lt;/p&gt;
&lt;p&gt;Claude still holds the top position on frontend changes (maps, characters, and threejs).&lt;/p&gt;
&lt;p&gt;But GPT 5.2 did much better than it&amp;#39;s predecessor overall. GPT 5.2&amp;#39;s frontend changes were noticeably better than Codex 5.1 Max.&lt;/p&gt;
&lt;p&gt;And the backend changes were about as good as Gemini 3 Pro: both of them effectively one shotted multiplayer positions, shots, and maps.&lt;/p&gt;
&lt;p&gt;You can see for yourself: let&amp;#39;s dive into the prompts.&lt;/p&gt;
&lt;h2&gt;1. Boxes and Physics&lt;/h2&gt;
&lt;p&gt;The first thing we asked it to do was build a basic 3D map with polygons.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to create a browser-based version of counter strike, using three js.&lt;/p&gt;
&lt;p&gt;For now, just make this local: don&amp;#39;t worry about backends, Instant, or
anything like that.&lt;/p&gt;
&lt;p&gt;For the first version, just make the main character a first-person view with
a cross hair. Put enemies at random places. Enemies have HP. You can
shoot them, and kill them. When an enemy is killed, they respawn.&lt;/p&gt;
&lt;p&gt;Make everything simple polygons -- rectangles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GPT 5.2 got one type error in it&amp;#39;s first try. But we pasted the error back it built a working frontend.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s GPT 5.2 versus it&amp;#39;s predecessor:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex 5.1 Max&lt;/th&gt;
&lt;th&gt;GPT 5.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_codex.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike_52/map_gpt52.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;GPT 5.2 makes clear improvement over Codex 5.1 Max. If you compare this to Claude and Gemini, we think Claude still did the best job. The map and the lighting look the most interesting. But at this point it feels like GPT 5.2 did about as well as Gemini:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claude 4.5 Opus&lt;/th&gt;
&lt;th&gt;Gemini 3 Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_claude.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_gemini.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;2. Characters&lt;/h2&gt;
&lt;p&gt;The next challenge was to make the characters more interesting. Instead of a simple box, we wanted enemis that looked like people:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to make the enemies look more like people. Use a bunch of square polygons to represent a person, and maybe a little gun&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GPT 5.2 improved a bunch here:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex 5.1 Max&lt;/th&gt;
&lt;th&gt;GPT 5.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_codex.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike_52/character_gpt52.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;That&amp;#39;s a noticeable improvement in our book. If you compare to Claude and Gemini, it feels like Claude still wins, but GPT 5.2 is about as good as Gemini again:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claude 4.5 Opus&lt;/th&gt;
&lt;th&gt;Gemini 3 Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_claude.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_gemini.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;3. Gun in our field-of-view&lt;/h2&gt;
&lt;p&gt;Next up was adding a gun in our field of view alongside an animation when we shoot:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to make it so I also have a gun in my field of view. When I shoot, the gun moves a bit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We didn&amp;#39;t notice much of an improvement here. In fact, GPT 5.2 had an error, when 5.1 Max got it done in one shot. Here&amp;#39;s the side-by-side with it&amp;#39;s predecessor:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex 5.1 Max&lt;/th&gt;
&lt;th&gt;GPT 5.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_codex.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike_52/recoil_gpt_52.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;It&amp;#39;s interesting to note that the error it had was similar to Gemini&amp;#39;s (troubles attaching the gun to the field of view).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claude 4.5 Opus&lt;/th&gt;
&lt;th&gt;Gemini 3 Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_claude.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_gemini.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;In our last test Gemini 3 Pro got really stuck here, so despite the slight error from 5.2, the rankings didn&amp;#39;t change.&lt;/p&gt;
&lt;h2&gt;3.Adding sounds and animations&lt;/h2&gt;
&lt;p&gt;The final challenge for the frontend was sounds and animations:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to use chiptunes to animate the sound of shots. I also want to animate deaths.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&amp;#39;s predecessor vs 5.2:&lt;/p&gt;
&lt;div class=&quot;essay-breakout grid grid-cols-1 gap-4 md:grid-cols-2&quot;&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Codex 5.1 Max&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/UTCslk3hyNOnXSVlZmodcIqkFoHwgkPIl007aJxQINJ00?metadata-video-title=sound_codex&amp;video-title=sound_codex&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 885/626;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;GPT 5.2&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/BnBA9Hg4HQgo19Ey8Ul02GnFzhTGZGTLOBY3shHvaF4Q&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 1024/703;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;We didn&amp;#39;t change the ratings here. We like the 5.2&amp;#39;s animation, but Claude&amp;#39;s version still felt more interesting.&lt;/p&gt;
&lt;div class=&quot;essay-breakout grid grid-cols-1 gap-4 md:grid-cols-2&quot;&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Claude 4.5 Opus&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/E2bse7dt01Pap3Yrwr9aKyr00Hxw7pV5rXsJSMIx006TqM?metadata-video-title=sound_claude&amp;video-title=sound_claude&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 769/631;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Gemini 3 Pro&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/knIuqiEW9yVL4FB6BOCHl02102026GX4ZakdwIYb01y7WNg?metadata-video-title=sound_gemini&amp;video-title=sound_gemini&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 943/663;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;4. Sharing positions&lt;/h2&gt;
&lt;p&gt;Things started to change when time came to add the backend! Goal 1 was to just make it so we shared positions for each player:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to use Instant presence.
Don&amp;#39;t save anything in the database, just use presence and topics. You can look up the docs.
There should should just be one single room.
You no longer the need to have the enemies that are randomly placed. All the players are what get placed.
For now, don&amp;#39;t worry about shots. Let&amp;#39;s just make it so the positions of the players are what get set in presence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Previously Codex 5.1 Max needed a few iterations to get things right. GPT 5.2 got this done out of the box. Here&amp;#39;s a snippet of how it felt:&lt;/p&gt;
&lt;iframe
  src=&quot;https://player.mux.com/027a01R1LsOJ00rv88aD1T8KpFEHSx1qHwZopOOo02MVI02A&quot;
  style=&quot;width: 100%; border: none; aspect-ratio: 1024/575;&quot;
  allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
  allowfullscreen
&gt;&lt;/iframe&gt;

&lt;p&gt;It was interesting to note that like Codex, GPT 5.2 was the rate model that relied &lt;em&gt;very&lt;/em&gt; heavily on REPLing to understand an API, rather than reading docs.&lt;/p&gt;
&lt;h2&gt;5. Sharing shots&lt;/h2&gt;
&lt;p&gt;Next up was making sure shots worked. GPT 5.2 got a lot better with making shots work.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s make shots work. When I shoot, send the shot as a topic, and make it affect the target&amp;#39;s HP. When the target HP goes to zero, they should die and respawn.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Just like Claude, GPT 5.2 got this done in one shot:&lt;/p&gt;
&lt;iframe
  src=&quot;https://player.mux.com/MMhn300c6g1OF5J02WPMPULnQaygq7o8O02jTnXObJ800Tk&quot;
  style=&quot;width: 100%; border: none; aspect-ratio: 1024/575;&quot;
  allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
  allowfullscreen
&gt;&lt;/iframe&gt;

&lt;p&gt;5.1 Codex Max needed more shots to get this right.&lt;/p&gt;
&lt;h2&gt;6. Maps&lt;/h2&gt;
&lt;p&gt;The final part of the game was to build maps. This included creating schema, seeding data, and making sure permissions worked.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So, now I want you to make it so the front page is actually a list of maps. Since our UI is using lots of polygons, make the style kind of polygonish
Make the UI look like the old counter strike map selection screen. I want you to save these maps in the database. Each map has a name. Use a script to generate 5 random maps with cool names.
Then, push up some permissions so that anyone can view maps, but they cannot create or edit them.
When you join a map, you can just use the map id as the room id for presence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We remember Claude having a lot of trouble with this. Codex 5.1 Max needed a few shots to get this right, and Gemini 3 Pro got this done in one shot.&lt;/p&gt;
&lt;p&gt;Well, GPT 5.2 now got this done in one shot too. We think Gemini&amp;#39;s UI is a bit better, but the backends were similar.&lt;/p&gt;
&lt;p&gt;One surprise here though, was that GPT 5.2 was a lot more sheepish about running CLI commands. It simply asked us to run the commands for it.&lt;/p&gt;
&lt;p&gt;We first thought this was a gotcha for this particular task, but after prodding it to push it&amp;#39;s changes to Vercel, it also made the mistake of just &amp;quot;telling&amp;quot; us vercel cli commands, rather than running it.&lt;/p&gt;
&lt;h2&gt;Finishing thoughts&lt;/h2&gt;
&lt;p&gt;GPT 5.2 did do better than Codex 5.1 Max. It chose some surprising steps (like using REPLs instead of reading docs, or sharing commands rather than running them), but overall it&amp;#39;s an improvement. We&amp;#39;re excited to see how the 5.2 codex model feels.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: You may ask: why medium? Lots of hackers prefer using &lt;code&gt;high&lt;/code&gt;. For now we choose whatever the CLI default is. We didn&amp;#39;t want to start customizing CLIs and introduce bias that way.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-2&quot; href=&quot;#user-content-fnref-2&quot;&gt;[2]&lt;/a&gt;  A bit of a revealed preference in this leaderboard: we vibe-coded the animations using Claude 4.5 Opus.&lt;/p&gt;
</description>
      <pubDate>Fri, 12 Dec 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Codex, Opus, Gemini try to build Counter Strike</title>
      <link>https://instantdb.com/essays/agents_building_counterstrike</link>
      <guid isPermaLink="true">https://instantdb.com/essays/agents_building_counterstrike</guid>
      <description>&lt;p&gt;In the last week we’ve had three major model updates: Gemini 3 Pro, Codex Max 5.1, Claude Opus 4.5. We thought we’d give them a challenge:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build a basic version of Counter Strike.&lt;/strong&gt; The game had to be a 3D UI and it had to be multiplayer.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re curious, pop open (an ideally large computer screen) and you can try out each model&amp;#39;s handiwork yourself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Codex Max 5.1&lt;/strong&gt;: &lt;a href=&quot;https://cscodex.vercel.app/&quot;&gt;https://cscodex.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Opus 4.5&lt;/strong&gt;: &lt;a href=&quot;https://csclaude.vercel.app/&quot;&gt;https://csclaude.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini 3 Pro&lt;/strong&gt;: &lt;a href=&quot;https://csgemini.vercel.app/&quot;&gt;https://csgemini.vercel.app/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We have a full video of us going through the build &lt;a href=&quot;https://youtu.be/fm-OoCWQlmc&quot;&gt;here&lt;/a&gt;, but for those who prefer text, you get this post.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ll go over some of our high-level impressions on each model, then dive deeper into the performance of specific prompts.&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;We signed up for the highest-tier plan on each model provider and used the defaults set for their CLI. For Codex, that’s 5.1 codex-max on the medium setting. For Claude it’s Opus 4.5. And with Gemini it&amp;#39;s 3 pro.&lt;/p&gt;
&lt;p&gt;We then gave each model about 7 consecutive prompts. Prompts were divided into two categories:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; At first agents only having to worry about the game mechanics. Design the scene, the enemies, the logic for shooting, and some sound effects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; Once that was done agents would then make the game multiplayer. They would need to build be selection of rooms. Users could join them and start shooting.&lt;/p&gt;
&lt;h2&gt;A High-Level Overview&lt;/h2&gt;
&lt;p&gt;So, how&amp;#39;d each model do?&lt;/p&gt;
&lt;p&gt;In a familiar tune with the other Anthropic models, &lt;strong&gt;Opus 4.5 won out on the frontend&lt;/strong&gt;. It made nicer maps, nicer characters, nicer guns, and generally had the right scene from the get-go.&lt;/p&gt;
&lt;p&gt;Once the design was done, &lt;strong&gt;Gemini 3 Pro started to win in the backend&lt;/strong&gt;. It got less errors adding multiplayer and persistence. In general Gemini did the best with making logical rather than visual changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codex Max felt like an “in-between” model on both frontend and backend.&lt;/strong&gt; It got a lot of “2nd place” points in our book. It did reasonably well on the frontend and reasonably well on the backend, but felt less spikey then the other models.&lt;/p&gt;
&lt;p&gt;Here’s the scorecard in detail:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boxes + Physics&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Characters + guns&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POV gun&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sounds&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moving&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shooting&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Saving rooms&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bonus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Okay, now let’s get deeper into each prompt.&lt;/p&gt;
&lt;h1&gt;1. Boxes and Physics&lt;/h1&gt;
&lt;p&gt;Goal number 1 was to set up the physics for the game. Models needed to design a map with a first-person viewpoint, and the ability to shoot enemies.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I want you to create a browser-based version of counter strike, using three js.&lt;/p&gt;
&lt;p&gt;For now, just make this local: don&amp;#39;t worry about backends, Instant, or
anything like that.&lt;/p&gt;
&lt;p&gt;For the first version, just make the main character a first-person view with
a cross hair. Put enemies at random places. Enemies have HP. You can
shoot them, and kill them. When an enemy is killed, they respawn.&lt;/p&gt;
&lt;p&gt;Make everything simple polygons -- rectangles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here’s a side-by-side comparison of the visuals each model came up with:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_codex.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_claude.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/map_gemini.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Visually Claude came up with the most interesting map. There were obstacles, a nice floor, and you could see everything well.&lt;/p&gt;
&lt;p&gt;Gemini got the something nice working too.&lt;/p&gt;
&lt;p&gt;Codex had an error on it’s first run &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; (it called a function without importing it), but it fixed it real quick. Once bugs were fixed, it’s map was the least visually pleasing. Things were darker, there were no obstacles, and it was hard to tell the floor.&lt;/p&gt;
&lt;h1&gt;2. Characters&lt;/h1&gt;
&lt;p&gt;Now that we had a map and some polygons, we asked the models to style up the characters. This was our prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want you to make the enemies look more like people. Use a bunch of square polygons to represent a person, and maybe a little gun&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here’s the result of their work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_codex.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_claude.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/enemy_gemini.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Again it feels like Claude did the best job here. The character look quite human — almost at the level of design in Minecraft. Gemini did well too. Codex made it’s characters better, but everything was a single color, which really diminished it compared to the others.&lt;/p&gt;
&lt;h1&gt;3. Gun in our field-of-view&lt;/h1&gt;
&lt;p&gt;We then asked each model to add a gun to our first-person view. When we shoot, we wanted a recoil animation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want you to make it so I also have a gun in my field of view. When I shoot, the gun moves a bit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here’s the side-by-side of how the recoil felt for each model:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_codex.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_claude.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/recoil_gemini.gif?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Here both Claude and Codex got the gun working in one shot. Claude’s gone looks like a real darn pistol though.&lt;/p&gt;
&lt;p&gt;Gemini had an issue trying to stick the gun to the camera. This got us in quite a back and forth, until we realized that the gun was transparent.&lt;/p&gt;
&lt;h1&gt;4. Adding sounds…and animations&lt;/h1&gt;
&lt;p&gt;We were almost done the frontend: the final step was sound. Here’s what we asked:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want you to use chiptunes to animate the sound of shots. I also want to animate deaths.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;All models added sounds pretty easily. The ending part in our prompt: “I also want to animate deaths.” was added at the spur of the moment in the video. Our intention was to add sound to deaths. &lt;em&gt;But&lt;/em&gt; that’s not what happened.&lt;/p&gt;
&lt;p&gt;All 3 models misunderstood the sentence in in the same way: they thought the wanted to animate how the characters died. Fair enough, re-reading the sentence again, we would understand it that way too.&lt;/p&gt;
&lt;p&gt;Here’s the results they came up with:&lt;/p&gt;
&lt;div class=&quot;essay-breakout grid grid-cols-1 gap-4 md:grid-cols-3&quot;&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Codex&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/UTCslk3hyNOnXSVlZmodcIqkFoHwgkPIl007aJxQINJ00?metadata-video-title=sound_codex&amp;video-title=sound_codex&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 885/626;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Claude&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/E2bse7dt01Pap3Yrwr9aKyr00Hxw7pV5rXsJSMIx006TqM?metadata-video-title=sound_claude&amp;video-title=sound_claude&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 769/631;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;div class=&quot;text-center font-mono text-sm font-bold&quot;&gt;Gemini&lt;/div&gt;
    &lt;iframe
      src=&quot;https://player.mux.com/knIuqiEW9yVL4FB6BOCHl02102026GX4ZakdwIYb01y7WNg?metadata-video-title=sound_gemini&amp;video-title=sound_gemini&quot;
      style=&quot;width: 100%; border: none; aspect-ratio: 943/663;&quot;
      allow=&quot;accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;&quot;
      allowfullscreen
    &gt;&lt;/iframe&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;All the models got the sound done easily. They all got animations, but we thought Claude’s animation felt the most fun.&lt;/p&gt;
&lt;h1&gt;5. Sharing positions&lt;/h1&gt;
&lt;p&gt;Now that all models had a real frontend, we asked them to make it multiplayer.&lt;/p&gt;
&lt;p&gt;We didn’t want the models to worry about shots just yet: goal 1 was to share the movement positions. Here’s what we asked it to do:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I want you to use Instant presence.&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t save anything in the database, just use presence and topics. You can
look up the docs.&lt;/p&gt;
&lt;p&gt;There should should just be one single room.&lt;/p&gt;
&lt;p&gt;You no longer the need to have the enemies that are randomly placed. All the players are what get placed.&lt;/p&gt;
&lt;p&gt;For now, don&amp;#39;t worry about shots. Let&amp;#39;s just make it so the positions of the players are what get set in presence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Gemini got this right in one shot. Both Codex and Claude needed some more prodding.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Moving&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;It was interesting to see how each model tried to solve problems:&lt;/p&gt;
&lt;p&gt;Codex used &lt;em&gt;lots&lt;/em&gt; of introspection. It would constantly look at the typescript library and look at the functions that were available. It didn’t seem to look at the docs as much.&lt;/p&gt;
&lt;p&gt;Claude looks at the docs a bunch. It read and re-read our docs on presence, but rarely introspected the library like Codex did.&lt;/p&gt;
&lt;p&gt;Gemini seemed to do both. It looked at the docs, but then I think because it constantly ran the build step, it found any typescript errors it had, and fixed it up.&lt;/p&gt;
&lt;p&gt;Gemini made the fastest progress here, though all of them got through, as long as we pasted the errors back.&lt;/p&gt;
&lt;h1&gt;6. Making shots work&lt;/h1&gt;
&lt;p&gt;Then we moved to getting shots to work. Here was the prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Now let&amp;#39;s make shots work. When I shoot, send the shot as a topic, and
make it affect the target&amp;#39;s HP. When the target HP goes to zero, they should die and respawn.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Shooting&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Claude got this right in one shot. Gemini and Codex had a few issues to fix, but just pasting the errors got them though.&lt;/p&gt;
&lt;h1&gt;7. Multiple maps&lt;/h1&gt;
&lt;p&gt;Now that all models had a single room working, it was time to get them supporting &lt;em&gt;multiple&lt;/em&gt; rooms.&lt;/p&gt;
&lt;p&gt;The reason we added this challenge, was to see (a) how they would deal with a new API (persistence), and (b) how they would deal with the refactor necessary for multiple rooms.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So, now I want you to make it so the front page is actually a list of
maps. Since our UI is using lots of polygons, make the style kind of
polygonish&lt;/p&gt;
&lt;p&gt;Make the UI look like the old counter strike map selection screen.
I want you to save these &lt;code&gt;maps&lt;/code&gt; in the database. Each map has a name.
Use a script to generate 5 random maps with cool names.&lt;/p&gt;
&lt;p&gt;Then, push up some permissions so that anyone can view maps, but they cannot
create or edit them.&lt;/p&gt;
&lt;p&gt;When you join a map, you can just use the map id as the room id for
presence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The maps UI&lt;/h2&gt;
&lt;p&gt;All models did great with the UI. Here’s how each looked:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codex&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/ui_codex.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/ui_claude.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/posts/counter_strike/ui_gemini.png?lightbox&quot; alt=&quot;&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;We kind of like Gemini’s UI the most, but they were all pretty cool.&lt;/p&gt;
&lt;h2&gt;The Persistence&lt;/h2&gt;
&lt;p&gt;And the persistence worked well too. They all dutifully created schema for maps, pushed a migration, and seeded 5 maps.&lt;/p&gt;
&lt;h2&gt;The Refactor&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;But&lt;/em&gt; things got complicated in the refactor.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;gpt 5.1 codex max (medium)&lt;/th&gt;
&lt;th&gt;Claude 4.5 Opus&lt;/th&gt;
&lt;th&gt;Gemini 3 Pro&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Saving rooms&lt;/td&gt;
&lt;td&gt;🥈&lt;/td&gt;
&lt;td&gt;🥉&lt;/td&gt;
&lt;td&gt;🥇&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Gemini got things done in one shot. It also chose to keep the map id in the URL, which made it much handier to use. Codex took one back and forth with a query error.&lt;/p&gt;
&lt;p&gt;But Claude &lt;em&gt;really&lt;/em&gt; got stuck. The culprit was hooks. Because useEffect can run multiple times, it ended up having a few very subtle bugs. For example, it made 2 canvas objects instead of 1. It also had multiple animation refs running at once.&lt;/p&gt;
&lt;p&gt;It was hard to get it to fix things by itself. We had to put our engineer hats on and actually look at the code to unblock Claude here.&lt;/p&gt;
&lt;p&gt;This did give us a few ideas though:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Claude’s issues were human-like. How many of us get tripped up with useEffect running twice, or getting dependency arrays wrong? I think improving the React DX on these two issues could really push humans and agents further.&lt;/li&gt;
&lt;li&gt;And would have happened if a non-programmer was building this? They would have gotten really stuck. We think there needs to be more tools to go from “strictly vibe coding”, to “real programming”. Right now the jump feels too steep.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At the end, all models built real a multiplayer FPS, with zero code written by hand! That’s pretty darn cool.&lt;/p&gt;
&lt;h2&gt;Parting thoughts&lt;/h2&gt;
&lt;p&gt;Well, models have definitely improved. They can take much higher-level feedback, and much higher-level documentation. What really strikes us though is how much they can iterate on their own work thanks to the CLI.&lt;/p&gt;
&lt;p&gt;There’s still lots to go though. The promise that you never have to look at the code doesn’t quite feel real yet.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: Interestingly, Gemini was very eager to run &lt;code&gt;npm run build&lt;/code&gt; over and over again, before terminating. Codex did not do this, and Claude did this more sparingly. This may explain why Gemini got fewer errors.&lt;/p&gt;
</description>
      <pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Count-Min Sketches in JS — frequencies, but without the data</title>
      <link>https://instantdb.com/essays/count_min_sketch</link>
      <guid isPermaLink="true">https://instantdb.com/essays/count_min_sketch</guid>
      <description>&lt;div class=&quot;text-lg italic font-medium&quot;&gt;
  Our teammate Daniel introduced Count-Min Sketches in &lt;a href=&quot;/&quot;&gt;Instant&lt;/a&gt; (a sync engine you can spin up in less than a minute). Sketches were so small and so fast that I got into a rabbit hole learning about them. The following post came out of the process.
&lt;/div&gt;

&lt;p&gt;I have read and re-read just about every one of PG Wodehouse’s 71 novels. He’s one of my favorite authors. Wodehouse can take seemingly silly plots (quite a few involving stealing pigs) and twist them until you’re rapt with attention. And he’s a master of the English language.&lt;/p&gt;
&lt;p&gt;Wodehouse is known for eccentric diction. Instead of &amp;quot;Freddie walked over&amp;quot;, he’ll say &amp;quot;Freddie (shimmied | beetled | ambled) over&amp;quot;. You may wonder, how many times did he use the word &amp;#39;beetle&amp;#39;?&lt;/p&gt;
&lt;p&gt;Well I could tell you &lt;em&gt;approximately&lt;/em&gt; how many times Wodehouse used any word in his entire lexicon, just by loading the data structure embedded in this image:&lt;/p&gt;
&lt;div class=&quot;flex justify-center&quot;&gt;
  &lt;img class=&quot;m-0&quot; src=&quot;/posts/count_min_sketch/compressedSketch.png&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Compressed, it&amp;#39;s 50 kilobytes and covers a 23 megabyte text file, or 3.7 million words. We can use it to answer count estimates with 0.05% error rate and 99% confidence. (If you aren&amp;#39;t familiar with the probability terms here, no worries, we&amp;#39;ll go over them in this post.)&lt;/p&gt;
&lt;p&gt;You can try it yourself right here:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;intro-try-sketch&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;h1&gt;The Count-Min Sketch&lt;/h1&gt;
&lt;p&gt;The magic needed to make this happen is called the &lt;strong&gt;Count-Min Sketch&lt;/strong&gt; — a data structure that can give you frequency estimates over giant amounts of data &lt;em&gt;without&lt;/em&gt; becoming a giant object itself.&lt;/p&gt;
&lt;p&gt;You could use it to make passwords safer: track all known passwords on the internet, and detect whenever someone chooses a common password. &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Or you could use it estimate the popularity of links: update a sketch whenever a user looks at a tweet, and you can query for approximate views. &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Or, use it to make databases faster: track the values of different columns, so you can estimate how many rows a filter would return. This is how we use them in Instant: our query planner decides which indexes to use based on estimates from sketches. &lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So how do Count-Min Sketches work? In this post we&amp;#39;ll find out by building one from scratch, in JavaScript!&lt;/p&gt;
&lt;h1&gt;Setup&lt;/h1&gt;
&lt;p&gt;Let&amp;#39;s dust off Bun &lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt; and spin up a project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir sketches
cd sketches
bun init
cat &amp;gt; wodehouse.txt &amp;lt;&amp;lt; &amp;#39;EOF&amp;#39;
At the open window of the great library of Blandings Castle,
drooping like a wet sock, as was his habit when he had nothing
to prop his spine against, the Earl of Emsworth, that amiable
and boneheaded peer, stood gazing out over his domain.
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&amp;#39;ve just made an &lt;code&gt;index.ts&lt;/code&gt; file, and a little toy &lt;code&gt;wodehouse.txt&lt;/code&gt; that we can play with as we go along.&lt;/p&gt;
&lt;p&gt;Time to &lt;code&gt;bun run --watch&lt;/code&gt;, and we&amp;#39;re ready to hack!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun run --watch index.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;An exact solution&lt;/h1&gt;
&lt;p&gt;First things first: let&amp;#39;s write a straightforward algorithm. If we wanted to count words &lt;em&gt;exactly&lt;/em&gt;, how would we do it?&lt;/p&gt;
&lt;p&gt;Well we could read &lt;code&gt;wodehouse.txt&lt;/code&gt;, parse each word and count them. Here we go:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts
import fs from &amp;#39;fs&amp;#39;;

// 1. Read the file
const wodehouse = fs.readFileSync(&amp;#39;wodehouse.txt&amp;#39;, &amp;#39;utf-8&amp;#39;);

// 2. Split it into words
function toWords(text: string): string[] {
  return text
    .split(&amp;#39;\n&amp;#39;)
    .flatMap((line) =&amp;gt; line.split(&amp;#39; &amp;#39;))
    .map((w) =&amp;gt; w.trim().toLowerCase())
    .filter((w) =&amp;gt; w);
}

// 3. Get exact counts
function countWords(words: string[]): { [w: string]: number } {
  const result: { [w: string]: number } = {};
  for (const word of words) {
    result[word] = (result[word] || 0) + 1;
  }
  return result;
}

const exactCounts = countWords(toWords(wodehouse));

console.log(&amp;#39;exactCounts&amp;#39;, exactCounts);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This logs a little map in our terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;exactCounts {
  at: 1,
  the: 3,
  // ...
  &amp;quot;castle,&amp;quot;: 1,
  drooping: 1,
  // ...
  &amp;quot;domain.&amp;quot;: 1,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works, but we&amp;#39;ll have a few problems.&lt;/p&gt;
&lt;h2&gt;Stems&lt;/h2&gt;
&lt;p&gt;What if the word &amp;quot;castle&amp;quot; was used without a comma? Or if instead of &amp;quot;drooping&amp;quot; Wodehouse wrote &amp;quot;drooped&amp;quot;?&lt;/p&gt;
&lt;p&gt;We would get different counts. It would be nice if we could normalize each word so no matter how Wodehouse wrote &amp;quot;droop&amp;quot;, we&amp;#39;d get the same count.&lt;/p&gt;
&lt;p&gt;This is a common natural-language processing task called &amp;quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Stemming&quot;&gt;stemming&lt;/a&gt;&amp;quot;. There are some great algorithms and libraries for this, but for our post we can write a rough function ourselves:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts
// ...
// 2. Split it into words
function stem(word: string) {
  let w = word.toLowerCase().replaceAll(/&lt;sup id=&quot;user-content-fnref-a-z&quot;&gt;&lt;a href=&quot;#user-content-fn-a-z&quot;&gt;[a-z]&lt;/a&gt;&lt;/sup&gt;/g, &amp;#39;&amp;#39;);
  if (w.endsWith(&amp;#39;ing&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 4) {
    w = w.slice(0, -3);
  } else if (w.endsWith(&amp;#39;ed&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 3) {
    w = w.slice(0, -2);
  } else if (w.endsWith(&amp;#39;s&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 3 &amp;amp;&amp;amp; !w.endsWith(&amp;#39;ss&amp;#39;)) {
    w = w.slice(0, -1);
  } else if (w.endsWith(&amp;#39;ly&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 3) {
    w = w.slice(0, -2);
  } else if (w.endsWith(&amp;#39;er&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 4) {
    w = w.slice(0, -2);
  } else if (w.endsWith(&amp;#39;est&amp;#39;) &amp;amp;&amp;amp; w.length &amp;gt; 4) {
    w = w.slice(0, -3);
  }
  return w;
}

function toWords(text: string): string[] {
  return text
    .split(&amp;#39;\n&amp;#39;)
    .flatMap((line) =&amp;gt; line.split(&amp;#39; &amp;#39;))
    .map(stem)
    .filter((w) =&amp;gt; w);
}
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With it our console.log starts to show stemmed words:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;exactCounts {
  at: 1,
  the: 3,
  // ...
  castle: 1, // No more `,`
  droop: 1, // No more `ing`!
  // ...
  &amp;quot;domain&amp;quot;: 1, // No more `.`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now we have better exact counts. But there&amp;#39;s another problem.&lt;/p&gt;
&lt;h2&gt;Growth&lt;/h2&gt;
&lt;p&gt;What happens when you look at more words? Our &lt;code&gt;exactCounts&lt;/code&gt; grows with the vocabulary of &lt;code&gt;words&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;exact-counts-growth&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t &lt;em&gt;too big&lt;/em&gt; of an issue with Wodehouse specifically: after all the English dictionary itself could fit in memory.&lt;/p&gt;
&lt;p&gt;But as our vocabulary gets larger, our data structure gets more annoying. Imagine if we had to track &lt;em&gt;combinations&lt;/em&gt; of words: suddenly keeping counts would take more space than the words themselves. Could we do something different?&lt;/p&gt;
&lt;h1&gt;An intuition for sketches&lt;/h1&gt;
&lt;p&gt;Ideally, we would be able to divorce the size of our vocabulary from the size of our counts data structure. Here&amp;#39;s one way to do that.&lt;/p&gt;
&lt;h2&gt;Columns of Buckets&lt;/h2&gt;
&lt;p&gt;Our &lt;code&gt;exactCounts&lt;/code&gt; was an unbounded hash map. Let&amp;#39;s make a bounded version.&lt;/p&gt;
&lt;p&gt;We can spin up a &lt;em&gt;fixed&lt;/em&gt; number of buckets. Each bucket stores a count. We then take a word, hash it, and increment its corresponding bucket. Here&amp;#39;s how this could work:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;single-row-insert&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;When we want to know the count of word, we hash it, find the corresponding bucket, and that&amp;#39;s our count:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;single-row-query&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;With this we&amp;#39;ve solved our growth problem! No matter how large our vocabulary gets, our buckets stay a fixed size.&lt;/p&gt;
&lt;p&gt;But of course this comes with new consequences.&lt;/p&gt;
&lt;h2&gt;The &amp;#39;sketch&amp;#39; in sketches.&lt;/h2&gt;
&lt;p&gt;Our counts become estimates. If you look at the demo, both &amp;#39;wet&amp;#39; and &amp;#39;castle&amp;#39; ended up in the second bucket. If we asked &amp;quot;How many times is &amp;#39;castle&amp;#39; used?&amp;quot;, we&amp;#39;d get 622.&lt;/p&gt;
&lt;p&gt;Now, it does suck that we got 622 instead of 454 for &amp;#39;castle&amp;#39;. But if you think about it, it&amp;#39;s not such a big deal. Both words are used infrequently. Even when you put them together they pale in comparison to more common words. And if you&amp;#39;re worried about errors we can already intuit a way to reduce them.&lt;/p&gt;
&lt;h2&gt;More buckets, fewer errors&lt;/h2&gt;
&lt;p&gt;To reduce errors we can add more buckets. The more buckets we have, the fewer collisions we&amp;#39;ll have, and the lower our chances of errors are. (You may wonder how &lt;em&gt;much&lt;/em&gt; lower do our errors get? We&amp;#39;ll get to that soon!)&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;more-buckets&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;We may be feeling pretty good here, but we&amp;#39;re not done yet. We&amp;#39;re going to have a serious problem with high-frequency words.&lt;/p&gt;
&lt;h2&gt;Managing frequencies&lt;/h2&gt;
&lt;p&gt;What happens if we add a word like &amp;#39;like&amp;#39;? Say it landed where &amp;#39;peer&amp;#39; was:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;high-frequency&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If we asked for the count of &amp;#39;peer&amp;#39;, we&amp;#39;d now get back 9,262.&lt;/strong&gt; That estimation is wildly inflated by &amp;#39;like&amp;#39;. Not very useful.&lt;/p&gt;
&lt;p&gt;If we want to make our estimations better, we would need a way to reduce the chance of very-high frequency words influencing counts. How can we do this?&lt;/p&gt;
&lt;h2&gt;Rows of Hashes&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s one way to reduce the influence of high-frequency words: we&amp;#39;ll add more hashes!&lt;/p&gt;
&lt;p&gt;We can set up a row of hash functions, each with their own buckets. To add a word, we go through each row, hash it and increment the corresponding bucket. Here&amp;#39;s how this looks:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;two-rows-insert&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;When we want to know the count, we go through each row, find the corresponding bucket and pick the minimum value we find. &lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;two-rows-query&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;This is pretty cool: a particular word could get unlucky in one hash function, but as long as it gets a lucky bucket from &lt;em&gt;some&lt;/em&gt; row, we&amp;#39;ll get a respectable count.&lt;/p&gt;
&lt;p&gt;We can look at &amp;#39;peer&amp;#39; again for an example. hash1 got us into the same bucket as &amp;#39;like&amp;#39;. But hash2 got us into our own bucket. That means a better estimation! And it also means we can intuit a way to improve our confidence even more.&lt;/p&gt;
&lt;h2&gt;More hash functions...more confidence&lt;/h2&gt;
&lt;p&gt;To improve confidence we can add more hash functions. The more hash functions we have, the higher the chance that we find at least &lt;em&gt;one&lt;/em&gt; good bucket. (You may wonder, how much more confident do we get? We&amp;#39;ll get to that soon!)&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;more-rows-confidence&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;Of course, this depends on how correlated the hash functions are. We&amp;#39;ll want to be sure that they are independent of each other, so adding a new hash function fully shuffles around the words.&lt;/p&gt;
&lt;p&gt;If we do this right, and we build out columns of buckets and rows of hashes, we&amp;#39;ll have our Count-Min Sketch!&lt;/p&gt;
&lt;h1&gt;Implementing the Sketch&lt;/h1&gt;
&lt;p&gt;Let&amp;#39;s go ahead and write out our ideas in code then.&lt;/p&gt;
&lt;h2&gt;Creating a sketch&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll kick off by typing our &lt;code&gt;Sketch&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts

// 4. Create a sketch
type Sketch = {
  rows: number;
  columns: number;
  buckets: Uint32Array;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We keep track of a &lt;code&gt;rows&lt;/code&gt;, &lt;code&gt;columns&lt;/code&gt;, and all of our &lt;code&gt;buckets&lt;/code&gt;. Technically &lt;code&gt;buckets&lt;/code&gt; are arranged as a matrix so we &lt;em&gt;could&lt;/em&gt; use an array of arrays to store them. But a single array of buckets is more efficient. &lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;To make life easier let&amp;#39;s create a little builder function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts

// 4. Create a sketch
// ...
function createSketch({
  rows,
  columns,
}: {
  rows: number;
  columns: number;
}): Sketch {
  return { rows, columns, buckets: new Uint32Array(rows * columns) };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we use it, we&amp;#39;ve got ourselves a sketch!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const sketch = createSketch({ rows: 2, columns: 5 });

console.log(&amp;#39;created: &amp;#39;, sketch);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our console.log shows us a nifty object!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;created: {
  rows: 2,
  columns: 5,
  buckets: Uint32Array(10) [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding words&lt;/h2&gt;
&lt;p&gt;Alright, now for the meat and potatoes. Let&amp;#39;s implement &lt;code&gt;add&lt;/code&gt;. We want to say:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take a word&lt;/li&gt;
&lt;li&gt;For each row, hash it and find its corresponding bucket&lt;/li&gt;
&lt;li&gt;Increment the corresponding bucket&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here we go:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function add({ rows, columns, buckets }: Sketch, word: string) {
  for (let rowIdx = 0; rowIdx &amp;lt; rows; rowIdx++) {
    const hash = Bun.hash.xxHash3(word, BigInt(rowIdx));
    const columnIdx = Number(hash % BigInt(columns));
    const globalIdx = rowIdx * columns + columnIdx;
    buckets[globalIdx]!++;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We go through each row. &lt;code&gt;xxHash3&lt;/code&gt; takes a seed argument. We can pass the &lt;code&gt;rowIdx&lt;/code&gt; into our &amp;#39;seed&amp;#39;, so for every row we produce an independent hash value!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const hash = Bun.hash.xxHash3(word, BigInt(rowIdx));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;columnIdx&lt;/code&gt; tells us which bucket to use inside a particular row:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const columnIdx = Number(hash % BigInt(columns));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And &lt;code&gt;globalIdx&lt;/code&gt; accounts for the particular row that we we&amp;#39;re looking at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const globalIdx = rowIdx * columns + columnIdx;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Increment that bucket, and we&amp;#39;re done!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;buckets[globalIdx]!++;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can try it out and see how it feels.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;add(sketch, stem(&amp;#39;castle&amp;#39;));
console.log(&amp;#39;after castle&amp;#39;, sketch);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;after castle {
  rows: 2,
  columns: 5,
  buckets: Uint32Array(10) [ 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suuper cool! Notice the two increments in &lt;code&gt;buckets&lt;/code&gt;, accounting for our different rows.&lt;/p&gt;
&lt;h2&gt;Getting counts&lt;/h2&gt;
&lt;p&gt;All that&amp;#39;s left is to get a count. This is going to look similar to &amp;#39;add&amp;#39;. We want to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take a word&lt;/li&gt;
&lt;li&gt;For each row, hash it and nab the corresponding bucket&lt;/li&gt;
&lt;li&gt;Find the minimum value from all the corresponding buckets&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s do it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function check({ rows, columns, buckets }: Sketch, word: string) {
  let approx = Infinity;
  for (let rowIdx = 0; rowIdx &amp;lt; rows; rowIdx++) {
    const hash = Bun.hash.xxHash3(word, BigInt(rowIdx));
    const columnIdx = Number(hash % BigInt(columns));
    const globalIdx = rowIdx * columns + columnIdx;
    approx = Math.min(approx, buckets[globalIdx]!);
  }
  return approx;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do the same math to get our &lt;code&gt;globalIdx&lt;/code&gt; for each row as we did in &lt;code&gt;add&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We track the minimum number we see, and we have our &lt;code&gt;check&lt;/code&gt;! Let&amp;#39;s try it out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;console.log(&amp;#39;check castle&amp;#39;, check(sketch, &amp;#39;castle&amp;#39;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aaand we get our result!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;check castle 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Congratulations, you&amp;#39;ve implemented a Count-Min Sketch!&lt;/p&gt;
&lt;h1&gt;Getting real&lt;/h1&gt;
&lt;p&gt;Alright, now that we have a real Count-Min Sketch, let&amp;#39;s put it to the test. We&amp;#39;ll find out approximately how many times &amp;#39;beetle&amp;#39; is used in Wodehouse&amp;#39;s texts.&lt;/p&gt;
&lt;h2&gt;Get all of Wodehouse&lt;/h2&gt;
&lt;p&gt;I went ahead and compiled all 61 novels from Project Gutenberg into one giant text file. You can go ahead and download it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl https://www.instantdb.com/posts/count_min_sketch/wodehouse-full.txt \
  -o wodehouse-full.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We have a &lt;code&gt;wodehouse-full.txt&lt;/code&gt; file we can play with now. Let&amp;#39;s load it up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts
// ...
const allWodehouse = fs.readFileSync(&amp;#39;wodehouse-full.txt&amp;#39;, &amp;#39;utf-8&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Getting exact counts&lt;/h2&gt;
&lt;p&gt;We can use up our &lt;code&gt;toWords&lt;/code&gt; and &lt;code&gt;exactCounts&lt;/code&gt; to get a feel for the vocabulary:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts
const allWodehouse = fs.readFileSync(&amp;#39;wodehouse-full.txt&amp;#39;, &amp;#39;utf-8&amp;#39;);
const allWords = toWords(allWodehouse);
const allExactCounts = countWords(allWords);

console.log(&amp;#39;exact beetle&amp;#39;, allExactCounts[stem(&amp;#39;beetle&amp;#39;)]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we look at &amp;quot;beetle&amp;quot;, we can see it&amp;#39;s used exactly 59 times. What would a sketch return?&lt;/p&gt;
&lt;h2&gt;Trying out sketches&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s create a sketch for our wodehouse words:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// index.ts
// ...
const allSketch = createSketch({ rows: 5, columns: 5437 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add our words:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;for (const word of allWords) {
  add(allSketch, word);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if we check out &amp;#39;beetle&amp;#39;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;console.log(&amp;#39;allSketch beetle&amp;#39;, check(allSketch, stem(&amp;#39;beetle&amp;#39;)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&amp;#39;ll see 78!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;allSketch beetle 78
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bit over, but not so bad. &lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re curious, try out different sizes and see what you get:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;configurable-try-sketch&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;h1&gt;A breather to celebrate&lt;/h1&gt;
&lt;p&gt;Congratulations! You just built a Count-Min Sketch from scratch, and used it on Wodehouse. If you&amp;#39;d like to see the full code example, I put this up in its entirety on &lt;a href=&quot;https://github.com/instantdb/count-min-sketch&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Hope you had a lot of fun :).&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re still curious there&amp;#39;s more to learn here, I present to you...2 bonus sections!&lt;/p&gt;
&lt;h1&gt;Bonus 1: Probabilities&lt;/h1&gt;
&lt;p&gt;When we created our sketch for Wodehouse, we chose some seemingly random numbers: 5437 columns and 5 rows. Is there a method to this madness?&lt;/p&gt;
&lt;p&gt;Absolutely. We can use some math to help set bounds around our estimations.&lt;/p&gt;
&lt;h2&gt;Error Rate &amp;amp; Confidence&lt;/h2&gt;
&lt;p&gt;There are two numbers we can play with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;errorRate&lt;/strong&gt; tells us how far off we expect our estimation to be&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;confidence&lt;/strong&gt; tells us how likely it is that we are actually within our estimation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;#39;s make them concrete. The full text for Wodehouse is about 3.7 million words long (not unique words, here we are counting every occurrence).&lt;/p&gt;
&lt;p&gt;Say we want an error rate of 0.05% and a 99% confidence.&lt;/p&gt;
&lt;p&gt;0.05% of 3.7 million is 1850. We are in effect saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;You can expect the estimation we give you to be overcounted by at most 1850, and we&amp;#39;ll be right 99% of the time&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&amp;#39;s pretty cool! How can we be certain like this?&lt;/p&gt;
&lt;h2&gt;Formulas&lt;/h2&gt;
&lt;p&gt;Turns out, you can tie the &lt;code&gt;errorRate&lt;/code&gt; and the &lt;code&gt;confidence&lt;/code&gt; to the number of &lt;code&gt;rows&lt;/code&gt; and &lt;code&gt;columns&lt;/code&gt; in a sketch! Here are the formulas:&lt;/p&gt;
&lt;p&gt;Given an &lt;code&gt;errorRate&lt;/code&gt;, get this many &lt;code&gt;columns&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;$$
columns = \frac{e}{errorRate}
$$&lt;/p&gt;
&lt;p&gt;Given a &lt;code&gt;confidence&lt;/code&gt;, get this many &lt;code&gt;rows&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;$$
rows = \ln(\frac{1}{1 - confidence})
$$&lt;/p&gt;
&lt;p&gt;Now how did we get these formulas? Let&amp;#39;s derive them.&lt;/p&gt;
&lt;h2&gt;Variables&lt;/h2&gt;
&lt;p&gt;We can start by writing out some of the numbers that we just went through.&lt;/p&gt;
&lt;p&gt;We have:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;totalWords&lt;/code&gt;. This tells us how many occurrences have been counted in our Sketch. For Wodehouse, that&amp;#39;s 3.7M&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;errorRate&lt;/code&gt;. How far off we expect our estimation to be as a percentage of totalWords. For us it&amp;#39;s 0.05%&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;maximumOvercount&lt;/code&gt;. Our maximum allowed overestimation for a particular &lt;code&gt;totalWords&lt;/code&gt;. In our case, it&amp;#39;s 1850.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;confidence&lt;/code&gt;. This tells us how likely we are to be within within our estimation. We want 99%.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And our sketch has two properties that we can influence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;columns&lt;/code&gt;. This is the number of buckets in one row. We &lt;em&gt;somehow&lt;/em&gt; picked 5,437 for our Wodehouse sketch.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;rows&lt;/code&gt;. This is the number of hash functions in our sketch. We &lt;em&gt;somehow&lt;/em&gt; picked 5 rows for our Wodehouse sketch.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Our goal is to relate &lt;code&gt;errorRate&lt;/code&gt; and &lt;code&gt;confidence&lt;/code&gt; to a specific number of &lt;code&gt;columns&lt;/code&gt; and &lt;code&gt;rows&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Tying errorRate to columns&lt;/h2&gt;
&lt;p&gt;To build our intuition let&amp;#39;s consider a sketch with only 1 row:&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;single-row-buckets&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;Say we ask for a count of a word (&amp;#39;wet&amp;#39;). Our hash function will direct us to a bucket. What would we see if we looked into that bucket?&lt;/p&gt;
&lt;p&gt;&lt;sketch-demo demo=&quot;bucket-noise-breakdown&quot;&gt;&lt;/sketch-demo&gt;&lt;/p&gt;
&lt;p&gt;Well it would be composed of the &amp;quot;actual number of times&amp;quot; &amp;#39;wet&amp;#39; was used, and the noise that comes from all the other collisions that hit our bucket.&lt;/p&gt;
&lt;p&gt;If we write this out:&lt;/p&gt;
&lt;p&gt;$$
bucket_{word} = actualCount_{word} + noise_{word}
$$&lt;/p&gt;
&lt;h3&gt;Expected Noise&lt;/h3&gt;
&lt;p&gt;Now here&amp;#39;s a question: what is the expected value &lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt; of our noise for a word?&lt;/p&gt;
&lt;p&gt;The first thing we can remember is that our hash function distributes words uniformly across columns. This means that each word has a $\frac{1}{columns}$ chance of hitting our particular bucket.&lt;/p&gt;
&lt;p&gt;So if we write our our expectation, it would be:&lt;/p&gt;
&lt;p&gt;$$
expectedNoise_{word} = \frac{totalWords - actualCount_{word}}{columns}
$$&lt;/p&gt;
&lt;h3&gt;Simplifying Noise&lt;/h3&gt;
&lt;p&gt;If you think about, do we really &lt;em&gt;need&lt;/em&gt; to subtract the $actualCount_{word}$? We can simplify this formula by getting more conservative about what we promise.&lt;/p&gt;
&lt;p&gt;We can bound ourselves to the worst case scenario, where we ask for a word that hasn&amp;#39;t been seen before:&lt;/p&gt;
&lt;p&gt;$$
expectedNoise_{word} \le \frac{totalWords}{columns}
$$&lt;/p&gt;
&lt;p&gt;Pretty cool. Now we have a simple relation for our expected noise!&lt;/p&gt;
&lt;h3&gt;Help from Markov&lt;/h3&gt;
&lt;p&gt;But an expected value for noise isn&amp;#39;t useful yet. It just gives us an average. What we want is the &lt;em&gt;probability&lt;/em&gt; that something is below a &lt;code&gt;maximumOvercount&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s where &lt;strong&gt;Markov&amp;#39;s Inequality&lt;/strong&gt; &lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt; comes in. Markov&amp;#39;s Inequality is a proof about random variables that says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For any non-negative random variable, the probability that something is at least $n$ times its expected value is at most $\frac{1}{n}$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To get concrete, if we plug in $n = e$ &lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt; to Markov&amp;#39;s Inequality, we get:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The probability that something is at least $e$ times its expected value is at most $\frac{1}{e}$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, our noise is a non-negative random variable &lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;. And we have its expected value. If we use Markov&amp;#39;s Inequality we&amp;#39;ll get a real probability that we can use!&lt;/p&gt;
&lt;p&gt;$$
P(\text{Noise} \ge e \times expectedNoise_{word}) \le \frac{1}{e}
$$&lt;/p&gt;
&lt;h3&gt;A maximumOvercount with about 63% confidence&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s look that probability a bit more:&lt;/p&gt;
&lt;p&gt;$$
P(\text{Noise} \ge e \times expectedNoise_{word}) \le \frac{1}{e}
$$&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s get it&amp;#39;s complement:&lt;/p&gt;
&lt;p&gt;$$
P(\text{Noise} \le e \times expectedNoise_{word}) \ge 1 - \frac{1}{e}
$$&lt;/p&gt;
&lt;p&gt;And to make things more concrete, $1 - \frac{1}{e}$ is about 0.63.&lt;/p&gt;
&lt;p&gt;What is it saying then? Let&amp;#39;s write it out in English:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;The probability that noise is at most e times expectedNoise is at least ~63%&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you squint, we are talking about &lt;code&gt;maximumOvercount&lt;/code&gt; with about 63% confidence!&lt;/p&gt;
&lt;p&gt;If we set &lt;code&gt;maximumOvercount&lt;/code&gt; to to $e \times expectedNoise$, we can say with $1 - \frac{1}{e}$ confidence that our estimation will be within our bounds!&lt;/p&gt;
&lt;h3&gt;An errorRate with about 37% confidence&lt;/h3&gt;
&lt;p&gt;Now that we have a probability that uses &lt;code&gt;maximumOvercount&lt;/code&gt;, let&amp;#39;s start tying things back to &lt;code&gt;errorRate&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We said before:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can expect the estimation we give you to be overcounted by at most 1850&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Translated to a formula, this was:&lt;/p&gt;
&lt;p&gt;$$
3.7 \text{ million} \times 0.05% \le 1850
$$&lt;/p&gt;
&lt;p&gt;If we use variables:&lt;/p&gt;
&lt;p&gt;$$
totalWords \times errorRate \le maximumOvercount;
$$&lt;/p&gt;
&lt;p&gt;Now let&amp;#39;s start expanding &lt;code&gt;maximumOverCount&lt;/code&gt;, and see where we get:&lt;/p&gt;
&lt;p&gt;$$
totalWords \times errorRate \le e \times expectedNoise;
$$&lt;/p&gt;
&lt;p&gt;And since we know &lt;code&gt;expectedNoise&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;$$
totalWords \times errorRate \le \frac{e \times totalWords}{columns}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We&amp;#39;ve just tied errorRate and columns together!&lt;/strong&gt; Let&amp;#39;s keep going:&lt;/p&gt;
&lt;p&gt;$$
errorRate \le \frac{e}{columns}
{} \
{} \
columns \ge \frac{e}{errorRate}
$$&lt;/p&gt;
&lt;p&gt;Voila! We&amp;#39;ve gotten a formula for columns.&lt;/p&gt;
&lt;h3&gt;A solution for 1 row&lt;/h3&gt;
&lt;p&gt;If our goal was to get a particular error rate with about 63% confidence, we could just set:&lt;/p&gt;
&lt;p&gt;$$
columns = \frac{e}{errorRate}
{} \
{} \
rows = 1
$$&lt;/p&gt;
&lt;p&gt;But 63% confidence kind of sucks. How can we improve that?&lt;/p&gt;
&lt;h2&gt;Tying confidence to rows&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s remember our initial Markov Inequality:&lt;/p&gt;
&lt;p&gt;$$
P(\text{Noise} \ge e \times expectedNoise_{word}) \le \frac{1}{e}
$$&lt;/p&gt;
&lt;h3&gt;All bad rows&lt;/h3&gt;
&lt;p&gt;When &lt;code&gt;Noise &amp;gt; maximumOvercount&lt;/code&gt;, it basically means that our estimation has failed.&lt;/p&gt;
&lt;p&gt;We&amp;#39;ve gotten a &amp;quot;bad row&amp;quot;, where the bucket has highly frequent words in it. In this case we can paraphrase our probability to:&lt;/p&gt;
&lt;p&gt;$$
P(\text{1 row is bad}) \le \frac{1}{e}
$$&lt;/p&gt;
&lt;p&gt;Now what happens if we add more rows? What is the chance that &lt;em&gt;2&lt;/em&gt; rows are bad?&lt;/p&gt;
&lt;p&gt;Since our hash functions are independent, we know that our probabilities will be too. This means:&lt;/p&gt;
&lt;p&gt;$$
P(\text{2 rows are bad}) \le \left(\frac{1}{e}\right)^{2}
$$&lt;/p&gt;
&lt;p&gt;Which generalizes. Given some number of rows, what is the probability that &lt;em&gt;all&lt;/em&gt; rows are bad?&lt;/p&gt;
&lt;p&gt;$$
P(\text{all rows are bad}) \le \left(\frac{1}{e}\right)^{rows}
$$&lt;/p&gt;
&lt;p&gt;And now that we know the formula for &amp;quot;all rows are bad&amp;quot;, we actually &lt;em&gt;also&lt;/em&gt; know the formula for confidence.&lt;/p&gt;
&lt;h3&gt;Confidence&lt;/h3&gt;
&lt;p&gt;As long as we get 1 good row, we know that we&amp;#39;ll return a number within our estimation. In that case we can say our confidence is:&lt;/p&gt;
&lt;p&gt;$$
confidence = P(\text{at least 1 good row})
$$&lt;/p&gt;
&lt;p&gt;So what&amp;#39;s the probability of &lt;em&gt;at least&lt;/em&gt; 1 good row? It&amp;#39;s the complement of getting all bad rows:&lt;/p&gt;
&lt;p&gt;$$
P(\text{at least 1 good row}) = 1 - P(\text{all rows are bad})
$$&lt;/p&gt;
&lt;p&gt;Which gets us:&lt;/p&gt;
&lt;p&gt;$$
confidence = 1 - P(\text{all rows are bad})
$$&lt;/p&gt;
&lt;h3&gt;Expanding things out&lt;/h3&gt;
&lt;p&gt;Since we know $P(\text{all rows are bad})$, let&amp;#39;s expand it:&lt;/p&gt;
&lt;p&gt;$$
confidence = 1 - \left(\frac{1}{e}\right)^{rows}
$$&lt;/p&gt;
&lt;p&gt;Aand we&amp;#39;ve just connected &lt;code&gt;confidence&lt;/code&gt; to &lt;code&gt;rows&lt;/code&gt;! Let&amp;#39;s keep going.&lt;/p&gt;
&lt;p&gt;Isolate the term for rows:&lt;/p&gt;
&lt;p&gt;$$
\left(\frac{1}{e}\right)^{rows} = 1 - confidence
$$&lt;/p&gt;
&lt;p&gt;Remember $\left(\frac{1}{e}\right)^{rows}$ is the same as $e^{-rows}$&lt;/p&gt;
&lt;p&gt;$$
e^{-rows} = 1 - confidence
$$&lt;/p&gt;
&lt;p&gt;Take the natural log of both sides:&lt;/p&gt;
&lt;p&gt;$$
\ln(e^{-rows}) = \ln(1 - confidence)
$$&lt;/p&gt;
&lt;p&gt;Simplify the left side:&lt;/p&gt;
&lt;p&gt;$$
-rows = \ln(1 - confidence)
\ {}
rows = -\ln(1 - confidence)
$$&lt;/p&gt;
&lt;p&gt;Push the &lt;code&gt;-&lt;/code&gt; inside the &lt;code&gt;ln&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;$$
rows = \ln(\frac{1}{1 - confidence})
$$&lt;/p&gt;
&lt;p&gt;And we&amp;#39;ve gotten our formula for &lt;code&gt;rows&lt;/code&gt;!&lt;/p&gt;
&lt;h2&gt;Formulas to Code&lt;/h2&gt;
&lt;p&gt;Now we have formulas for &lt;em&gt;both&lt;/em&gt; &lt;code&gt;columns&lt;/code&gt; and &lt;code&gt;rows&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;$$
columns = \frac{e}{errorRate}
{} \
{} \
rows = \ln(\frac{1}{1 - confidence})
$$&lt;/p&gt;
&lt;p&gt;So if we wanted an error rate of 0.05% and a confidence of 99%, how many rows and columns would we need? Let&amp;#39;s calculate it in JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;function sketchWithBounds({
  errorRate,
  confidence,
}: {
  errorRate: number;
  confidence: number;
}): Sketch {
  const columns = Math.ceil(Math.E / errorRate);
  const rows = Math.ceil(Math.log(1 / (1 - confidence)));
  return createSketch({ rows, columns });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We try it out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const withBounds = sketchWithBounds({
  errorRate: 0.0005,
  confidence: 0.99,
});

console.log(&amp;#39;withBounds&amp;#39;, withBounds.columns, withBounds.rows);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we got 5437 columns and 5 rows!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;withBounds 5437 5
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Bonus 2: PNGs&lt;/h1&gt;
&lt;p&gt;Now, you may have wondered, how did we create our cool PNG? For posterity I thought I&amp;#39;d write out the algorithm.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s start off by installing a library to create PNGs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bun add pngjs
bun add -D @types/pngjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we&amp;#39;ll take a series of bytes. One pixel can be expressed as &lt;code&gt;R&lt;/code&gt; &lt;code&gt;G&lt;/code&gt; &lt;code&gt;B&lt;/code&gt; &lt;code&gt;A&lt;/code&gt;, each that&amp;#39;s one byte. So we can fit 4 bytes per pixel. Here&amp;#39;s a quick function to do that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PNG } from &amp;#39;pngjs&amp;#39;;

function createPNG({
  width,
  buffer,
}: {
  width: number;
  buffer: Buffer;
}): Buffer {
  const bytesPerPixel = 4; // RGBA
  const height = Math.ceil(buffer.length / (width * bytesPerPixel));
  const png = new PNG({
    width,
    height,
    colorType: 6, // RGBA
  });

  for (let i = 0; i &amp;lt; png.data.length; i++) {
    png.data[i] = buffer[i] ?? 0;
  }

  return PNG.sync.write(png);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A PNG for our Sketch&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s pick up our &lt;code&gt;allSketch&lt;/code&gt; we created before, and save it as a PNG:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const compressedSketch = await Bun.zstdCompress(allSketch.buckets);

fs.writeFileSync(
  &amp;#39;compressedSketch.png&amp;#39;,
  createPNG({ width: 150, buffer: compressedSketch }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aand we get our image!&lt;/p&gt;
&lt;div class=&quot;flex justify-center&quot;&gt;
  &lt;img class=&quot;m-0&quot; src=&quot;/posts/count_min_sketch/compressedSketch.png&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;But you may wonder, how would it look if we saved the exact counts?&lt;/p&gt;
&lt;h2&gt;A PNG for our exact counts&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s try that. We can pick up our &lt;code&gt;allExactCounts&lt;/code&gt; &lt;sup id=&quot;user-content-fnref-13&quot;&gt;&lt;a href=&quot;#user-content-fn-13&quot;&gt;[13]&lt;/a&gt;&lt;/sup&gt;, and save it as a PNG too:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const compressedExactCounts = await Bun.zstdCompress(
  JSON.stringify(allExactCounts),
);

fs.writeFileSync(
  &amp;#39;compressedExactCounts.png&amp;#39;,
  createPNG({ width: 150, buffer: compressedExactCounts }),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Load it up, and we see:&lt;/p&gt;
&lt;div class=&quot;flex justify-center&quot;&gt;
  &lt;img class=&quot;m-0&quot; src=&quot;/posts/count_min_sketch/compressedExactCounts.png&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Let&amp;#39;s see them side by side:&lt;/p&gt;
&lt;div class=&quot;flex items-start justify-center space-x-2&quot;&gt;
  &lt;div&gt;
    &lt;h3 class=&quot;text-center&quot;&gt;Sketch&lt;/h3&gt;
    &lt;img class=&quot;m-0 ml-2&quot; src=&quot;/posts/count_min_sketch/compressedSketch.png&quot; /&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;h3&gt;Exact Counts&lt;/h3&gt;
    &lt;img class=&quot;m-0&quot; src=&quot;/posts/count_min_sketch/compressedExactCounts.png&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h1&gt;Fin&lt;/h1&gt;
&lt;p&gt;Congratulations, you made it all the way through the bonus too!&lt;/p&gt;
&lt;p&gt;If you&#039;re into this stuff, I&#039;d suggest reading &lt;a href=&quot;http://dimacs.rutgers.edu/~graham/ssbd.html&quot; target=&quot;_blank&quot;&gt;Small Summaries for Big Data&lt;/a&gt;. It goes over the Count-Min Sketch, as well as a bunch of other probabilistic data structures. Plus, one of the co-authors invented the Count-Min Sketch!
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks to Joe Averbukh, Daniel Woelfel, Predrag Gruevski, Irakli Safareli, Nicole Garcia Fischer, Irakli Popkhadze, Mark Shlick, Ilan Tzitrin, Drew Harris, for reviewing drafts of this post&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: A sync engine you can try &lt;a href=&quot;/tutorial&quot;&gt;without even signing up&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;: See this &lt;a href=&quot;https://www.usenix.org/legacy/event/hotsec10/tech/full_papers/Schechter.pdf&quot;&gt;interesting paper&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;: I &lt;em&gt;think&lt;/em&gt; &lt;a href=&quot;https://web.archive.org/web/20170707141519/https://skillsmatter.com/skillscasts/6844-count-min-sketch-in-real-data-applications&quot;&gt;X&lt;/a&gt; is doing this, though I am not sure if it&amp;#39;s still the case.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;: For the curious, some of the code behind this lives &lt;a href=&quot;https://github.com/instantdb/instant/blob/main/server/src/instant/db/datalog.clj#L1349&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-6&quot; href=&quot;#user-content-fnref-6&quot;&gt;[6]&lt;/a&gt;  Bun&amp;#39;s standard library comes with a bunch of cool hashing and compression functions, so we won&amp;#39;t have to install extra packages to get our algorithms working:&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-7&quot; href=&quot;#user-content-fnref-7&quot;&gt;[7]&lt;/a&gt;  If we used a 2D array, each subarray would live in a separate place in memory. When we iterate, the CPU would have to jump around different places in memory, which would make its cache less useful.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;: You may be wondering: can we improve the error rate even more? Yes. One idea: &lt;a href=&quot;https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch#Reducing_bias_and_error&quot;&gt;conservative updating&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;: This is a great &lt;a href=&quot;https://www.youtube.com/watch?v=onZSWfbTeho&quot;&gt;explainer&lt;/a&gt; on Markov&amp;#39;s inequality.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;: Why do we pick the minimum value across rows? Well, when we added a word, we incremented the corresponding bucket in &lt;em&gt;every&lt;/em&gt; row. This means we know that at the minimum, a corresponding bucket will record the true count of our word. If some rows show a larger count, it&amp;#39;s because other words have collided and influenced the counts there.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt;: Intuitively, an Expected Value is a weighted average. This &lt;a href=&quot;https://www.youtube.com/watch?v=CBgCR1kHSUI&quot;&gt;video&lt;/a&gt; explains it well.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-12&quot; href=&quot;#user-content-fnref-12&quot;&gt;[12]&lt;/a&gt;  It&amp;#39;s non-negative because we only ever increment buckets.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-13&quot; href=&quot;#user-content-fnref-13&quot;&gt;[13]&lt;/a&gt;  You may wonder, is JSON stringify an efficient way to serialize it? At a glance it feels like it isn&amp;#39;t. But I ran a few tests with protobufs and msgpack, only to find out that JSON.stringify + zstd was more efficient. My guess is because zstd does a great job compressing the repetition in the JSON.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt;: The &lt;a href=&quot;https://dsf.berkeley.edu/cs286/papers/countmin-latin2004.pdf&quot;&gt;original paper&lt;/a&gt; chose to pick $e$, because it minimizes the number of buckets needed for a particular error rate and confidence. We could have picked any number here though, and we&amp;#39;d still be able to go through the proof.&lt;/p&gt;
</description>
      <pubDate>Mon, 13 Oct 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Video: Founding Firebase with James Tamplin</title>
      <link>https://instantdb.com/essays/founding_firebase</link>
      <guid isPermaLink="true">https://instantdb.com/essays/founding_firebase</guid>
      <description>&lt;p&gt;&lt;div class=&quot;md-video-container&quot;&gt;
  &lt;iframe
    width=&quot;100%&quot;
    src=&quot;https://www.youtube.com/embed/re64AhYrYBY?rel=0&amp;modestbranding=1&amp;playsinline=1&amp;autoplay=0&amp;cc_load_policy=1&quot;
    title=&quot;Founding Firebase with James Tamplin&quot;
    allow=&quot;autoplay; picture-in-picture&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Many of us built on top of Firebase, what was it like to build Firebase itself? We sat down with James Tamplin, the founder of Firebase to go over the early days. We recorded the video of the interview. We hope you enjoy it!&lt;/p&gt;
</description>
      <pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>HeroUI helps people build beautiful apps, on top of Instant</title>
      <link>https://instantdb.com/essays/heroui</link>
      <guid isPermaLink="true">https://instantdb.com/essays/heroui</guid>
      <description>&lt;p&gt;&lt;em&gt;This is part of a series of posts about the people who power their startups with Instant. Stories like Junior’s are what drive us to keep working on making the best tool for builders.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Junior Garcia is on a mission to help everyone make beautiful apps with &lt;a href=&quot;https://www.heroui.com/&quot; target=&quot;_blank&quot;&gt;HeroUI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Starting off in Venezuela, Junior built HeroUI components — a suite of primitives that have helped thousands of developers build frontends. From there Junior was accepted to YCombinator and launched HeroUI chat, an AI-powered tool that helps everyone build beautiful apps.&lt;/p&gt;
&lt;p&gt;In this post we’ll share his backstory, the lessons learned, and what’s ahead!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/heroui/junior.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h1&gt;From Venezuela&lt;/h1&gt;
&lt;p&gt;We start off in Valencia, Venezuela. Junior was studying electrical engineering at University, when he learned about microchips and how he could program them. He got hooked with an idea: how cool would it be to write a program that could get a microchip to make millions of calculations a second? It felt like a superpower.&lt;/p&gt;
&lt;p&gt;He just had one problem.&lt;/p&gt;
&lt;h1&gt;To Paper&lt;/h1&gt;
&lt;p&gt;For the first 19 years of his life, Junior didn’t have computer or an internet connection. He would have to be creative about how he would learn to program.&lt;/p&gt;
&lt;p&gt;So, Junior got creative. He went to his University’s library and picked up a book on Java. He would would go through each chapter and write out his solutions on paper. Then he would visit his girlfriend’s house, where he could borrow her computer and run his solutions. (You may be thinking, that’s a great girlfriend. Well, soon she would become Junior’s wife!)&lt;/p&gt;
&lt;p&gt;As Junior finished his book on Java, he got good enough to get a job at a tech company writing it. He dropped out of University and went to programming full time.&lt;/p&gt;
&lt;h1&gt;To Customizing Java&lt;/h1&gt;
&lt;p&gt;When Junior started building, he realized that the software written in Java didn’t tend to look so good. Java has it’s own renderer — different from what’s native on Windows and MacOS. This meant that the software often came off foreign and outdated.&lt;/p&gt;
&lt;p&gt;Bad UIs bothered Junior a lot more than many of his peers. In order to make Java programs beautiful, he would go through trouble after trouble. In those days it meant doing a bunch of black magic with PNGs to make Java programs feel natural. But once Junior went through the trouble he saw how users reacted, and he knew it was worth it.&lt;/p&gt;
&lt;h1&gt;To discovering a love of Design&lt;/h1&gt;
&lt;p&gt;Junior felt it in his bones that when you make apps beautiful, it’s not just about cosmetics. It’s about building something accessible and intuitive. People use intuitive software differently and it has a meaningful effect on their lives. After all, many of us use software for hours a day.&lt;/p&gt;
&lt;p&gt;This is when Junior realized that programming wasn’t his only passion. He loved design too.&lt;/p&gt;
&lt;p&gt;After writing apps in Java, he moved onto web technologies. Soon Junior started building a side project: he wanted to make it easy for developers to build portfolio sites.&lt;/p&gt;
&lt;h1&gt;To 25,000 stars on GitHub&lt;/h1&gt;
&lt;p&gt;To help developers build portfolio sites, Junior knew he’d need to use a series of shareable components. He couldn’t find anything that fit his needs, so he started to build them from scratch.&lt;/p&gt;
&lt;p&gt;Junior built component after component. He made sure that every primitive was accessible, came with animations, and all the best practices that delight users. Soon Junior realized that he was building a full component system.&lt;/p&gt;
&lt;p&gt;In early 2021, Junior packaged everything together and released HeroUI components (formerly NextUI). He was floored by the reception.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/heroui/stars.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Developers loved HeroUI components. &lt;strong&gt;Within a year, it hit 3000 stars. Within 2 years, 9000 stars. Today, over 25,000 stars.&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;To the first check&lt;/h1&gt;
&lt;p&gt;GitHub stars grew, but it didn’t mean the journey was easy.&lt;/p&gt;
&lt;p&gt;Junior had a full-time job, which meant he did all of this work on nights and weekends. When he had doubts, Junior had his wife’s belief to fall back too. He would get energized and then focus on users.&lt;/p&gt;
&lt;p&gt;One user reached out and surprised Junior. Turns out Vicente Zavarce was a happy HeroUI user. He was also the founder Yummy, one of Latin America’s biggest startups.&lt;/p&gt;
&lt;p&gt;Vicente wanted meet the team behind the components and scheduled a call. He was expecting to see a large group, but he just found Junior.&lt;/p&gt;
&lt;p&gt;Vicente was so impressed that he offered to make an investment. That kicked off a pre-seed round and HeroUI became a company. Junior could now focus full time on making it easy to create beautiful apps.&lt;/p&gt;
&lt;h1&gt;To YC S24&lt;/h1&gt;
&lt;p&gt;What followed was a flurry of work. HeroUI kept getting better. Junior and his team launched support for Tailwind and started to work on a series of pro components.&lt;/p&gt;
&lt;p&gt;When the team was building a checkout page for pro components, they released a secret URL to dogfood it. Users were so excited that they actually found the link and started buying.&lt;/p&gt;
&lt;p&gt;At this point Junior knew he had to grow the team. So he applied to YCombinator.&lt;/p&gt;
&lt;p&gt;Junior had no expectations, but YC saw the potential in him and in HeroUI. The YC partners know how hard it was to build a startup as a solo founder, and Junior was a solo founder. But after meeting him, the partners were convinced that he could do it, and HeroUI joined YC S24.&lt;/p&gt;
&lt;h1&gt;To HeroUI Chat&lt;/h1&gt;
&lt;p&gt;Being surrounded by such talented people, Junior was invigorated and kept making HeroUI better. At this point he was able to hire some of the best HeroUI contributors full-time, and they were full-steam ahead.&lt;/p&gt;
&lt;p&gt;During YC they started off by building a tool to help companies with design systems. They built the product, but the more they talked to users, the more they realized they actually wanted something else.&lt;/p&gt;
&lt;p&gt;Users wanted help building their UIs. At this point Claude Sonnet 3 had come out. That’s when Junior thought, &lt;strong&gt;what if you could use AI to help you create truly beautiful UIs?&lt;/strong&gt; That got Junior and the team excited. So they started building &lt;a target=&quot;_blank&quot; href=&quot;https://heroui.chat/&quot;&gt;HeroUI Chat&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Optimizing for speed&lt;/h2&gt;
&lt;p&gt;The first step was to decide how to build it. If you’re making an app that helps users make beautiful apps, you better make sure the app itself is beautiful.&lt;/p&gt;
&lt;p&gt;Junior was confident in the UI. He wanted to make sure the backend felt great too:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I was obsessed with speed. I wanted HeroUI chat to be really fast. Creating a new conversation, modifying a title, every tiny detail should feel fast.&lt;/p&gt;
&lt;p&gt;Junior Garcia&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They did an investigation, and they listed out what they needed to make apps feel fast. They would have to leverage the browser and work with IndexedDB. They would have to add caches and build out a suite of optimistic updates.&lt;/p&gt;
&lt;p&gt;Most of the solutions they found were too constraining: either they were full frameworks, or they were exceptionally difficult to use. So they decided they would build a sync engine from scratch. Until they found Instant.&lt;/p&gt;
&lt;h2&gt;Finding Instant: A fast &lt;em&gt;and&lt;/em&gt; realtime MVP in 2 days&lt;/h2&gt;
&lt;p&gt;Junior was scrolling Bookface, when he saw a post about Instant’s infra:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Not only did you have the optimistic updates I was looking for, but you had the real-time updates. You handled collisions too. Basically everything we were worried about.&lt;/p&gt;
&lt;p&gt;Junior Garcia&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instant looked like an exact fit, so they decided to give it a try. &lt;strong&gt;Within 2 days, HeroUI had a full MVP on Instant.&lt;/strong&gt; When we asked Junior how he thought about Instant after that, he answered:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At that point I did not want to use any database other than Instant&lt;/p&gt;
&lt;p&gt;Junior Garcia&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Junior and the team had used Firebase before, and knew how difficult it was to build apps when you don’t have relations. They were very happy with Instant’s relational query engine.&lt;/p&gt;
&lt;p&gt;And the optimistic updates had paid off too. Many HeroUI users (and employees) lived across continents. Instant’s local caches made people feel like everything fast, without Junior and team having to worry about setting up replicas across the globe.&lt;/p&gt;
&lt;h2&gt;Hitting #1 on Product Hunt&lt;/h2&gt;
&lt;p&gt;With the right infra in place, they could focus on their product, and they made a tool that was truly useful. They &lt;a href=&quot;https://www.producthunt.com/products/heroui-chat/launches/heroui-chat&quot; target=&quot;_blank&quot;&gt;launched&lt;/a&gt; on Product Hunt, and hit #1 for the day.&lt;/p&gt;
&lt;p&gt;&lt;div class=&quot;md-video-container&quot;&gt;
  &lt;iframe
    width=&quot;100%&quot;
    src=&quot;https://www.youtube.com/embed/rRT9lZfJjR0?rel=0&amp;modestbranding=1&amp;playsinline=1&amp;autoplay=0&amp;cc_load_policy=1&quot;
    title=&quot;HeroUI Demo&quot;
    allow=&quot;autoplay; picture-in-picture&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Users could build delightful UIs. Since everything was on top of hand-crafted components, AIs could focus on writing simpler code, which was easier for humans to maintain. And whenever AIs made a mistake, Junior and the team would jump and fix it in the platform.&lt;/p&gt;
&lt;p&gt;This flywheel of improvement kept on going and making HeroUI better.&lt;/p&gt;
&lt;h2&gt;The productivity benefits of real-time sync&lt;/h2&gt;
&lt;p&gt;Instant made it easy to build it MVP, but the HeroUI team saw that Instant helped them scale too. The biggest lever came from the client-side abstraction.&lt;/p&gt;
&lt;p&gt;In traditional apps every feature requires (a) a frontend change (b) an update to the store (c) an update to endpoints, and (d) an update on the database. With Instant, all of this compressed to one change. This meant the codebase was easier for engineers to onboard too, and features were simpler to implement.&lt;/p&gt;
&lt;p&gt;When we asked Junior what he would missed the most if he couldn’t use Instant anymore, this abstraction was what he mentioned:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I think I couldn&amp;#39;t deal with the frontend-backend request schlep anymore. Having to call an endpoint, send data, receive data, update the UI, handle the update, handle the rollback. Instant just does this automatically. You don&amp;#39;t have to communicate to the backend, or listen to changes. Losing this would make our lives so much harder on Hero.&lt;/p&gt;
&lt;p&gt;Junior Garcia&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;From UIs to full apps&lt;/h1&gt;
&lt;p&gt;It’s been 5 years of work, but Junior and the team are just getting started. Today developers, business owners, and big companies use HeroUI to build full frontends. But HeroUI keeps getting better faster.&lt;/p&gt;
&lt;p&gt;Soon you’ll be able to build full-stack web apps and mobile apps, with the same design system across platforms. HeroUI keeps marching towards the same goal — to help make all apps beautiful — and we are so excited to support them!&lt;/p&gt;
</description>
      <pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Mirando transforms Latin-American Real-Estate on top of Instant</title>
      <link>https://instantdb.com/essays/mirando</link>
      <guid isPermaLink="true">https://instantdb.com/essays/mirando</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a first of a series of posts about the people who power their startup with Instant! Stories like Ignacio and Javier’s are what drive us to keep working on making the best tools for builders.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Ignacio De Haedo and Javier Rey left their software engineering jobs at Meta to build &lt;a href=&quot;https://www.mirando.com.uy/&quot; target=&quot;_blank&quot;&gt;Mirando&lt;/a&gt;, an AI-powered real-estate platform for Latin America. In this post we’ll share their backstory, the lessons learned, and what’s ahead!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/mirando/javier_and_ignacio.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h1&gt;From Meta&lt;/h1&gt;
&lt;p&gt;Our story starts with Ignacio. Ignacio worked at Meta for 6 and half years. Towards the tail-end he started to burn out. On one flight from Latin America back to London he had a realization: he wasn’t tired of building, but he was tired of building things he didn’t care about. Ignacio knew it was time to move on.&lt;/p&gt;
&lt;p&gt;There was of course one person he had to convince: his wife. He got her blessing, pushed the button, and started to hack on his own projects.&lt;/p&gt;
&lt;h1&gt;To Mentor&lt;/h1&gt;
&lt;p&gt;The next 9 months was a crash-course on startups.&lt;/p&gt;
&lt;p&gt;When you work at Meta you get speciality tools to ship products and support to grow them. Ignacio had to get acquainted with infra outside of Meta and learn to grow his own products as a solo engineer.&lt;/p&gt;
&lt;h2&gt;Coming up with an idea&lt;/h2&gt;
&lt;p&gt;One app Ignacio wished he had when he was younger was a tool to help him pick careers. Talking to a lot of younger folks (including his younger brother), it felt more important than ever. And AI was just coming on the scene.&lt;/p&gt;
&lt;p&gt;What if AI could help you flesh out your thinking? You could start with rough goals, and AI would help you think through next steps. Ignacio soon saw that this was more general than careers.&lt;/p&gt;
&lt;p&gt;This product could help you achieve &lt;em&gt;any&lt;/em&gt; goal. It would be almost like having a…mentor!&lt;/p&gt;
&lt;h2&gt;Building Mentor&lt;/h2&gt;
&lt;p&gt;So Ignacio started building Mentor. He wanted to move quickly and focus on what made his product special: the UX for goals and a great AI integration.&lt;/p&gt;
&lt;p&gt;He searched for infra and discovered Instant. &lt;strong&gt;He tried it out and was able to build his version 1 within 2 weeks.&lt;/strong&gt; From his own words:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The fact that you include everything: from auth, a data layer, a client sdk, a way to mutate on the server, and permissions…it’s not any one thing. When you put this together they create a great experience.&lt;/p&gt;
&lt;p&gt;Ignacio De Haedo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When a tool is batteries-included, things just tend to work. Instant’s real-time abstractions also made it easier for Ignacio to create a delightful UX.&lt;/p&gt;
&lt;p&gt;Goals are inherently relational: every goal has a subgoal and so on. This was easy for him to express with Instant’s relational query language. And since everything is real-time, every tab auto-synced. Because Instant worked offline, it meant Ignacio’s users could run Mentor in spotty connections. And since optimistic updates came by default, every action on Mentor felt snappy.&lt;/p&gt;
&lt;p&gt;Soon Ignacio had a pretty darn compelling app. Here’s a quick demo of how Mentor can you help get in the best shape of your life:&lt;/p&gt;
&lt;p&gt;&lt;div class=&quot;md-video-container&quot;&gt;
  &lt;iframe
    width=&quot;100%&quot;
    src=&quot;https://www.youtube.com/embed/hp3byaULieQ?rel=0&amp;modestbranding=1&amp;playsinline=1&amp;autoplay=0&amp;cc_load_policy=1&quot;
    title=&quot;Mentor Demo&quot;
    allow=&quot;autoplay; picture-in-picture&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Now that Ignacio had a product he could focus on users.&lt;/p&gt;
&lt;h2&gt;Hitting the top of Product Hunt…twice&lt;/h2&gt;
&lt;p&gt;He launched on Product Hunt and hit the front page not &lt;a href=&quot;https://www.producthunt.com/products/mentor-v1/launches/mentor-v1&quot; target=&quot;_blank&quot;&gt;once&lt;/a&gt;, but &lt;a href=&quot;https://www.producthunt.com/products/mentor-v1/launches/mentor-ai-2&quot; target=&quot;_blank&quot;&gt;twice&lt;/a&gt;. He iterated until he felt Mentor reached a stable state: users were fans, and Ignacio himself used Mentor every day.&lt;/p&gt;
&lt;p&gt;As Ignacio kept improving Mentor, he traveled back to his hometown in Uruguay and met his close friend Javier. That’s when everything changed again.&lt;/p&gt;
&lt;h1&gt;To Meeting Javier&lt;/h1&gt;
&lt;p&gt;Javier was a veteran AI engineer. He worked on AI for the last 10 years, long before people had ever heard of ChatGPT or transformers.&lt;/p&gt;
&lt;p&gt;They both shared a love for real-estate and prop-tech, and they both saw an opportunity in Latin America — a market they were deeply familiar with, and knew was underserved.&lt;/p&gt;
&lt;p&gt;Latin America is full of great engineers, but most of them export their work to the United States. This means a lot of technology used day-to-day feels outdated. Ignacio and Javier experienced this first-hand with real-estate.&lt;/p&gt;
&lt;h1&gt;To Building Mirando&lt;/h1&gt;
&lt;p&gt;As home buyers, Ignacio and Javier found themselves frustrated. There was no single place to find every listing. Agents all had separate sites, and many agents listed the same homes. This meant that you’d have spend lots of time scouring different websites and manually de-duping homes.&lt;/p&gt;
&lt;p&gt;They realized the experience was no better for agents too. When a home buyer signs up with an agent, they want a tailored experience. Home buyers want to see a list of places that fit their requirements. To build a list like this agents would have to go across multiple different sites, negotiate with their colleagues, and build custom documents. That would take days.&lt;/p&gt;
&lt;p&gt;So Ignacio and Javier started to think of solutions. What if you could get all homes in place, and you could get an AI that could help you find homes that you love? Agents could use this too and reduce their research time from days to minutes.&lt;/p&gt;
&lt;h2&gt;The Script that Started It&lt;/h2&gt;
&lt;p&gt;As a proof of concept Javier built a script that amalgamated homes across a few agent sites in one place. Just this was already a huge improvement. So they started to turn the script into a real app.&lt;/p&gt;
&lt;h2&gt;Convincing Javier on Instant&lt;/h2&gt;
&lt;p&gt;Javier first started to build a version 1 on an Instant competitor &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;. When Ignacio saw this, he knew he had convince Javier to switch. The competitor’s product worked, but things weren’t real time, it took longer to build, and the devex didn’t feel right.&lt;/p&gt;
&lt;p&gt;So what did Ignacio do? &lt;strong&gt;He shipped a PR to demonstrate the difference&lt;/strong&gt;. The PR was full of deletions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The diff. It was crazy the number of lines of code I deleted. And we got wins. The live sync. The auth was better. And the free tier was a lot better. I showed him the diff, and [Javier] said I trust you.&lt;/p&gt;
&lt;p&gt;Ignacio De Haedo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://x.com/nachodeh/status/1871232369614582174&quot; target=&quot;_blank&quot;&gt;Ignacio reduced the code-size by 70%&lt;/a&gt;. With Javier on board, Ignacio built out the UX and Javier built out the AI.&lt;/p&gt;
&lt;h2&gt;Shipping Mirando&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.mirando.com.uy/&quot;&gt;Mirando&lt;/a&gt; launched with very happy users. Home buyers and agents finally had a place where they could look through multiple homes.&lt;/p&gt;
&lt;p&gt;Search was a first class citizen on Mirando too. You could create a search, you could share it, and it was real-time. The more you customized your search, the more info the AI had to tailor your experience.&lt;/p&gt;
&lt;p&gt;Soon large agencies were knocking down their doors. They wanted to give Mirando to their agents, so they could create custom-tailored searches for clients.&lt;/p&gt;
&lt;h2&gt;Surprises in real-time sync&lt;/h2&gt;
&lt;p&gt;Ignacio and Javier believed that when you’re searching for homes, it should feel &lt;em&gt;fun.&lt;/em&gt; You should be able to share your search with friends and loves ones. If someone likes a home, everyone should see it right away.&lt;/p&gt;
&lt;p&gt;Instant’s real-time sync came surprisingly handy for that. With reactive queries, they could create a search experience that felt collaborative. Here’s a demo of how a Mirando user could collaborate on a search with someone else:&lt;/p&gt;
&lt;p&gt;&lt;div class=&quot;md-video-container&quot;&gt;
  &lt;iframe
    width=&quot;100%&quot;
    src=&quot;https://www.youtube.com/embed/3wda2j2yJCE?rel=0&amp;modestbranding=1&amp;playsinline=1&amp;autoplay=0&amp;cc_load_policy=1&quot;
    title=&quot;Mirando Demo&quot;
    allow=&quot;autoplay; picture-in-picture&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;When we asked Ignacio what he loved the most about Instant, he picked sync:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The real-time sync. It&amp;#39;s the feature that reduces the most boilerplate code. It&amp;#39;s the feature that makes the app feel like magic. And it helps justify a lot of the UX efforts we want to build.&lt;/p&gt;
&lt;p&gt;Ignacio De Haedo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;From Montevideo to Latin America&lt;/h1&gt;
&lt;p&gt;It’s only the beginning for Ignacio and Javier. They started working on Mirando in January. Today they have large agencies signing up in Montevideo, Uruguay. They have a truly delightful experience, and an AI agent that keeps getting better after every search.&lt;/p&gt;
&lt;p&gt;They plan to grow to all of Latin America, and we’re so darn excited to be supporting their infra.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-1&quot; href=&quot;#user-content-fnref-1&quot;&gt;[1]&lt;/a&gt;  For gentlemanly reasons we will not mention names&lt;/p&gt;
</description>
      <pubDate>Mon, 18 Aug 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>GPT 5 vs Opus 4.1 for Vibe-Coded Apps</title>
      <link>https://instantdb.com/essays/gpt_5_vs_opus_4</link>
      <guid isPermaLink="true">https://instantdb.com/essays/gpt_5_vs_opus_4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;We&amp;#39;re InstantDB, we make it easy to add a backend with auth, file storage, and
real-time updates to your web and mobile apps.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;#39;ve been seeing posts comparing GPT-5 and Sonnet, but thought comparing GPT-5
and Opus 4.1 would be more interesting!&lt;/p&gt;
&lt;p&gt;So how do GPT-5 and Opus 4.1 perform with building apps? To find out I asked them both to build a full stack app for making chiptunes in Instant. Here’s the prompt I used:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Create a chiptunes app.

- Log in with magic codes
- Users should be able to compose songs
- Users should be able to share songs
- Users can only edit their own songs
- Make the theme really cool
- Let’s keep everything under 1000 lines of code.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I recorded myself going through the process in this &lt;a href=&quot;https://youtu.be/yzjC0wcMvxI&quot; target=&quot;_blank&quot;&gt;video&lt;/a&gt;. In this post I’ll share the results and some of the surprises I discovered when prompting!&lt;/p&gt;
&lt;h1&gt;What a change in 4 months…&lt;/h1&gt;
&lt;p&gt;We actually ran the same test in April. We compared o4-mini with Claude 3 Sonnet. o4-mini made a barebones version (see &lt;a href=&quot;https://codex-chiptunes.vercel.app/&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;). Sonnet made a good UI but couldn’t actually write the backend logic.&lt;/p&gt;
&lt;p&gt;Now both apps look pretty cool, both apps have auth, permissions, and a much slicker way to compose songs.&lt;/p&gt;
&lt;h1&gt;GPT5’s work&lt;/h1&gt;
&lt;p&gt;Here’s the result that GPT-5 came up with: &lt;a href=&quot;https://gpt5-chiptunes.vercel.app/&quot;&gt;https://gpt5-chiptunes.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can log in, create songs, and share them. This was my creation:&lt;/p&gt;
&lt;p&gt;&lt;demo-iframe uri=&quot;https://gpt5-chiptunes.vercel.app/song/3b527d40-abab-43bc-ad82-61ad0f22b12c&quot;&gt;&lt;/demo-iframe&gt;&lt;/p&gt;
&lt;h1&gt;Opus’ Work&lt;/h1&gt;
&lt;p&gt;And this is what Opus came up with: &lt;a href=&quot;https://opus-chiptunes.vercel.app/&quot;&gt;https://opus-chiptunes.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We needed a few more prompts to get sharing working, but once it did, here’s one song our co-founder Joe came up with:&lt;/p&gt;
&lt;p&gt;&lt;demo-iframe uri=&quot;https://opus-chiptunes.vercel.app/?song=79a4353d-8886-44a3-b905-b57b7bae27fd&quot;&gt;&lt;/demo-iframe&gt;&lt;/p&gt;
&lt;h1&gt;How much got done in one shot&lt;/h1&gt;
&lt;p&gt;Both models got a &lt;em&gt;lot&lt;/em&gt; done in one shot.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;What got done in one shot&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT-5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Opus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create songs?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share Songs?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI?&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;They both figured out auth, data models, permissions, and at least the flow to create songs in one go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One difference is that GPT-5 was able to get song sharing working in one shot.&lt;/strong&gt; Opus needed two additional nudges to get there. Initially Opus talked about making songs shareable, but did not actually implement it. First Opus added support for sharing songs, but gated it to logged in users. A second prompt helped Opus open songs for public consumption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However, Opus’ UI was more slick.&lt;/strong&gt; You can also see that GPT-5&amp;#39;s UI has some responsiveness issues on mobile. I do think OpenAI improved UI skills a lot compared to their earlier models. For now I think Opus has the edge in UI.&lt;/p&gt;
&lt;h1&gt;Hiccups&lt;/h1&gt;
&lt;p&gt;Both models made a few errors before the projects built. Here’s how that looked:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Places the models had an error&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT-5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Opus&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.SignedIn?&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query Issues?&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next Query Params?&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Both models made about 2 errors. &lt;strong&gt;All errors were all related to new features.&lt;/strong&gt; Next.js has a new flow for query params, and Instant just added a &amp;quot;db.SignedIn&amp;quot; component.&lt;/p&gt;
&lt;p&gt;But &lt;strong&gt;both models fixed all errors in one shot&lt;/strong&gt;. They just needed me to paste an error message and they were able to solve it.&lt;/p&gt;
&lt;p&gt;It was interesting to see how GPT-5 made an error with &amp;quot;db.SignedIn&amp;quot;. Instructions for how to use it were already included in the &lt;a href=&quot;https://www.instantdb.com/llm-rules/next/cursor-rules.md&quot; target=&quot;_blank&quot;&gt;rules.md&lt;/a&gt; file. I think this is related to how closely the models follow rules.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opus seemed to follow the rule file more closely, while GPT-5 seems to explore more&lt;/strong&gt;. Opus used the exact same patterns that provided in the rules file. This let them skip past the &amp;quot;db.SignedIn&amp;quot; bug. On the other hand, GPT-5 seemed to be more free with what it tried. It did get more bugs, but it wrote code that was objectively more &amp;quot;different&amp;quot; then the examples that we provided. In one case, it wrote a simpler schema file.&lt;/p&gt;
&lt;h1&gt;Gaps are closing&lt;/h1&gt;
&lt;p&gt;This is &lt;a href=&quot;https://github.com/stopachka/gpt-5-chiptunes&quot; target=&quot;_blank&quot;&gt;GPT-5 source&lt;/a&gt;, and this is the &lt;a href=&quot;https://github.com/stopachka/opus-chiptunes&quot; target=&quot;_blank&quot;&gt;Opus source&lt;/a&gt;. In the last few months it feels like Claude and Claude Code have been the dominant choice for vibe coding apps. With the new GPT5 model it feels like the gap is closing.&lt;/p&gt;
&lt;p&gt;Really interesting times ahead!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Thanks to Joe Averbukh, Daniel Woelfel for reviewing drafts of this post&lt;/em&gt;&lt;/p&gt;
</description>
      <pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>How and where will agents ship software?</title>
      <link>https://instantdb.com/essays/agents</link>
      <guid isPermaLink="true">https://instantdb.com/essays/agents</guid>
      <description>&lt;p&gt;We’re entering a new phase of software engineering. People are becoming addicted to agents. Beginners are vibe-coding apps and experts are maxing out their LLM subscriptions. This means that a lot more people are going to make a lot more apps, and for that we’re going to need new tools.&lt;/p&gt;
&lt;p&gt;Today we’re releasing an API that gives you and your agents full-stack backends. Each backend comes with a database, a sync engine, auth tools, file storage, and presence.&lt;/p&gt;
&lt;p&gt;Agents can use these tools to ship high-level code that’s easier for them to write and for humans to review. It’s all hosted on multi-tenant infrastructure, so you can spin up millions of databases in milliseconds. We have a &lt;a href=&quot;#demo&quot;&gt;demo&lt;/a&gt; at the end of this essay.&lt;/p&gt;
&lt;p&gt;Let us explain exactly why we built this. We think that humans and agents can make the most progress when they have (1) built-in abstractions that (2) can be hosted efficiently and (3) expose data.&lt;/p&gt;
&lt;h1&gt;Built-in Abstractions&lt;/h1&gt;
&lt;p&gt;To build an app you write two kinds of code. The business logic that solves your specific problem, and the generic stuff that most apps have to take care of: authenticating users, making queries, running permissions, uploading files, and executing transactions.&lt;/p&gt;
&lt;p&gt;These are simultaneously critical to get right, full of edge cases, and also not the differentiating factor for your app — unless they’re broken.&lt;/p&gt;
&lt;p&gt;If all this work isn’t differentiating, why build it? When a good abstraction exists, it’s a waste of tokens to build it again.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/good_abstractions.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;And agents need good abstractions even more than human programmers do.&lt;/p&gt;
&lt;h2&gt;Locality&lt;/h2&gt;
&lt;p&gt;To make agents work well we need to manage their context windows. It’s very easy to break through limits. Especially when agents write code that involves multiple moving pieces.&lt;/p&gt;
&lt;p&gt;Consider what happens when an agent adds a feature to a traditional client-server app. They change (a) the frontend (b) the backend and (c) the database. In order to safely make these changes, they have to remember more of the codebase and be exact about how things work together.&lt;/p&gt;
&lt;p&gt;Good abstractions can combine multiple moving pieces into one piece. This is more conducive to local reasoning. The agent only has to concern themselves with a smaller interface, so they don’t have to remember so much. They can use less context and write higher-level code. And that’s great for humans too. After all we have to review the agent’s work. Shorter, higher-level code is easier to understand. &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;And when both humans and agents make more progress, they build more apps. And when they build more apps, how will they host them?&lt;/p&gt;
&lt;h1&gt;Cost-Efficient Hosting&lt;/h1&gt;
&lt;p&gt;The dominant way to host applications has been to use virtual machines. VMs are efficient when you have a single app that serves many users. They’re inefficient when you have many apps that serve fewer users.&lt;/p&gt;
&lt;h2&gt;Overhead&lt;/h2&gt;
&lt;p&gt;Let me illustrate with some napkin math. Consider 1 app that servers 20,000 active users, versus 20,000 apps that serve 1 active user:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/big_vs_small.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For our 1 big app, we would need 2 beefy VMs. That’s about $800 a month. Not only is this affordable, but it makes for a fast app. Slow algorithms can take advantage of hefty CPUs and a lot more data can stay in memory.&lt;/p&gt;
&lt;p&gt;For our 20,000 small apps we would need 40,000 VMs. That’s about $95,000 a month. Not only is this expensive, but it makes for slow apps. Slow algorithms would choke tiny CPUs and less data would stay in memory.&lt;/p&gt;
&lt;h2&gt;Friction&lt;/h2&gt;
&lt;p&gt;We’re not suggesting that people want to make 20,000 apps. We’re pointing out an inefficiency. Running applications today comes with overhead, particularly in RAM.&lt;/p&gt;
&lt;p&gt;And when there’s overhead there’s friction. Today platforms freeze machines or limit how many apps you can spin up. In an era where every human can create lots of apps, this feels like a bummer.&lt;/p&gt;
&lt;p&gt;Could we do better?&lt;/p&gt;
&lt;h2&gt;Getting Specific&lt;/h2&gt;
&lt;p&gt;Let’s think about why we needed VMs in the first place. VMs let programmers write code that’s arbitrarily different. But most apps aren’t arbitrarily different.&lt;/p&gt;
&lt;p&gt;If we can get specific about what applications actually do, we can choose better isolation strategies.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/getting_specific.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;For example what if we knew that an agent didn’t have to use the GPU? We could skip traditional VMs and use Micro VMs &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt; instead. That reduces the overhead by a few tens of megabytes of RAM, and lets us spin down inactive apps &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;. That’s better, but we can keep going.&lt;/p&gt;
&lt;p&gt;What if we knew that an agent wanted to write Javascript functions? We could skip VMs and use V8 Isolates &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;. Each isolate takes about 3 megabytes of RAM. That’s 2 orders of magnitude more efficient. But we can still keep going.&lt;/p&gt;
&lt;p&gt;What if we knew that agent wanted to write access controls? We could give them a more restricted language like CEL &lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;. CEL only needs a few kilobytes of overhead per function. That’s close to 4 orders of magnitude more efficient than VMs. And we can still keep going.&lt;/p&gt;
&lt;p&gt;What if the agent didn’t have to write any code at all? If we knew what the agent was trying to accomplish — say to authenticate users — we could give them a multi-tenant service which did that.&lt;/p&gt;
&lt;h2&gt;A maximally efficient future&lt;/h2&gt;
&lt;p&gt;We can create efficient apps by choosing appropriate isolation strategies.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/max_efficient.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Shared abstractions could be served from multi-tenant services on big machines. Permissions could use CEL, javascript callbacks could run on V8 Isolates, and shell commands run on Micro VMs. If we did that, 20,000 apps with 1 active user would cost about the same as 1 app with 20,000 users.&lt;/p&gt;
&lt;p&gt;Humans and agents would be able to deploy apps with little friction. Once these apps are deployed, how will people use them?&lt;/p&gt;
&lt;h1&gt;Exposed Data&lt;/h1&gt;
&lt;p&gt;Traditionally, end-users were non-technical and would be stuck with whatever the application developer gave them. But now every user has an LLM too.&lt;/p&gt;
&lt;p&gt;If one agent helps build the software, why shouldn’t another agent be able to extend it?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/exposed_data.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When every user has an agent, extendable software is an advantage. It’s in the application developer’s best interest: it can turn their apps into platforms, which are stickier. And it’s in the end-user’s best interest: they can get more out of their apps.&lt;/p&gt;
&lt;p&gt;To make software extendable, developers generally used APIs. But APIs have a problem: application developers have to build them first. This means users are limited by what application developers &lt;em&gt;thought&lt;/em&gt; were needed.&lt;/p&gt;
&lt;p&gt;Databases are different. When apps are written on a database-like abstraction, users are free to make arbitrary queries and transactions. The application developer doesn’t have to foresee much. End-users can read and write whatever data they need to build all sorts of custom UIs &lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;And if that&amp;#39;s true, database-like abstractions are going to be an advantage.&lt;/p&gt;
&lt;h1&gt;A Multi-Tenant Sync Engine&lt;/h1&gt;
&lt;p&gt;So if agents and humans work best when they have (1) built-in abstractions that are (2) hosted efficiently and (3) expose data, what infrastructure works best?&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s start by thinking through what agents are good at. Agents are good at writing self-contained code. Code that they can reason about in one place, without too much extraneous state and edge cases. This is why the traditional client-server architecture is hard for them: it involves multiple parts that all need to work in unison — a server, a client, and a database.&lt;/p&gt;
&lt;p&gt;There are several ways to build self-contained apps. You can build a local-only desktop app (but then — no internet, multiple devices, or collaboration). You can build a server-only app (then you get latency, no offline mode, hosting costs). Or you could build a client-only app that treats the backend like a remote database.&lt;/p&gt;
&lt;p&gt;In other words, a sync engine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/sync_engine.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Sync engines let you work with data as if it was local and not worry about fetching it, persisting it, managing optimistic state, atomic transactions, retries and many other schleps. That’s a powerful abstraction (1).&lt;/p&gt;
&lt;p&gt;Queries and transactions are straight-forward to sandbox. You can host them on multi-tenant platforms. Which makes for efficient apps (2).&lt;/p&gt;
&lt;p&gt;And since you get a database-like abstraction, exposing data is relatively straightforward too (3).&lt;/p&gt;
&lt;p&gt;That’s the future we are building Instant for.&lt;/p&gt;
&lt;h1&gt;A Tool for Builders&lt;/h1&gt;
&lt;p&gt;When we started Instant, agents were nowhere in sight. We focused on builders. Turns out if you design for builders, you end up making something good for agents too.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/agents/instant_arch.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Builders want good abstractions. So we built a sync engine, permissions, auth, file storage, and ephemeral state (like cursors).&lt;/p&gt;
&lt;p&gt;Builders also want efficient hosting. They have lots of projects, and it sucks when apps end up frozen. So we made our sync engine and database multi-tenant. This way we could offer a generous free tier.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;demo&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Exposing the API&lt;/h1&gt;
&lt;p&gt;Instant is already great for builders. Real startups use Instant, and push upwards of 10,000 concurrent connections.&lt;/p&gt;
&lt;p&gt;Today we&amp;#39;re making it even easier. We&amp;#39;re releasing three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/instantdb/instant/tree/main/client/packages/platform&quot; target=&quot;_blank&quot;&gt;A platform SDK&lt;/a&gt; that lets you create new apps on demand&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.instantdb.com/docs/using-llms#instant-mcp-server&quot; target=&quot;_blank&quot;&gt;A remote MCP server&lt;/a&gt; that makes it easy to integrate Instant in your editor.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.instantdb.com/docs/using-llms#instant-rules&quot; target=&quot;_blank&quot;&gt;A set of Agent rules&lt;/a&gt; that teach LLMs how to use Instant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Put this together and you get a toolkit that lets humans and agents make more progress and do it efficiently. Let&amp;#39;s try them out.&lt;/p&gt;
&lt;p&gt;&lt;agents-essay-demo-section&gt;&lt;/agents-essay-demo-section&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=44585002&quot;&gt;Discussion on HN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to Joe Averbukh, Daniel Woelfel, Alex Kotliarskyi, Ian Alejandro Sinnott, Cam Glynn, Anupam Batra, Predrag Gruevski, Irakli Popkhadze, Cody Breene, Kote Mushegiani, Nicole Garcia Fischer for reviewing drafts of this essay&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-1&quot; href=&quot;#user-content-fnref-1&quot;&gt;[1]&lt;/a&gt;  We can probably make the review experience even better. If code is high-level enough, maybe we don’t need to show it. We could build UIs around abstractions and use them to summarize changes.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;: ​​To learn more, check out &lt;a href=&quot;https://firecracker-microvm.github.io/&quot;&gt;Firecracker&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;: Though there’s some caveats to Micro VMs. Spinning up VMs still take a few hundred milliseconds. Some operations are &lt;a href=&quot;https://github.com/kata-containers/kata-containers/issues/3452&quot;&gt;slow&lt;/a&gt;, and sometimes you can&amp;#39;t spin them down (if you have a database with logical replication for example).&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;: Check out &lt;a href=&quot;https://blog.cloudflare.com/cloud-computing-without-containers/&quot;&gt;this essay&lt;/a&gt; from Cloudflare&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;: The &lt;a href=&quot;https://cel.dev/&quot;&gt;CEL website&lt;/a&gt; is a good place to learn more.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-6&quot; href=&quot;#user-content-fnref-6&quot;&gt;[6]&lt;/a&gt;  This opens up more questions. If you expose data, could you expose UIs too? What if every app shared their UI components. This is a bit too hazy to include in the essay, but it could make for an interesting experiment.&lt;/p&gt;
</description>
      <pubDate>Mon, 14 Jul 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili, Nikita Prokopov</author>
    </item>
    <item>
      <title>Sync Engines are the Future</title>
      <link>https://instantdb.com/essays/sync_future</link>
      <guid isPermaLink="true">https://instantdb.com/essays/sync_future</guid>
      <description>&lt;p&gt;&lt;em&gt;Hi! Niki here, also known as @nikitonsky. You might know me for &lt;a href=&quot;https://github.com/tonsky/datascript&quot;&gt;DataScript&lt;/a&gt;, &lt;a href=&quot;https://tonsky.me/blog/the-web-after-tomorrow/&quot;&gt;The Web After Tomorrow&lt;/a&gt; or &lt;a href=&quot;https://www.hytradboi.com/2022/your-frontend-needs-a-database/&quot;&gt;Your frontend needs a database&lt;/a&gt;. Last December, I joined Instant to continue my journey of bringing databases into the browser. Here’s my mission:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The modern browser is an OS. Modern web app is a distributed app. So any web app developer is facing a well-known, well-understood, notoriously hard problem: syncing data.&lt;/p&gt;
&lt;p&gt;Look, I’ve been around. I’ve seen trends come and go. I’ve seen data sync treated as a non-existent problem for two decades now. You’ve got XHR. You’ve got fetch. You’ve got REST and GraphQL. What else might you want?&lt;/p&gt;
&lt;p&gt;The problem is, all these tools are low-level. They solve the problem of getting data once. But getting data is a continuous process: data changes over time and becomes stale, requests fail, updates arrive later than you might’ve wanted, or out of order. Errors will happen. Occasional &lt;code&gt;if (!response.ok)&lt;/code&gt; will not get you very far.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;fetch(new Request(&amp;#39;/user/update&amp;#39;, { method: &amp;#39;POST&amp;#39; })).then((response) =&amp;gt; {
  if (!response.ok) {
    // Do what? Pretend it never happened?
    // Stop the entire application?
    // Retry? What if user already issued another update that invalidates this one?
    // What if update actually got through?
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you can’t just give up and declare everything invalid. You have to keep working. You need a system. &lt;em&gt;You can’t solve this problem at the level of single request.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It’s also ill-advised to try to solve data sync &lt;em&gt;while also working on a product&lt;/em&gt;. These problems require patience, thoroughness, and extensive testing. They can’t be rushed. And you already have a problem on your hands you don’t know how to solve: your product. Try solving both, fail at both &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Funny enough, edge cases aren’t that unique from project to project. Everyone wants their data synced. Everyone wants their data correct and delivered exactly once. Everyone wants it fast, compact, and in time. A perfect case for a library.&lt;/p&gt;
&lt;p&gt;Such a library would be called a database. But we’re used to thinking of a database as something server-related, a big box that runs in a data center. It doesn’t have to be like that! Databases have two parts: a place where data is stored and a place where data is delivered. That second part is usually missing.&lt;/p&gt;
&lt;p&gt;Think about it: we want two computers to talk and coordinate how to sync data. It’s obvious that both computers will need to run some code, and that code will need to be compatible. In short, we want to run a database on the frontend. It’s not enough to “just fetch data” over some simple JSON protocol or a generic JDBC driver. As data changes on both sides on completely independent timelines, you need to push, pull, coordinate, negotiate, validate, retry, guard against. Data sync is a complex problem, and the client needs to be as sophisticated as the backend. &lt;em&gt;They need to work together.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But once you do that, you’re free. You’ll get your data synced for you—more reliably and efficiently than you could ever do by hand. You’ll be able to work with your data as if it’s all local and forget about sync most of the time.&lt;/p&gt;
&lt;p&gt;In a perfect world, where everything is solved, what would programming look like? 99% business logic, 1% setup, right? Pure data and operations on data. People don’t want quarter-inch drill bits, they want quarter-inch holes. Paraphrasing that for programming: people don’t want databases. They want data.&lt;/p&gt;
&lt;p&gt;Well, that’s what sync engines are supposed to solve—pure, clean, functional business code, decoupled from the horrors of an unreliable network. The best time of my life was when I was working with local data and &lt;em&gt;something else&lt;/em&gt; synced it in the background.&lt;/p&gt;
&lt;p&gt;You’d get a database on your hands, too. It might sound controversial, but databases can be good at managing data. Queries are more concise, access is faster, and data is more organized. I’m a minimalist myself, but some things are simply better when queried from a (local) database. Would be faster, too.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for (id of ids) {
  const user = users[id];
  for (const post_id of user.post_ids) {
    const post = posts[post_id];
    for (const comment_id of post.comment_ids) {
      const comment = comments[comment_id];
      if (comment.author_id === id) {
        // there must be a better way...
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quick: what’s the data structure for when you want to query both posts by authors and authors by posts? Or: I’ve yet to see a code base that has maintained a separate in-memory index for data they are querying. Or does a hash join, for that matter. Usually it’s some form of four nested loops over an uncontrollable mix of maps and arrays. Not judging—I’ve been there—but there are tools that do it better and faster for you. Easier to read, too.&lt;/p&gt;
&lt;p&gt;Then there’s SQL. It’s the best, and it’s the worst. I took a break from it for a few years, and I completely forgot what crazy things it can do—but also how crazy some simple things are. Something as simple as&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const query = {
  goals: {
    todos: {},
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;turns into&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT g.*, gt.todos
FROM goals g
JOIN (
  SELECT g.id, json_agg(t.*) as todos
  FROM goals g
  LEFT JOIN todos t on g.id = t.goal_id
  GROUP BY 1
) gt on g.id = gt.id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;when queried through SQL.&lt;/p&gt;
&lt;p&gt;Of course, there’s legacy, there’s existing tooling, and there are all the teaching materials. It’s hard to replace SQL, and it’s twice as hard to beat it. All I’m saying is: if you don’t like databases because of SQL, I get you. Really. I understand. You are not alone.&lt;/p&gt;
&lt;p&gt;What I know for a fact is that you can get where you going without SQL. I worked with Datalog for a while, and did all the same things without ever touching SQL. I know it’s possible—I’ve seen it myself. There are other, equally powerful query languages that can get real work done with (possibly) better ergonomics. SQL is not the end of the road.&lt;/p&gt;
&lt;p&gt;So, what’s the significance of sync engines? I have a theory that every major technology shift happened when one part of the stack collapsed with another. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web apps collapsed cross-platform development. Instead of developing two or three versions of your app, you now develop one, available everywhere!&lt;/li&gt;
&lt;li&gt;Node.js collapsed client and server development. You get one language instead of two! You can share code between them!&lt;/li&gt;
&lt;li&gt;Docker collapsed the distinction between dev and prod.&lt;/li&gt;
&lt;li&gt;React collapsed HTML and JS, Tailwind collapsed JS and CSS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So where does that leave sync engines? They collapse the database and the server. If your database is smart enough and capable enough, why would you even need a server? Hosted database saves you from the horrors of hosting and lets your data flow freely to the frontend.&lt;/p&gt;
&lt;p&gt;I never thought this was possible in practice, but then Roam Research proved me wrong. For the first few years after public release, they didn’t have a single server. Everything was synced to and served from Firebase. Living the dream.&lt;/p&gt;
&lt;p&gt;That more or less covers it. We are building a sync engine because syncing data ad hoc, situationally is both hard and error-prone. We are also building it because we believe it simplifies the stack in a meaningful way. After all, we want our AI overlords to have a good time programming, too.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=43397640&quot;&gt;Discussion on HN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks Stepan Parunashvili, Joe Averbukh, and Kevin Lynagh for reviewing drafts of this essay.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-1&quot; href=&quot;#user-content-fnref-1&quot;&gt;[1]&lt;/a&gt;  Unless you have unlimited time and resources. Yes, Figma and Linear both built their sync engines while also building their product. Exceptions happen.&lt;/p&gt;
</description>
      <pubDate>Mon, 17 Mar 2025 00:00:00 GMT</pubDate>
      <author>Nikita Prokopov</author>
    </item>
    <item>
      <title>A Major Postgres Upgrade with Zero Downtime</title>
      <link>https://instantdb.com/essays/pg_upgrade</link>
      <guid isPermaLink="true">https://instantdb.com/essays/pg_upgrade</guid>
      <description>&lt;div class=&quot;text-lg font-medium&quot;&gt;
We’re Instant, a modern Firebase. You can spin up a database and make queries within a minute — no login required.
&lt;/div&gt;

&lt;p&gt;Right before Christmas we discovered that our Aurora Postgres instance needed a major version upgrade. We found a great essay by the &lt;a href=&quot;https://eng.lyft.com/postgres-aurora-db-major-version-upgrade-with-minimal-downtime-4e26178f07a0&quot;&gt;Lyft team&lt;/a&gt;, showing how they ran their upgrade with about 7 minutes of downtime.&lt;/p&gt;
&lt;p&gt;We started with Lyft’s checklist but made some changes, particularly with how we switched masters. &lt;strong&gt;In our process we got to 0 seconds of downtime.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Doing a major version upgrade is stressful, and reading other’s reports definitely helped us along the way. So we wanted to write an experience report of our own, in the hopes that it’s as useful to you as reading others were for us.&lt;/p&gt;
&lt;p&gt;In this write-up we’ll share the path we took — from false starts, to gotchas, to the steps that ultimately worked. Fair warning, our system runs at a modest scale. We have less than a terabyte of data, we read about 1.8 million tuples per second, and write about 500 tuples per second as of this writing. If you run at a much higher scale, this may be less relevant to you.&lt;/p&gt;
&lt;p&gt;With all that said, let’s get into the story!&lt;/p&gt;
&lt;h1&gt;State of Affairs&lt;/h1&gt;
&lt;p&gt;Let’s start with a brief outline of our system:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/the_system.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Browsers connect to sync servers. Sync servers keep track of active queries. Sync servers also listen to Postgres’ write-ahead log; they take transactions, find affected queries, and send novelty back to browsers. &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; Crucially, all Instant databases are hosted under one Aurora Postgres instance. &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Trouble Erupts&lt;/h2&gt;
&lt;p&gt;After our open source launch in August &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;, we experienced about a 100x increase in throughput. For the first 2 months, whenever we saw perf issues they usually lived in our Client SDK or the Sync Server. When we hit a new high in December though, our Aurora Postgres instance started to spike in CPU and stumble.&lt;/p&gt;
&lt;p&gt;To give us breathing room, we kept upgrading the size of the machine, until we reached db.r6g.16xlarge. &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt; We had to do something about the queries we were writing.&lt;/p&gt;
&lt;h2&gt;Sometimes, new is better than old&lt;/h2&gt;
&lt;p&gt;We started to reproduce slow queries locally and began to optimize them. Within the first hour we noticed something strange: one teammate constantly reported faster query results then the rest of us.&lt;/p&gt;
&lt;p&gt;Turns out this teammate was running Postgres 16, while most of us (and our production instance) were running Postgres 13.&lt;/p&gt;
&lt;p&gt;We did some more backtesting and realized that Postgres 16 improved many of the egregious queries by 30% or more. Not bad. There came our first learning: sometimes, just upgrading Postgres is a great way to improve perf. &lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So we thought, let’s upgrade to Postgres 16. Now how do we go about it?&lt;/p&gt;
&lt;h1&gt;False Starts&lt;/h1&gt;
&lt;p&gt;We were a team of 4 and we were in a crunch. If we could find a quick option we’d have been happy to take it. Here’s what we tried:&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;in-place-upgrade&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1) In-Place Upgrades...but they take 15 minutes&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/in_place.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The easiest choice would have been to run an in-place upgrade. Put the database in maintenance mode, upgrade major versions, then turn it back on again. In RDS console you can do this with a &lt;a href=&quot;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.PostgreSQL.MajorVersion.html#USER_UpgradeDBInstance.Upgrading.Manual:~:text=the%20RDS%20API.-,Console,-To%20upgrade%20the&quot;&gt;few button clicks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The big problem is the downtime. Your DB is in maintenance mode for the entirety of the upgrade. The Lyft team said an in-place upgrade would have caused them a &lt;a href=&quot;https://eng.lyft.com/postgres-aurora-db-major-version-upgrade-with-minimal-downtime-4e26178f07a0#4831&quot;&gt;30 minute&lt;/a&gt; outage.&lt;/p&gt;
&lt;p&gt;We wanted to test this for ourselves though, in case a smaller database upgraded more quickly. So we cloned our production database and tested an in-place upgrade. Even with our smaller size, it took about 15 minutes for the clone to come back online.&lt;/p&gt;
&lt;p&gt;Crunch or not, a 15-minute outage was off the table for us. Since launch we had folks sign up across the U.S, Europe and Asia; traffic ebbed and flowed, but there wasn’t a period where 15 minutes of downtime felt tolerable.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;blue-green-deployment&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2) Blue-Green Deployments...but you can’t have active replication slots&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/blue_green.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Well, Aurora Postgres also has &lt;a href=&quot;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/blue-green-deployments-overview.html&quot;&gt;blue-green deployments&lt;/a&gt;. AWS spins up an upgraded replica for you, and you can switch masters with a button click. They promise about a minute of downtime.&lt;/p&gt;
&lt;p&gt;With such little operational effort, a minute of downtime sounded like a great option for us.&lt;/p&gt;
&lt;p&gt;So we cloned our DB and tested a blue-green deployment. Yup, the connection came back in a minute! It looked like we were done. Until we tried a full rehearsal.&lt;/p&gt;
&lt;p&gt;We spun up a complete staging environment, this time with active sync servers and connected clients. Now the blue-green deployment would go on for 30 minutes, and then break with a configuration error:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Creation of blue/green deployment failed due to incompatible parameter settings. See &lt;a href=&quot;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/blue-green-deployments-creating.html#blue-green-deployments-creating-preparing-postgres&quot;&gt;link&lt;/a&gt; to help resolve the issues, then delete and recreate the blue/green deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The next few hours were frustrating: we would change a setting, start again, wait 30 minutes, and invariably end up with the same error.&lt;/p&gt;
&lt;p&gt;Once we exhausted the suggestions from this error message, we began a process of elimination: when did the upgrade work, and what change made it fail? Eliminating the sync servers revealed the issue: active replication slots.&lt;/p&gt;
&lt;p&gt;Remember how our sync servers listen to Postgres’ write-ahead log? To do this, we opened &lt;a href=&quot;https://www.postgresql.org/docs/current/logicaldecoding-explanation.html#LOGICALDECODING-REPLICATION-SLOTS&quot;&gt;replication slots&lt;/a&gt;. We couldn’t create a blue-green deployment when the master DB had active replication slots. The AWS docs did not mention this. &lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;At least this experience highlighted a learning: &lt;em&gt;always&lt;/em&gt; run a rehearsal that’s as close to production as possible, you never know what you’ll find.&lt;/p&gt;
&lt;p&gt;In order to stop using replication slots we’d have to disconnect our sync servers. But then we would lose reactivity, potentially for 30 minutes. Apps would appear broken if we queries were out of sync that long; blue-green deployments were off the table too.&lt;/p&gt;
&lt;h1&gt;A Plan for Going Manual&lt;/h1&gt;
&lt;p&gt;When the managed options don’t work, it’s time to go manual. We knew that a manual upgrade would have to involve three steps:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/going_manual.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;First, we would stand up a new replica running Postgres 16 — Let’s call this machine &amp;quot;16&amp;quot;. Once 16 was running, we could get our sync servers to subscribe to 16. The remaining step would be to switch writes &amp;quot;all in one go&amp;quot; (what this meant TBD) to 16. When that was done, migration done.&lt;/p&gt;
&lt;p&gt;Now to figure out the steps&lt;/p&gt;
&lt;h1&gt;1) Replicate to 16&lt;/h1&gt;
&lt;p&gt;The first problem was to create our replica running Postgres 16.&lt;/p&gt;
&lt;h2&gt;a) Clone-Upgrade-Replicate led to...lost data&lt;/h2&gt;
&lt;p&gt;Lyft had a great &lt;a href=&quot;https://eng.lyft.com/postgres-aurora-db-major-version-upgrade-with-minimal-downtime-4e26178f07a0#a7df&quot;&gt;series of steps&lt;/a&gt; to create a replica, so we tried to follow it. There were three stages:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/clone_upgrade_replicate.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;First, we clone our database, then we upgrade our clone, and then we start replication. By the end, our clone would have become a replica running Postgres 16.&lt;/p&gt;
&lt;p&gt;Steps 1 (clone) &amp;amp; 2 (upgrade) worked great. The trouble started with step 3 (replicate).&lt;/p&gt;
&lt;h3&gt;Lost PG functions&lt;/h3&gt;
&lt;p&gt;When we turned on replication, we saw this error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;:ERROR: function is_jsonb_valid_timestamp(jsonb) does not exist at character 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s weird. We &lt;em&gt;did&lt;/em&gt; have a custom Postgres function called &lt;code&gt;is_jsonb_valid_timestamp&lt;/code&gt;. And the function existed on both machines; if we logged in with PSQL, we could write queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select is_jsonb_valid_timestamp(&amp;#39;1724344362000&amp;#39;::jsonb);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt; is_jsonb_valid_timestamp
--------------------------
 t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We thought maybe there was an error with our WAL level, or maybe some input worked in 13, but stopped working in 16.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;search-paths&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Search paths&lt;/h3&gt;
&lt;p&gt;So we went down a rabbit hole investigating and searching in &lt;a href=&quot;https://www.postgresql.org/message-id/flat/D2B9F2A20670C84685EF7D183F2949E2373D64%40gigant.nidsa.net#8132cc2fa455dd1f1bb02c63cdd04678&quot;&gt;PG’s mailing list.&lt;/a&gt; Finally, we discovered the problem was &lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH&quot;&gt;search paths&lt;/a&gt;. &lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;show search_path;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;   search_path
-----------------
 &amp;quot;$user&amp;quot;, public
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Postgres stores custom functions in a &lt;a href=&quot;https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PUBLIC&quot;&gt;schema&lt;/a&gt;. When you write a function in your query, PG uses a &lt;code&gt;search_path&lt;/code&gt; to decide which schema to look into. During replication, Postgres was having trouble finding our function. To get around this issue, we &lt;a href=&quot;https://github.com/instantdb/instant/pull/593&quot;&gt;wrote a PR&lt;/a&gt; to add the &lt;code&gt;public&lt;/code&gt; prefix explicitly in all our function definitions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- Before:
create or replace function is_jsonb_valid_timestamp(value jsonb)
-- After:                   👇
create or replace function public.is_jsonb_valid_timestamp(value jsonb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note to us: make sure to use &lt;code&gt;public&lt;/code&gt; in all our function definitions. &lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;With PG functions working, 3) replicate ran smoothly! Or so we thought.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;missing-data&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Missing data&lt;/h3&gt;
&lt;p&gt;For all intents and purposes, our new clone looked like a functioning replica. But we wanted to absolutely make sure that we didn’t lose any data.&lt;/p&gt;
&lt;p&gt;Thankfully, we had a special &lt;code&gt;transactions&lt;/code&gt; table — it’s an immutable table we use internally &lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;instant=&amp;gt; \d transactions;

   Column   |            Type             | -- ...
------------+-----------------------------+
 id         | bigint                      |
 app_id     | uuid                        |
 created_at | timestamp without time zone |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we never modify rows, we could also use the &lt;code&gt;transactions&lt;/code&gt; table for quick sanity checks — was there any data lost in the table? Here’s the query we ran to do that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- On 13
select max(id) from transactions;
select count(*) from transactions where id &amp;lt; :max-id;

-- Wait for :max-id to replicate ...
-- On 16
select COUNT(*) from transactions where id &amp;lt; :max-id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To our surprise...we found 13 missing transactions! That definitely stumped us. We weren’t quite sure where the data loss came from &lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;b) Create, Replicate...worked great!&lt;/h2&gt;
&lt;p&gt;So we went back to the drawing board. One problem with our replica checklist was that it had about 13 steps in it. If we could remove the number of steps, perhaps we could kill whatever caused this data loss.&lt;/p&gt;
&lt;p&gt;So we cooked up an alternate approach:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/create_replicate.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Instead of creating, cloning, and then upgrading, we would start with a fresh database running Postgres 16, and replicate from scratch. Lyft chose to clone their DB, because they had over 30TB of data and could leverage &lt;a href=&quot;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Managing.Clone.html#Aurora.Clone.Overview&quot;&gt;Aurora Cloning&lt;/a&gt;. But we had less than a terabyte of data; starting replication from scratch wasn’t a big deal for us. &lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;So we created a checklist and ended up with 7 steps:&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;replica-checklist&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;border&quot;&gt;

&lt;h3 class=&quot;font-bold font-mono text-center bg-gray-100 p-2 mt-0&quot;&gt;Checklist: Create an upgraded Replica&lt;/h3&gt;

&lt;div class=&quot;mr-2&quot;&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;16: Create a new Postgres Aurora Database on Postgres 16.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Make sure to set &lt;code&gt;wal_level = logical&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;13: Extract the schema&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dump ${DATABASE_URL} --schema-only -f dump.schema.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;16: Import the schema into 16&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;psql ${NEW_DATABASE_URL} -f dump.schema.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;13: Create a publication&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create publication pub_all_table for all tables;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;16: Create a subscription with copy_data = true&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create subscription pub_from_scratch
connection &amp;#39;host=host_here dbname=name_here port=5432 user=user_here password=password_here&amp;#39;
publication pub_from_scratch
with (
  copy_data = true, create_slot = true, enabled = true,
  connect = true,
  slot_name = &amp;#39;pub_from_scratch&amp;#39;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm that there’s no data loss&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt; -- On 13
 select max(id) from transactions;
 select count(*) from transactions where id &amp;lt; :max-id;

 -- Wait for :max-id to replicate ...
 -- On 16
 select count(*) from transactions where id &amp;lt; :max-id;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;16: Run vacuum analyze&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt; vacuum (verbose, analyze, full);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;

&lt;/div&gt;

&lt;p&gt;We ran step 6 with bated breath...and it all turned out well! &lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt; Now we had a replica running Postgres 16.&lt;/p&gt;
&lt;h1&gt;2) Switching Subscriptions&lt;/h1&gt;
&lt;p&gt;Next step, to switch subscriptions. Let’s remind ourselves what we’re looking to do:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/switch_subs.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;We’d need to get our sync servers to create replication slots in 16, rather than 13.&lt;/p&gt;
&lt;p&gt;To do this, we added a &lt;code&gt;next-database-url&lt;/code&gt; variable to our sync servers. During startup, if &lt;code&gt;next-database-url&lt;/code&gt; was set, sync servers would subscribe from there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;;; invalidator.clj
;; `start` runs when the machine boots up
(defn start
  ([process-id]
    ; ...
    (wal/start-worker {:conn-config
                      (or (config/get-next-aurora-config)
                          ;; Use the next db so that we don&amp;#39;t
                          ;; have to worry about restarting the
                          ;; invalidator when failing over to a
                          ;; new db.
                          (config/get-aurora-config))})
    ; ...
    ))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we deployed this change, sync servers replicated from 16. Phew, this was at least one step in the story that didn’t feel nerve-wracking!&lt;/p&gt;
&lt;h1&gt;3) Switching Writes&lt;/h1&gt;
&lt;p&gt;Now to worry about writes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/switch_writes.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Ultimately, we needed to click some button and trigger a switch. To make the switch work, we’d need to follow two rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;16 must be caught up&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If there are &lt;em&gt;any&lt;/em&gt; writes in 13 that haven’t replicated to 16 yet, we can’t turn on writes to 16. Otherwise transactions would come in the wrong order&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Once caught up, all new writes must go to 16&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If &lt;em&gt;any&lt;/em&gt; write accidentally goes to 13, we could lose data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, how could we follow these rules?&lt;/p&gt;
&lt;h2&gt;We could stop the world...but that’s downtime&lt;/h2&gt;
&lt;p&gt;The simplest way to switch writes would have been to stop the world:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Turn off all writes.&lt;/li&gt;
&lt;li&gt;Wait for 16 to catch up&lt;/li&gt;
&lt;li&gt;Enable writes again — this time they all go to 16&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If we manually executed each step in ‘stop the world&amp;#39;, we’d have about a minute of downtime. We could write a function which did these steps for us, and get to only a few seconds of downtime. But we had already spent a day setting up our manual method, could we do better?&lt;/p&gt;
&lt;p&gt;Since we were switching manually we had finer control over our connections. We realized that with just a little bit more work...we could have no downtime at all!&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;zero-downtime-algo&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Or we could write an algorithm with zero downtime!&lt;/h2&gt;
&lt;p&gt;Our co-author Daniel shared an algorithm he used at his previous startup:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/pg_upgrade/no_downtime.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;First, we pause all new transactions. Then, we wait for active transactions to complete and for 16 to catch up. Finally we unpause all transactions and have them go to 16. If we did this right, we could switch major versions without any downtime at all!&lt;/p&gt;
&lt;h3&gt;The benefits of being small&lt;/h3&gt;
&lt;p&gt;Sounds good in theory, but it can be hard to pull off. Unless of course you run at a modest scale.&lt;/p&gt;
&lt;p&gt;Our switching algorithm hinges on being able to control all active connections. If you have tons of machines, how could you control all active connections?&lt;/p&gt;
&lt;p&gt;Well, since our throughput was still modest, we could temporarily scale our sync servers down to just one giant machine. Clojure and Java came handy here too. We had threads and the JVM is efficient, so we could take full advantage of the &lt;a href=&quot;https://instances.vantage.sh/aws/ec2/m6a.16xlarge?region=us-east-1&amp;os=linux&amp;cost_duration=monthly&amp;reserved_term=Standard.noUpfront&quot;&gt;m6a.16xlarge&lt;/a&gt; sync server we moved to for the switch.&lt;/p&gt;
&lt;h3&gt;Writing out a failover function&lt;/h3&gt;
&lt;p&gt;So we went forward and translated our zero-downtime algorithm into code. Here’s how it looked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;(defn do-failover-to-new-db []
  (let [prev-pool aurora/-conn-pool
        next-pool (start-new-pool next-config)
        next-pool-promise (promise)]

    ;; 1. Make new connections wait
    (alter-var-root #&amp;#39;aurora/conn-pool (fn [_] (fn [] @next-pool-promise)))

    ;; 2. Give existing transactions 2.5 seconds to complete.
    (Thread/sleep 2500)
    ;; Cancel the rest
    (sql/cancel-in-progress sql/default-statement-tracker)

    ;; 3. Wait for 16 to catch up
    (let [tx (transaction-model/create! aurora/-conn-pool
                                        {:app-id (config/instant-config-app-id)})]
      (loop [i 0]
        (if-let [row (sql/select-one next-pool
                                      [&amp;quot;select * from transactions where app_id = ?::uuid and id = ?::bigint&amp;quot;
                                      (config/instant-config-app-id) (:id tx)])]
          (println &amp;quot;we are caught up!&amp;quot;)
          ;; Still waiting...
          (do (Thread/sleep 50)
              (recur inc i)))))


    ;; 4 accept new connections!
    (deliver next-pool-promise next-pool)
    (alter-var-root #&amp;#39;aurora/-conn-pool (fn [_] next-pool))))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We spun up staging again, ran our failover function...buut transactions failed again. We were getting unique constraint violations on our transactions table.&lt;/p&gt;
&lt;h3&gt;Don’t forget sequences&lt;/h3&gt;
&lt;p&gt;This time the fix was easy to catch: sequences. Postgres does not &lt;a href=&quot;https://www.postgresql.org/docs/current/logical-replication-restrictions.html&quot;&gt;replicate sequence&lt;/a&gt; data. This meant that when a new &lt;code&gt;transaction&lt;/code&gt; row was created, we were using ids that already existed.&lt;/p&gt;
&lt;p&gt;To fix it, we incremented our sequences in the failover function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;-           (println &amp;quot;we are caught up!&amp;quot;)
+           (sql/execute! next-pool
+                         [&amp;quot;select setval(&amp;#39;transactions_id_seq&amp;#39;, ?::bigint, true)&amp;quot;
+                         (+ (:id row) 1000)])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This time we ran the failover function...and it worked great!&lt;/p&gt;
&lt;p&gt;If you’re curious, here’s how the actual failover &lt;a href=&quot;https://github.com/instantdb/instant/blob/main/server/src/instant/jdbc/failover.clj#L25-L87&quot;&gt;function&lt;/a&gt; looked for production.&lt;/p&gt;
&lt;h3&gt;Running in Prod&lt;/h3&gt;
&lt;p&gt;Now that we had a good practice run, we got ourselves ready, had our sparkling waters in hand, and began to run our steps in production.&lt;/p&gt;
&lt;p&gt;After about a 3.5 second pause &lt;sup id=&quot;user-content-fnref-13&quot;&gt;&lt;a href=&quot;#user-content-fn-13&quot;&gt;[13]&lt;/a&gt;&lt;/sup&gt;, the failover function completed smoothly! We had a new Postgres instance serving requests, and best of all, nobody noticed. &lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3&gt;Future Improvements&lt;/h3&gt;
&lt;p&gt;Our &lt;code&gt;do-failover-to-new-db&lt;/code&gt; worked at our scale, but will probably fail us in a few months. There are two improvements we plan to make:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We paused &lt;em&gt;both&lt;/em&gt; writes and reads. But technically we don’t need to pause reads. Daniel pushed &lt;a href=&quot;https://github.com/instantdb/instant/pull/743&quot;&gt;up a PR&lt;/a&gt; to be explicit about read-only connections. In the future we can skip pausing them.&lt;/li&gt;
&lt;li&gt;In December we were able to scale down to one big machine. We’re approaching the limits to one big machine today. &lt;sup id=&quot;user-content-fnref-15&quot;&gt;&lt;a href=&quot;#user-content-fn-15&quot;&gt;[15]&lt;/a&gt;&lt;/sup&gt; We’re going to try to evolve this into a kind of &lt;code&gt;two-phase-commit&lt;/code&gt;, where each machine reports their stage, and a coordinator progresses when all machines hit the same stage.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Fin&lt;/h1&gt;
&lt;p&gt;Aand that’s our story of how did our major version upgrade. We wanted to finish up with a summary of learnings, in the hopes that’s easier for you to get back to this essay when you’re considering an upgrade. Here’s what we wish we knew when we started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sometimes, newer Postgres versions improve perf. Make sure to check this if you face perf issues.&lt;/li&gt;
&lt;li&gt;If you need to upgrade&lt;ol&gt;
&lt;li&gt;Pick a buddy if you can, it’s a lot more fun (and less nerve-racking) to do this with a partner.&lt;/li&gt;
&lt;li&gt;Before you do anything in production, do a full rehearsal. Use a staging environment that mimics production as closely as possible&lt;/li&gt;
&lt;li&gt;If you are okay with 15 minutes of downtime, do an &lt;a href=&quot;#in-place-upgrade&quot;&gt;in-place upgrade&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;If you don’t have active replication slots and are okay with a minute of downtime, try a &lt;a href=&quot;#blue-green-deployment&quot;&gt;blue-green deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;When you need to do a manual upgrade:&lt;ol&gt;
&lt;li&gt;If you can, skip cloning and create a replica from scratch. There are only &lt;a href=&quot;#replica-checklist&quot;&gt;7 steps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;If you wrote custom pg functions, make sure to check your &lt;a href=&quot;#search-paths&quot;&gt;search_path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Do some sanity checks to make sure you don’t &lt;a href=&quot;#missing-data&quot;&gt;lose data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;If you can get writes down to one machine, try our &lt;a href=&quot;#zero-downtime-algo&quot;&gt;algorithm for zero downtime&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hopefully, this was a fun read for you :)&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=42867657&quot;&gt;Dicussion on HN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to Nikita Prokopov, Joe Averbukh, Martin Raison, Irakli Safareli, Ian Sinnott for reviewing drafts of this essay&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: Our sync strategy was inspired by Figma’s LiveGraph and Asana’s Luna. The LiveGraph team wrote a &lt;a href=&quot;https://www.figma.com/blog/livegraph-real-time-data-fetching-at-figma/&quot;&gt;great essay&lt;/a&gt; that explains the sync strategy. You can read our original &lt;a href=&quot;https://www.instantdb.com/essays/next_firebase&quot;&gt;design essay&lt;/a&gt; to learn more about Instant&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-2&quot; href=&quot;#user-content-fnref-2&quot;&gt;[2]&lt;/a&gt;  You may be wondering: how do we host multiple &amp;quot;Instant databases&amp;quot;, under one &amp;quot;Aurora database&amp;quot;? The short answer is that we wrote a query engine on top of Postgres. This lets us create a multi-tenant system where we can &amp;quot;spin up&amp;quot; dbs on demand. I hope to share more about this in a separate essay.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;: All of the code (including this blog) is open sourced &lt;a href=&quot;https://github.com/instantdb/instant&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://instances.vantage.sh/aws/rds/db.r6g.16xlarge&quot;&gt;db.r6g.16xlarge&lt;/a&gt; would cost us north of 6K per month. That was out of the question for the kind of traffic we were handling.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-5&quot; href=&quot;#user-content-fnref-5&quot;&gt;[5]&lt;/a&gt;  In case you were wondering, we also looked to optimize the queries. After we upgraded (took about a day and a half), we added a partial index that improved perf another 50% or so.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;: We did see a note about replication in &lt;a href=&quot;https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/blue-green-deployments-switching.html#blue-green-deployments-switching-guardrails&quot;&gt;&amp;quot;Switchover Guardrails&amp;quot;&lt;/a&gt;, but this note is about the second step: after 1) creating a green deployment, we 2) run the switch.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-7&quot; href=&quot;#user-content-fnref-7&quot;&gt;[7]&lt;/a&gt;  The key to discovering this issue was our co-author Daniel’s sleuthing. He planned test upgrades locally: going from 13 → 14 → 15 → 16, to see where things broke. When Daniel tried 13 → 14, it failed. To sanity check things, he then tried a migration from 13 → 13…and that failed too! From there we knew something had to be up with our process.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-8&quot; href=&quot;#user-content-fnref-8&quot;&gt;[8]&lt;/a&gt;  An alternative would have been to enhance the dump file with the search path. We like the idea of being more explicit in our definitions though; especially if we can find a good linter.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-9&quot; href=&quot;#user-content-fnref-9&quot;&gt;[9]&lt;/a&gt;  Why do we have it? We use the transaction’s id column for record-keeping inside sync servers.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;: If you are curious, you can look at a slice of the checklist we used &lt;a href=&quot;https://gist.github.com/stopachka/f05d3682223e206ed6465cafe3ec9f2a&quot;&gt;here&lt;/a&gt;. If you have a hunch where the data loss could have come from, let us know&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-11&quot; href=&quot;#user-content-fnref-11&quot;&gt;[11]&lt;/a&gt;  Though even with 30TB, it would only take a week to transfer at a modest 50 mb/second.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;: You may be wondering — sure, the transactions table was okay, but what if there was data loss in other tables? We wrote a &lt;a href=&quot;https://github.com/instantdb/instant/blob/main/server/src/instant/jdbc/failover.clj#L258&quot;&gt;more involved script&lt;/a&gt; to check for every table too. We really wanted to make sure there was no data loss.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-13&quot; href=&quot;#user-content-fnref-13&quot;&gt;[13]&lt;/a&gt;  About 2.5 seconds to let active queries complete, and about 1 second for the replica to catch up&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-14&quot; href=&quot;#user-content-fnref-14&quot;&gt;[14]&lt;/a&gt;  You may be wondering, how did we run the function? Where’s the feature flag? That’s one more Clojure win: we could SSH into production, and execute this function in our REPL!&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-15&quot; href=&quot;#user-content-fnref-15&quot;&gt;[15]&lt;/a&gt;  The big bottleneck is all the active websocket connections on one machine — it slows down the sync engine too much. If we improve perf, perhaps we can get to one big machine again!&lt;/p&gt;
</description>
      <pubDate>Wed, 29 Jan 2025 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili, Daniel Woelfel</author>
    </item>
    <item>
      <title>Video: Building a Sync Engine in Clojure</title>
      <link>https://instantdb.com/essays/conj</link>
      <guid isPermaLink="true">https://instantdb.com/essays/conj</guid>
      <description>&lt;p&gt;&lt;div class=&quot;md-video-container&quot;&gt;
  &lt;iframe
    width=&quot;100%&quot;
    src=&quot;https://www.youtube.com/embed/6FikTQf8qho?rel=0&amp;modestbranding=1&amp;playsinline=1&amp;autoplay=0&amp;cc_load_policy=1&quot;
    title=&quot;Building a Sync Engine in Clojure&quot;
    allow=&quot;autoplay; picture-in-picture&quot;
    allowfullscreen
  &gt;&lt;/iframe&gt;
&lt;/div&gt;&lt;/p&gt;
&lt;p&gt;Following up from &lt;a href=&quot;https://www.instantdb.com/essays/next_firebase&quot;&gt;A Graph-Based
Firebase&lt;/a&gt;, Stopa (CTO of
Instant), gave a talk about building Instant at Clojure Conj 2024! In this talk
he discusses the common schleps developers face when building apps, and how
Instant compresses them.&lt;/p&gt;
&lt;p&gt;Give this a watch if you’re interested in learning more about how Instant works under the hood!&lt;/p&gt;
</description>
      <pubDate>Thu, 24 Oct 2024 00:00:00 GMT</pubDate>
      <author>Joe Averbukh</author>
    </item>
    <item>
      <title>Instant raises $3.4M seed to build a modern Firebase</title>
      <link>https://instantdb.com/essays/seed</link>
      <guid isPermaLink="true">https://instantdb.com/essays/seed</guid>
      <description>&lt;p&gt;&lt;img src=&quot;/img/essays/seed_header.png&quot; alt=&quot;Instant raises $3.4M seed to build a modern Firebase&quot;&gt;&lt;/p&gt;
&lt;p&gt;One month ago we open sourced Instant and had one of the &lt;a href=&quot;https://news.ycombinator.com/item?id=41322281&quot;&gt;largest Show HN’s&lt;/a&gt; for a YC company. Today we’re &lt;a href=&quot;https://techcrunch.com/2024/10/02/instant-harkens-back-to-a-pre-google-firebase/&quot;&gt;announcing our $3.4M seed&lt;/a&gt;. We’re backed by YCombinator, SV Angel, and a number of technical angels, including James Tamplin, the original CEO of Firebase, Paul Graham, Co-founder of YCombinator, Greg Brockman, Co-founder of OpenAI, and Jeff Dean, chief scientist of Google DeepMind.&lt;/p&gt;
&lt;h2&gt;What is Instant?&lt;/h2&gt;
&lt;p&gt;In two sentences: Instant is a modern Firebase. We make you productive by giving your frontend a real-time database.&lt;/p&gt;
&lt;p&gt;What does that actually mean?&lt;/p&gt;
&lt;p&gt;Imagine you’re a hacker who loves building apps. You have an exciting idea, and are ready to &lt;strong&gt;make something people want.&lt;/strong&gt; You want to build an MVP fast, that doesn’t completely suck. So how do you do it?&lt;/p&gt;
&lt;p&gt;Most of the time we make a three-tier architecture with client, server, and a database. On the server side we write endpoints to glue our frontend with our database. We might use an ORM to make it easier to work with our db, and add a cache to serve requests faster. On the client we need to reify json from the server and paint a screen. We add stores to manage state, and write mutations to handle updates. This is just for basic functionality.&lt;/p&gt;
&lt;p&gt;If we want our UIs to feel fast, we write optimistic updates so we don’t need to wait for the server. If we want live updates without refreshing we either poll or add websockets. And if we want to support offline mode, we need to integrate IndexedDB and pending transaction queues.&lt;/p&gt;
&lt;p&gt;That’s a lot of work!&lt;/p&gt;
&lt;p&gt;To make things worse, whenever we add a new feature, we go through the same song and dance over and over again: add models to our DB, write endpoints on our server, create stores in our frontend, write mutations, optimistic updates, etc.&lt;/p&gt;
&lt;p&gt;Could it be better? We think so!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.instantdb.com/readmes/compression.svg&quot; alt=&quot;Instant compresses the schleps!&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you had a database on the client, you wouldn’t need to manage stores, selectors, endpoints, caches, etc. You could just write queries to fetch the data you want. If these queries were reactive, you wouldn’t have to write extra logic to re-fetch whenever new data appears. Similarly you could just make transactions to apply mutations. These transactions could apply changes optimistically and be persisted locally. Putting this all together, you can build delightful applications without the normal schleps.&lt;/p&gt;
&lt;p&gt;So we built Instant. Instant gives you a database you can use in the client, so you can focus on what’s important: &lt;strong&gt;building a great UX for your users, and doing it quickly&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;How is Instant different from Firebase or Supabase?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/comparison_matrix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You may be wondering, what makes Instant so modern compared to Firebase, and how is it different from Supabase?&lt;/p&gt;
&lt;p&gt;Both Firebase and Supabase provide a database on the client as well. Firebase comes with realtime, optimistic updates, and offline mode, but does not support relations. Supabase is relational at it’s core, but optimistic updates and offline mode need to be hand-rolled for every feature. If you could have Firebase with relations, you’d have an infrastructure capable of building some of the best apps today like Figma, Notion, or Linear.&lt;/p&gt;
&lt;p&gt;Our architecture is inspired by &lt;a href=&quot;https://www.figma.com/blog/livegraph-real-time-data-fetching-at-figma/&quot;&gt;Figma’s LiveGraph&lt;/a&gt; and &lt;a href=&quot;https://blog.asana.com/2020/09/worldstore-distributed-caching-reactivity-part-2/&quot;&gt;Asana’s LunaDB&lt;/a&gt;. We also built Instant to be multi-tenant and don’t need to spin up an actual database for users. This enables us to give users a database in &amp;lt;10ms with a click of a button. And unlike our competitors, we can offer a free tier to users where their projects are never paused and there is no limit to the number of active projects they can have.&lt;/p&gt;
&lt;p&gt;To learn more about how Instant works under the hood, check out our essay &lt;a href=&quot;https://www.instantdb.com/essays/next_firebase&quot;&gt;A Graph-Based Firebase&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Who is Instant?&lt;/h2&gt;
&lt;p&gt;We’re &lt;a href=&quot;https://linkedin.com/in/joeaverbukh&quot;&gt;Joe&lt;/a&gt; and &lt;a href=&quot;https://x.com/stopachka&quot;&gt;Stopa&lt;/a&gt;, engineers, best friends, and co-founders. We first met in San Francisco in 2014 and worked together as senior and staff engineers at Facebook and Airbnb.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/joe_stopa.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When we worked at Facebook, most designers used Sketch. At that time no one thought there could be something better. Figma came out and changed the game. Similarly, in the 2010s, Evernote was one of the best note taking apps. In 2024 most people use Notion instead.&lt;/p&gt;
&lt;p&gt;Features like multiplayer, optimistic updates, and offline mode are what differentiate the best apps. As app users grow accustomed to instant experiences, reactivity will become table stakes for modern applications. Today delivering these features is difficult and requires a bespoke solution from a team of engineers at top tech companies. In the future, there will be infrastructure that all developers use to get these features for free.&lt;/p&gt;
&lt;p&gt;That’s what we’re building with Instant, a platform to build applications of the future.&lt;/p&gt;
&lt;h2&gt;Instant is growing&lt;/h2&gt;
&lt;p&gt;After being heads down for two years, Instant &lt;a href=&quot;https://github.com/instantdb/instant&quot;&gt;open sourced&lt;/a&gt; at the end of August 2024. On the same day we announced on Hacker News, amassed over 1k points, and &lt;a href=&quot;https://hnrankings.info/41322281/&quot;&gt;hit #1 for several hours&lt;/a&gt;. It’s been a whirlwind since.&lt;/p&gt;
&lt;p&gt;We’re getting a new office in San Francisco and looking for founding engineers to grow Instant. If you want to be part of a small team solving some of the hardest problems in web development &lt;a href=&quot;/hiring&quot;&gt;check out our hiring page!&lt;/a&gt;&lt;/p&gt;
</description>
      <pubDate>Tue, 01 Oct 2024 00:00:00 GMT</pubDate>
      <author>Joe Averbukh</author>
    </item>
    <item>
      <title>A Graph-Based Firebase</title>
      <link>https://instantdb.com/essays/next_firebase</link>
      <guid isPermaLink="true">https://instantdb.com/essays/next_firebase</guid>
      <description>&lt;p&gt;In &lt;a href=&quot;/essays/db_browser&quot;&gt;A Database in the Browser&lt;/a&gt;, I wrote that the schleps we face as UI engineers are actually database problems in disguise &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;. This begged the question: would a database-looking solution solve them?&lt;/p&gt;
&lt;p&gt;My co-founder Joe and I decided to build one and find out. This became &lt;a href=&quot;https://instantdb.com/&quot;&gt;Instant&lt;/a&gt;. I’d describe it as a graph-based successor to Firebase.&lt;/p&gt;
&lt;p&gt;You have relational queries, auth, and permissions. Optimistic updates come out of the box, and everything is reactive. It&amp;#39;s an architecture you can use today.&lt;/p&gt;
&lt;p&gt;Working on Instant has felt like an evolutionary process. We picked constraints and followed the path that unfolded. This led us to places we would never have predicted. For example, we started with SQL but ended up with a triple store and a query language that transpiles to Datalog.&lt;/p&gt;
&lt;p&gt;What were these constraints? Why triple store? What query language? In this essay, I’ll walk you through the design journey — from problems to solve, to choices made, to what’s next.&lt;/p&gt;
&lt;p&gt;I hope by the end, you’re as excited as I am about what this could mean for building apps and the people who use them.&lt;/p&gt;
&lt;h1&gt;Delightful Apps&lt;/h1&gt;
&lt;p&gt;Our journey starts by looking at what exists today. Think about the most &lt;em&gt;delightful&lt;/em&gt; apps you’ve tried. What comes to mind? To me, it’s apps like Figma, Linear, and Notion. And if you asked why, I’d say three reasons: Optimistic Updates, Multiplayer, and Offline-Mode.&lt;/p&gt;
&lt;h2&gt;Optimistic Updates&lt;/h2&gt;
&lt;p&gt;Once you’re in the flow of Figma or Notion, you rarely see a loading screen. This is because every change you make is applied instantly. It’s painful to do this well. You need a method for applying changes on the client and server. You need a queue to maintain order. You need undo. And the edge cases get daunting: if you have multiple changes waiting and the first one fails, what should happen? You need some way to cancel the dependents &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Challenging to build but transformative once done. Interaction time changes how you use an application. Get fast enough, and your fingertips become your primary constraint. I think this is the key to unlocking flow. &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Multiplayer&lt;/h2&gt;
&lt;p&gt;Speed itself is delightful, but it’s taken further with multiplayer. Every feature in Linear is collaborative by default. Assigned a task? All active sessions see your change. &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;There’s a pattern to multiplayer too. Developers think it’s a nice-to-have. But then some company builds it, and we’re stunned by the result. Figma did this for Sketch, and Notion did this for Evernote.&lt;/p&gt;
&lt;p&gt;But most apps aren’t multiplayer. This isn’t because we’ve hit a sweet spot of text editors, task managers, and design tools. Multiplayer is just too hard to build. &lt;sup id=&quot;user-content-fnref-5&quot;&gt;&lt;a href=&quot;#user-content-fn-5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Offline-Mode&lt;/h2&gt;
&lt;p&gt;Finally, delightful apps work offline. Some not &lt;em&gt;completely&lt;/em&gt; offline, but they all handle spotty connections.&lt;/p&gt;
&lt;p&gt;And offline-mode has the same pattern as multiplayer. It feels like a nice-to-have, but build it and you leap past your competitors. Why? Two reasons:&lt;/p&gt;
&lt;p&gt;First, though internet connectivity is abundant, there’s a tail end. The subway, the airplane, the spotty cafe. Seems minor, but eliminating the tail-end can be transformative. When we know that an app will work no &lt;em&gt;matter what,&lt;/em&gt; we use it differently. &lt;sup id=&quot;user-content-fnref-6&quot;&gt;&lt;a href=&quot;#user-content-fn-6&quot;&gt;[6]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Second, your app becomes &lt;em&gt;even faster&lt;/em&gt;. Offline-mode amortizes read latency. For example, the first time you load Linear, it may take time to fetch everything. But then, subsequent loads feel instant; you’ll just see offline data first. &lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h1&gt;Applications from The Future&lt;/h1&gt;
&lt;p&gt;Combine these features, and you get an application available everywhere, as fast as your fingertips, and multiplayer by default.&lt;/p&gt;
&lt;p&gt;Compared to the average web app, this is a difference in kind. Linear is so fast that you fall into flow states closing tasks. No one would say this about Jira. Notion’s offline-mode lets you store every note there. People don’t do this in Dropbox Paper. In Figma, two designers can collaborate on the same file. This was unheard of in the days of Sketch.&lt;/p&gt;
&lt;p&gt;These applications let you work in new ways. They become tools that you can master. And I think this is how most apps will be in the future. We prefer the experience, and the Notions of the world teach us to expect it.&lt;/p&gt;
&lt;p&gt;As an industry, we’ll need to find new abstractions that make building apps like this easy. I think it’s worth the effort to find them now.&lt;/p&gt;
&lt;h1&gt;Bespoke Solutions&lt;/h1&gt;
&lt;p&gt;So let’s try to discover this abstraction. What works today? Linear and Notion exist; how do they do it?&lt;/p&gt;
&lt;p&gt;Thankfully there’s lots &lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt; of &lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt; interesting &lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt; work &lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt; that explains their architecture. Here’s a simplified view:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/architecture.png&quot; alt=&quot;The architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let’s go bottom-up:&lt;/p&gt;
&lt;h2&gt;A. DB&lt;/h2&gt;
&lt;p&gt;On the backend we start with a database. Users want a live view of some subset of data. We can keep live views by either polling the database or leveraging a write-ahead log. &lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;B. Permissions&lt;/h2&gt;
&lt;p&gt;The DB gives us a set of results, but we can’t just send this data up to users. We need to filter for what they are allowed to see.&lt;/p&gt;
&lt;p&gt;So we build a permission layer. This starts simple. But as an app gets complex, permissions resemble their own language. Facebook had the best design I’ve seen. Here’s how it looked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function IDenyIfArchived(_user, task) {
  if (task.isArchived) {
    return deny();
  }
  return allow();
}
// ...
{
  &amp;quot;task&amp;quot;: {
    read: [
      IAllowIfTeamUser,
    ],
    write: [
      IDenyIfArchived,
      IAllowIfTeamUser,
    ],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Developers write a set of IAllow or IDeny rules per model. Since all reads and writes go through this layer, engineers can be sure that their queries are safe. &lt;sup id=&quot;user-content-fnref-13&quot;&gt;&lt;a href=&quot;#user-content-fn-13&quot;&gt;[13]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;C. Sockets&lt;/h2&gt;
&lt;p&gt;Now we reach the websocket layer. Clients subscribe to different topics. For Notion, it could be “documents and comments.” Or for Linear it could be “team, task, and users.”&lt;/p&gt;
&lt;p&gt;Backend developers hand-craft live queries to satisfy these topics. There’s a balancing act to play here. The more complicated the query, the harder it is to keep a live view. &lt;sup id=&quot;user-content-fnref-14&quot;&gt;&lt;a href=&quot;#user-content-fn-14&quot;&gt;[14]&lt;/a&gt;&lt;/sup&gt; So we need to simplify queries as much as possible. Most often, this means we skip pagination and overfetch. &lt;sup id=&quot;user-content-fnref-15&quot;&gt;&lt;a href=&quot;#user-content-fn-15&quot;&gt;[15]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;D. In-Memory Store&lt;/h2&gt;
&lt;p&gt;Now we move to the frontend. Sockets funnel all this data into an in-memory store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const Store = {
  teams: {
    teamIdA: {...}
  },
  users: {
    userIdA: {...}
  },
  tasks: {
    taskIdA: {..., teamId: &amp;quot;teamIdA&amp;quot;, ownerId: [&amp;quot;userIdA&amp;quot;]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do this so all screens have consistent information. For example, if a user changes their profile picture, we should see updates everywhere. The best way to do that is to keep data normalized and in one place.&lt;/p&gt;
&lt;h2&gt;E. IndexedDB&lt;/h2&gt;
&lt;p&gt;But we need our app to work offline too. So we back our store with durable storage. For web this is IndexedDB. When our app loads, we hydrate the store with what was saved before. This is what enables offline-mode and amortizes read latency.&lt;/p&gt;
&lt;h2&gt;F. Screens&lt;/h2&gt;
&lt;p&gt;Okay, time to paint screens. Right now we have a store with normalized data. But normalized data isn’t directly useful for rendering. What a screen wants is a graph. Say we show a “team tasks” page in Linear; we’d want team info, all the tasks for the team, and the owner for the task:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/screens_want_graphs.png&quot; alt=&quot;Screens want graphs&quot;&gt;&lt;/p&gt;
&lt;p&gt;We can build this with a javascript function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function dataForTaskPage(store, teamId) {
  return {
    ...store.teams[teamId],
    tasks: store.tasksForTeam(teamId).map((task) =&amp;gt; {
      return { ...task, owner: store.users[task.ownerId] };
    }),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this causes too many re-renders, we can memoize it or use some kind of dirty-checking. With that, we have a page a user can interact with.&lt;/p&gt;
&lt;h2&gt;G. Mutations&lt;/h2&gt;
&lt;p&gt;Then users make changes. We want those changes to feel instant, so we support optimistic updates. This is how it usually looks:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/mutation_system.png&quot; alt=&quot;Mutation system&quot;&gt;&lt;/p&gt;
&lt;p&gt;Whatever mutation we make, our local store and server need to understand them. This way we can apply changes immediately.&lt;/p&gt;
&lt;p&gt;To do this well, we need to support undo. We need to maintain order, and we need to be able to cancel dependent mutations. Hard stuff, but Linear, Figma, and Notion all go through the schlep.&lt;/p&gt;
&lt;p&gt;Once this is done, we’ve got an application from the future on our hands.&lt;/p&gt;
&lt;h1&gt;What Exists&lt;/h1&gt;
&lt;p&gt;Oof. Lots of custom work. Could these apps have used an existing tool instead?&lt;/p&gt;
&lt;h2&gt;Firebase&lt;/h2&gt;
&lt;p&gt;Firebase comes closest. It has optimistic updates out of the box. It supports offline mode and is reactive by default. But, I think Firebase has two dealbreakers: relations and permissions.&lt;/p&gt;
&lt;h3&gt;Relations&lt;/h3&gt;
&lt;p&gt;The biggest dealbreaker is Firebase’s query strength. You’re limited to document lookups. When Firebase was built, this was a great tradeoff to make. It’s simpler to support optimistic updates and offline mode for document stores. But for sophisticated apps, you &lt;em&gt;need&lt;/em&gt; relations.&lt;/p&gt;
&lt;p&gt;Figma, Notion, and Linear all have relations. Notion has a recursive model where blocks reference other blocks. Linear has users, tasks, and teams. Figma has documents, objects and properties.&lt;/p&gt;
&lt;p&gt;If you need relations, document stores explode in complexity. You end up having to implement your own joins with hand-tuned caches. Another schlep.&lt;/p&gt;
&lt;h3&gt;Permissions&lt;/h3&gt;
&lt;p&gt;The second dealbreaker is Firebase’s permission system. &lt;sup id=&quot;user-content-fnref-16&quot;&gt;&lt;a href=&quot;#user-content-fn-16&quot;&gt;[16]&lt;/a&gt;&lt;/sup&gt; Firebase Realtime has a language that looks like a long boolean expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;auth != null &amp;amp;&amp;amp; (!data.exists() || data.child(&amp;#39;users&amp;#39;).hasChild(auth.id));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gets unmaintainable fast &lt;sup id=&quot;user-content-fnref-17&quot;&gt;&lt;a href=&quot;#user-content-fn-17&quot;&gt;[17]&lt;/a&gt;&lt;/sup&gt;. It improved in Firestore — there’s now a function-like abstraction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function isAuthorOrAdmin(userId, article) {
  let isAuthor = article.author == userId;
  let isAdmin = exists(/databases/$(database)/documents/admins/$(userId));
  return isAuthor || isAdmin;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But again, this wasn’t built for complex use cases. There’s no way to write an early return statement for example. If we’re aiming for Linear, Figma, or Notion, we need a system that can scale to complex rules.&lt;/p&gt;
&lt;h2&gt;Supabase, Hasura&lt;/h2&gt;
&lt;p&gt;So Firebase won’t work. What about Supabase or Hasura?&lt;/p&gt;
&lt;p&gt;They solve Firebase’s greatest dealbreaker: relations. Both Supabase and Hasura support relations.&lt;/p&gt;
&lt;p&gt;But they do this at the expense of a local abstraction. Neither support offline-mode or optimistic updates. Multiplayer is still crude. You write basic subscriptions and manage the client yourself.&lt;/p&gt;
&lt;p&gt;Supabase and Hasura also don’t have a powerful permission system. They use Postgres’s Row-Level Security. Permissions are written as policies. But this won’t work for sophisticated apps. You’ll need to write so many policies, that it’ll be impossible to reason about. It’ll get slow too — the planner will struggle with them.&lt;/p&gt;
&lt;h1&gt;The Missing Column&lt;/h1&gt;
&lt;p&gt;So Firebase has a great local abstraction, but no support for relations. Supabase and Hasura support relations, but have a poor local abstraction. Put this in a table and you have an interesting column to think about:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/comparison_matrix.png&quot; alt=&quot;Matrix&quot;&gt;&lt;/p&gt;
&lt;p&gt;What if a tool could support relations and a local abstraction? You could write any query that a Figma, Linear, or Notion would need. And you could handle all of the hard work they do locally: optimistic updates, multiplayer, and offline-mode.&lt;/p&gt;
&lt;p&gt;Add support for complex permissions, and you have a tool to build applications from the future!&lt;/p&gt;
&lt;h1&gt;Inspiration&lt;/h1&gt;
&lt;p&gt;A daunting column to satisfy. But again, if we look at how Figma, Linear, and Notion work, we find clues. Squint, and their architecture looks like a database!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/generalization.png&quot; alt=&quot;Generalization&quot;&gt;&lt;/p&gt;
&lt;p&gt;Again, screens need consistent data. Previously, we wrote functions and got data from the store. Remember &lt;code&gt;dataForTasksPage&lt;/code&gt;?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function dataForTaskPage(store, teamId) {
  return {
    ...store.teams[teamId],
    tasks: store.tasksForTeam(teamId).map((task) =&amp;gt; {
      return { ...task, owner: store.users[task.ownerId] };
    }),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well, this is just a query! If we had a local database — let’s call it Local DB — that understood some GraphQL-looking language, we could instead declare:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;teams {
  ...
  tasks: {
    ...
    owner: {
      ...
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And voila, we’d have data for our screens.&lt;/p&gt;
&lt;p&gt;Next, we backed our data into IndexedDB. Well, databases are good at caching. Our Local DB could back itself up in IndexedDB!&lt;/p&gt;
&lt;p&gt;And the mutation system? If our Local DB and Backend DB spoke the same language, both could understand and apply the same mutations. Local DB can handle undo/redo, and with that we have optimistic updates out of the box.&lt;/p&gt;
&lt;p&gt;What about sockets? Databases handle replication. So what if we made the client a special node? The Local DB already knows the queries to satisfy. So it can talk to the backend and get the data it needs.&lt;/p&gt;
&lt;p&gt;On the backend, what if we had the same kind of permission system that Facebook had? We’d have a fully expressive language that could scale to complex rules.&lt;/p&gt;
&lt;p&gt;Make the Backend DB handle live queries, and we have all the pieces for our missing column!&lt;/p&gt;
&lt;h1&gt;Local DB&lt;/h1&gt;
&lt;p&gt;Let’s dive into our Local DB first. This is what’s going to handle queries, caching, and talking to our server. If we do this right, we inform everything else.&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;p&gt;The minimum our Local DB needs is support for relations. Whatever we do, we should be able to express “Give me team info, related tasks, and the owner for each task”.&lt;/p&gt;
&lt;p&gt;We should also support recursive queries. For Notion, we need to say “Give me a block and expand all children recursively”.&lt;/p&gt;
&lt;p&gt;Our Local DB should also be easy to use. Firebase is famous for this. You can start working with a single index.html file. API calls are consistent and simple. You don’t need to specify a schema to get started. We should be just as easy to use. &lt;sup id=&quot;user-content-fnref-18&quot;&gt;&lt;a href=&quot;#user-content-fn-18&quot;&gt;[18]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;And our Local DB should be light. At least on the client. Yes we can cache the download. But I don’t think developers will take you up on an offer that doubles their bundle.&lt;/p&gt;
&lt;p&gt;Finally, our Local DB should be simple. Every feature in our Local DB needs to be supported by our multiplayer backend. This won’t ship if our spec is too large.&lt;/p&gt;
&lt;h1&gt;Exploring SQL&lt;/h1&gt;
&lt;p&gt;A SQL-based tool is closest at hand. I enjoyed looking at &lt;a href=&quot;https://github.com/jlongster/absurd-sql&quot;&gt;absurd-sql&lt;/a&gt;. This uses sql.js (SQLLite compiled to webassembly) and persists state into IndexedDB.&lt;/p&gt;
&lt;p&gt;SQL is battle tested and supports a wide array of features. But if you take the constraints we set out, you’ll see it’s a bad bet.&lt;/p&gt;
&lt;h2&gt;Schema and Size&lt;/h2&gt;
&lt;p&gt;My investigation began with two light issues.&lt;/p&gt;
&lt;p&gt;First, SQL has a schema. Schema is useful, but it make things less easy than Firebase. You can hack immediately in Firebase, but there’s upfront work with a schema. &lt;sup id=&quot;user-content-fnref-19&quot;&gt;&lt;a href=&quot;#user-content-fn-19&quot;&gt;[19]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Second, there’s size. sql.js is about 400KBs gzipped. Yes this can be cached, but I just don’t see most apps adopting a library that adds this overhead.&lt;/p&gt;
&lt;p&gt;Both reservations have reasonable counters. We could infer a schema on our user’s behalf, or write a lighter implementation of SQL. With problems like this we could have moved forward.&lt;/p&gt;
&lt;h2&gt;Language&lt;/h2&gt;
&lt;p&gt;But SQL as a language turns out to be a dealbreaker. SQL isn’t simple or easy. It’s a tough combination of lots of features, with little of it being useful for the frontend.&lt;/p&gt;
&lt;p&gt;Consider the most common query for UIs: Fetch nested relations. Remember our &lt;code&gt;dataForTaskPage&lt;/code&gt;?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function dataForTaskPage(store, teamId) {
  return {
    ...store.teams[teamId],
    tasks: store.tasksForTeam(teamId).map((task) =&amp;gt; {
      return { ...task, owner: store.users[task.ownerId] };
    }),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is one SQL query for it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  teams.*, tasks.*, owner.*
FROM teams
JOIN tasks ON tasks.team_id = teams.id
JOIN users as owner ON tasks.owner_id = owner.id
WHERE teams.id = ?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it works. But it’s inconvenient. Our query will return an exploded list of rows. Each row represents an owner, with tasks and teams duplicated. But what we actually wanted was a nested structure. Something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  teams: [{id: 2, name: &amp;quot;Awesome Team&amp;quot;, tasks: [{..., owner: {}}, ...]}, ...]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this work, we could use a &lt;code&gt;GROUP BY&lt;/code&gt; with &lt;code&gt;json_group_array&lt;/code&gt; and &lt;code&gt;json_object&lt;/code&gt;. Like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT
  teams.*,
  json_group_array(
    json_object(
      &amp;#39;id&amp;#39;, tasks.id,
      &amp;#39;title&amp;#39;, tasks.title,
      &amp;#39;owner&amp;#39;, json_object(&amp;#39;id&amp;#39;, owner.id, &amp;#39;name&amp;#39;, owner.name))
  ) as tasks
FROM teams
JOIN tasks ON tasks.team_id = teams.id
JOIN users as owner ON owner.id = tasks.owner_id
GROUP BY teams.id
WHERE teams.id = ?
&lt;/code&gt;&lt;/pre&gt;
&lt;p align=&quot;center&quot;&gt;
  &lt;em&gt;Try it &lt;a href=&quot;https://sqlime.org/#gist:3e02f01fdc8a0d131a5a07ac7b4a6d70&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/em&gt;
&lt;/p&gt;

&lt;p&gt;But you can already see we’re going off the beaten path. What if we had subscribers for each task? We’d need at least two more joins. One more &lt;code&gt;GROUP BY&lt;/code&gt;. Likely we’d want a subquery. And if we wanted to support the Notion case? We’d want a &lt;code&gt;WITH RECURSIVE&lt;/code&gt; clause.&lt;/p&gt;
&lt;p&gt;Now we’re in a tough spot. The frontend’s common case is SQL’s advanced case. We shouldn’t need advanced features for common cases.&lt;/p&gt;
&lt;p&gt;Plus, what about all the SQL features we’d rarely use in the frontend? The spec for the core language is over 1700 pages long &lt;sup id=&quot;user-content-fnref-20&quot;&gt;&lt;a href=&quot;#user-content-fn-20&quot;&gt;[20]&lt;/a&gt;&lt;/sup&gt;. We’d have to implement reactivity for all 1700 pages. I don’t think the schlep is worth it.&lt;/p&gt;
&lt;h1&gt;Another Approach&lt;/h1&gt;
&lt;p&gt;SQL is out. Let’s start with a different question then: How do we make frontend queries easy?&lt;/p&gt;
&lt;p&gt;The most common query is our “fetch nested relations”. For Linear it’s “team, with related tasks and their owners”. Or for Notion, we want “blocks, with child blocks expanded”. Or for Figma, “documents with their comments, layers, and properties”.&lt;/p&gt;
&lt;p&gt;See a pattern here? They’re all graphs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/graphs_everywhere.png&quot; alt=&quot;Graphs everywhere&quot;&gt;&lt;/p&gt;
&lt;p&gt;And this pointed us to a question: would a graph database make frontend queries easy?&lt;/p&gt;
&lt;h1&gt;Triple Stores&lt;/h1&gt;
&lt;p&gt;So we wrote a graph database to find out. We chose Triple Stores, one of the simplest kinds of graph databases. If you haven’t tried one, here’s a quick intuition:&lt;/p&gt;
&lt;p&gt;Imagine we’re trying to express a graph with data structures. What do we need?&lt;/p&gt;
&lt;p&gt;Well, we need to be able to express a node with attributes. To say:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;User with id 1 has name &amp;quot;Joe&amp;quot;
Team with id 2 has name &amp;quot;Awesome Team&amp;quot;
Task with id 3 has title &amp;quot;Code&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These sentences translate to lists:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[1, &amp;#39;name&amp;#39;, &amp;#39;Joe&amp;#39;][(2, &amp;#39;name&amp;#39;, &amp;#39;Awesome Team&amp;#39;)][(3, &amp;#39;title&amp;#39;, &amp;#39;Code&amp;#39;)];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we want a way to describe references. To say:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;Task with id 3 has an &amp;quot;owner&amp;quot; reference to User with id 1
Team with id 2 has a &amp;quot;task&amp;quot; reference to Task with id 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Well...these translate to lists just as well:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[3, &amp;#39;owner&amp;#39;, 1][(2, &amp;#39;tasks&amp;#39;, 3)];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put these lists in a table, and you have a triple store! &lt;em&gt;Triple&lt;/em&gt; is the name of the list we’ve been writing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[1, &amp;#39;name&amp;#39;, &amp;#39;Joe&amp;#39;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first item is always an &lt;code&gt;id&lt;/code&gt;, the second the &lt;code&gt;attribute&lt;/code&gt;, and the third, the &lt;code&gt;value&lt;/code&gt;. Turns out triples are all we need to express a graph.&lt;/p&gt;
&lt;p&gt;Here’s a more fleshed out example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/triple_store_graph.png&quot; alt=&quot;Triple Store → Graph&quot;&gt;&lt;/p&gt;
&lt;p&gt;And once you’ve expressed a graph, you can traverse it. Triple stores have interesting query languages. Here’s Datalog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;(pull db &amp;#39;[* {:team/task [* {:task/owner [*]}]}] team-id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this we’ve replaced &lt;code&gt;dataForTasksPage&lt;/code&gt;!&lt;/p&gt;
&lt;h1&gt;Exploring Triple Stores&lt;/h1&gt;
&lt;p&gt;Triple stores felt like our rubicon moment. An entire architecture unravelled from our choice.&lt;/p&gt;
&lt;h2&gt;Schema and Size&lt;/h2&gt;
&lt;p&gt;My investigation kicked off with two happy surprises.&lt;/p&gt;
&lt;p&gt;First, I always assumed that if we wanted relations, we would need a schema. But it turns out triple stores don’t need one. &lt;sup id=&quot;user-content-fnref-21&quot;&gt;&lt;a href=&quot;#user-content-fn-21&quot;&gt;[21]&lt;/a&gt;&lt;/sup&gt; I think a schema is helpful. But to compete with Firebase, it’s a win that we can make this optional.&lt;/p&gt;
&lt;p&gt;Then there’s size. Triple stores are notoriously light. Datascript is one of the most battle-tested triple stores. It’s transpiled from Clojurescript and carries the extra weight of Clojure. But even then, the bundle size is about 90KB.&lt;/p&gt;
&lt;h2&gt;Simple&lt;/h2&gt;
&lt;p&gt;But the killer feature is how simple triple stores are. &lt;strong&gt;You can write a roughly complete implementation in less than a hundred lines of Javascript&lt;/strong&gt; &lt;sup id=&quot;user-content-fnref-22&quot;&gt;&lt;a href=&quot;#user-content-fn-22&quot;&gt;[22]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The query planner uses 3 main indexes &lt;sup id=&quot;user-content-fnref-23&quot;&gt;&lt;a href=&quot;#user-content-fn-23&quot;&gt;[23]&lt;/a&gt;&lt;/sup&gt;. Datalog — the query language I mentioned — is so simple that there isn’t a spec &lt;sup id=&quot;user-content-fnref-24&quot;&gt;&lt;a href=&quot;#user-content-fn-24&quot;&gt;[24]&lt;/a&gt;&lt;/sup&gt;. The mutation system boils down two primitives &lt;sup id=&quot;user-content-fnref-25&quot;&gt;&lt;a href=&quot;#user-content-fn-25&quot;&gt;[25]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Even with the 100 LOC version, you can express a query like “Give me all the owners for the tasks where this person is a subscriber” &lt;sup id=&quot;user-content-fnref-26&quot;&gt;&lt;a href=&quot;#user-content-fn-26&quot;&gt;[26]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;80/20 for Multiplayer&lt;/h2&gt;
&lt;p&gt;Turns out triple stores are a great answer for multiplayer too. Once we make our Local DB collaborative, we’ll need to support conflicts. What should happen when two people change something at the same time?&lt;/p&gt;
&lt;p&gt;Notion, Figma, and Linear all use last-write-wins. This means that whichever change reaches the server last wins.&lt;/p&gt;
&lt;p&gt;This can work well, but we need to be creative about it. Imagine if two of us changed the same Figma Layer. One of us changed the font size, and the other changed the background color. If we’re creative about how we save things, there shouldn’t be a conflict in the first place.&lt;/p&gt;
&lt;p&gt;How does Figma do this? They store their properties in a special way. They store them as...triples! &lt;sup id=&quot;user-content-fnref-27&quot;&gt;&lt;a href=&quot;#user-content-fn-27&quot;&gt;[27]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[1, &amp;#39;fontSize&amp;#39;, 20][(1, &amp;#39;backgroundColor&amp;#39;, &amp;#39;blue&amp;#39;)];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These triples say that the Layer with id 1 has a fontSize 20 and backgroundColor blue. Since they are different rows, there’s no conflict.&lt;/p&gt;
&lt;p&gt;And voila, we have the same kind of conflict-resolution as Figma. &lt;sup id=&quot;user-content-fnref-28&quot;&gt;&lt;a href=&quot;#user-content-fn-28&quot;&gt;[28]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;But Speed and Scale?&lt;/h2&gt;
&lt;p&gt;At this point, you may wonder: this is great and all, but what about speed and scale?&lt;/p&gt;
&lt;p&gt;Well, the core technology is old &lt;sup id=&quot;user-content-fnref-29&quot;&gt;&lt;a href=&quot;#user-content-fn-29&quot;&gt;[29]&lt;/a&gt;&lt;/sup&gt;. Datalog and triple stores have been around for decades. This also means that people have built reactive implementations &lt;sup id=&quot;user-content-fnref-30&quot;&gt;&lt;a href=&quot;#user-content-fn-30&quot;&gt;[30]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;But what makes me most optimistic about the answer here, is that Facebook runs on a graph database. Tao is facebook’s in-house data store. If you look at Tao, it’s not so different from a triple store! &lt;sup id=&quot;user-content-fnref-31&quot;&gt;&lt;a href=&quot;#user-content-fn-31&quot;&gt;[31]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Easy?&lt;/h2&gt;
&lt;p&gt;This is getting exciting. But what about ease of use? This is how the “Give me all the owners for the tasks where this person is a subscriber” query looks in Datalog:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;{:find ?owner,
 :where [[?task :task/owner ?owner]
         [?task :task/subscriber sub-id]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Datalog as a language is elegant and simple. But it’s not easy the same way Firebase is. You need to learn a logic-based language. Then you get back triples. But in the UI you want typed objects.&lt;/p&gt;
&lt;p&gt;This would be a deal-breaker. But here’s where Datalog’s strength comes in. &lt;strong&gt;It’s so small that we can just keep it as our base layer, and write a friendlier language on top.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;InstaQL&lt;/h2&gt;
&lt;p&gt;That’s how InstaQL was born. If you look at what’s intuitive for the UI, I think GraphQL syntax comes closest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;teams {
  ...
  tasks: {
    ...
    owner: {
      ...
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You just declare what you want; the shape of the query looks like the result.&lt;/p&gt;
&lt;p&gt;InstaQL was heavily inspired by GraphQL. It’s a similar-looking language and produces Datalog. Here’s how queries look:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  teams: {
    $: {where: {id: 1}},
    tasks: {owner: {}},
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the first departure from GraphQL: InstaQL is written with plain javascript objects. This lets us avoid a build step; after all Firebase doesn’t need one. And there’s another win: if the language itself is written with objects and arrays, engineers can write functions that manipulate them.&lt;/p&gt;
&lt;p&gt;The second departure is in the mutation system. In GraphQL you define mutations as functions in the backend. This is a problem because then you can’t do optimistic updates out of the box. Without talking to the server, there’s no way to know what a mutation does.&lt;/p&gt;
&lt;p&gt;In InstaQL, mutations look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;transact([
  tx.tasks[taskId]
    .update({title: &amp;quot;New Task&amp;quot;})
    .link({owner: ownerId}}
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These mutations produce triple store assertions and retractions. So our Local DB can apply them, and we have optimistic updates out of the box again. &lt;sup id=&quot;user-content-fnref-32&quot;&gt;&lt;a href=&quot;#user-content-fn-32&quot;&gt;[32]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h1&gt;Instant Today&lt;/h1&gt;
&lt;p&gt;So we wrote a triple store, and Instant was born. Today you have a reactive database with offline mode, optimistic updates, multiplayer, auth, and permissions at your fingertips.&lt;/p&gt;
&lt;p&gt;Locally, there&amp;#39;s a triple store that understands InstaQL. You can write queries like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  teams: {
    $: {where: {id: 1}},
    tasks: {owner: {}}
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And get back objects:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  teams: [
    {
      id: 1,
      name: &amp;#39;Awesome Team&amp;#39;,
      tasks: [{ id: 3, title: &amp;#39;Code&amp;#39;, owner: [{ id: 1, name: &amp;#39;Joe&amp;#39; }] }],
    },
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every query works offline, and all changes are applied instantly. The server has a reactive layer that broadcasts novelty. You can write permissions, and you have an SDK you can use for web, React Native, and Node.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s been thrilling to see users try Instant. When they write their first relational query I see delight in their eyes, and boy is that thrilling.&lt;/p&gt;
&lt;p&gt;If you’re excited about this stuff, &lt;a href=&quot;https://instantdb.com&quot;&gt;sign up and give us a try&lt;/a&gt;. We will reach out to your personally for feedback.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=32595895&quot;&gt;Dicussion on HN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks Joe Averbukh, Alex Reichert, Mark Shlick, Slava Akhmechet, Nicole Garcia Fischer, Daniel Woelfel, Jake Teton-Landis, Rudi Chen, Dan Vingo, Dennis Heihoff for reviewing drafts of this essay.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-1&quot; href=&quot;#user-content-fnref-1&quot;&gt;[1]&lt;/a&gt;  ​​Think optimistic updates, reactivity, and offline mode. I’ll cover them in this essay, so no need to jump into the previous one.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-2&quot; href=&quot;#user-content-fnref-2&quot;&gt;[2]&lt;/a&gt;  ​​Or you could put them in a failure queue and try again later. Lots to think about.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-3&quot; href=&quot;#user-content-fnref-3&quot;&gt;[3]&lt;/a&gt;  ​​I am still on the lookout for a paper about this, but in the meantime, consider this thought experiment. Imagine a guitar. How would the experience be, if when you pulled on string, there was a lag before you heard the sound?&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;: ​​Even &lt;a href=&quot;https://twitter.com/stopachka/status/1557485881539297282&quot;&gt;“Changed your profile info”&lt;/a&gt; is reactive!&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-5&quot; href=&quot;#user-content-fnref-5&quot;&gt;[5]&lt;/a&gt;  ​​Streaming changes alone is a painful task. But consider the nuances. For example, you would think you could apply all changes everywhere immediately. But this doesn’t always work. Imagine Facebook comments. If new comments showed up as you viewed a post, your screen would constantly shift. This is why you see a button instead.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-6&quot; href=&quot;#user-content-fnref-6&quot;&gt;[6]&lt;/a&gt;  ​​Consider Dropbox Paper and Notion. Could you realistically keep a journal in Paper? What would you do on an airplane? Or what if you want to jot something down in some foreign place? Well, this is why Notion eats Paper’s cake.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-7&quot;&gt;&lt;a href=&quot;#user-content-fn-7&quot;&gt;[7]&lt;/a&gt;&lt;/sup&gt;: The CTO of Linear goes over this win and more in his &lt;a href=&quot;https://twitter.com/artman/status/1558081796914483201&quot;&gt;tweet thread&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-8&quot;&gt;&lt;a href=&quot;#user-content-fn-8&quot;&gt;[8]&lt;/a&gt;&lt;/sup&gt;: ​​This talk on &lt;a href=&quot;https://www.youtube.com/watch?v=WxK11RsLqp4&amp;t=2169s&quot;&gt;Linear’s architecture&lt;/a&gt; was great.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-9&quot;&gt;&lt;a href=&quot;#user-content-fn-9&quot;&gt;[9]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://www.notion.so/blog/data-model-behind-notion&quot;&gt;​​The data model behind Notion’s flexibility&lt;/a&gt; is awesome.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-10&quot;&gt;&lt;a href=&quot;#user-content-fn-10&quot;&gt;[10]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://www.figma.com/blog/how-figmas-multiplayer-technology-works/&quot;&gt;​​Figma’s multiplayer essay&lt;/a&gt; is a classic.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-11&quot;&gt;&lt;a href=&quot;#user-content-fn-11&quot;&gt;[11]&lt;/a&gt;&lt;/sup&gt;: ​​Figma’s &lt;a href=&quot;https://www.figma.com/blog/livegraph-real-time-data-fetching-at-figma/&quot;&gt;LiveGraph&lt;/a&gt; is very cool.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-12&quot;&gt;&lt;a href=&quot;#user-content-fn-12&quot;&gt;[12]&lt;/a&gt;&lt;/sup&gt;: ​​I say this like it’s no big deal. But live views are challenging. Here’s a &lt;a href=&quot;https://www.quora.com/What-is-the-point-of-RethinkDBs-push-capability&quot;&gt;quora answer&lt;/a&gt; that explains some nuances.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-13&quot; href=&quot;#user-content-fnref-13&quot;&gt;[13]&lt;/a&gt;  ​​The alternative approach is to make permission checks ad-hoc; some at the API layer, some inside functions, etc. But then, you can never be sure if you’re really allowed to see the data you’re manipulating.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-14&quot; href=&quot;#user-content-fnref-14&quot;&gt;[14]&lt;/a&gt;  ​​If you poll, complicated queries can took long. If you leverage a write-ahead log, it’s difficult to know what change affects them.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-15&quot; href=&quot;#user-content-fnref-15&quot;&gt;[15]&lt;/a&gt;  ​​Eventually backend developers evolve their work into sophisticated systems. This is how Figma’s LiveGraph was born.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-16&quot; href=&quot;#user-content-fnref-16&quot;&gt;[16]&lt;/a&gt;  The examples that follow are from Firebase’s documentation.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-17&quot;&gt;&lt;a href=&quot;#user-content-fn-17&quot;&gt;[17]&lt;/a&gt;&lt;/sup&gt;: ​​At Airbnb I helped build &lt;a href=&quot;https://medium.com/airbnb-engineering/hacking-human-connection-the-story-of-awedience-ebf66ee6af0e&quot;&gt;Awedience&lt;/a&gt;. This worked on top of Firebase. I had to do hack after hack to make permissions work. I almost wrote a higher level language for it.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-18&quot;&gt;&lt;a href=&quot;#user-content-fn-18&quot;&gt;[18]&lt;/a&gt;&lt;/sup&gt;: ​​Kevin Lacker has a &lt;a href=&quot;https://www.youtube.com/watch?v=qCdpTji8nxo&quot;&gt;great talk&lt;/a&gt; about writing these kind of APIs.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-19&quot; href=&quot;#user-content-fnref-19&quot;&gt;[19]&lt;/a&gt;  ​​I think over the long-term a database benefits from a schema. But it hurts ease-of-use. This doesn’t mean we chuck schema entirely. It just means we should be upfront about this cost. As you’ll see, we can be creative about it too.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-20&quot;&gt;&lt;a href=&quot;#user-content-fn-20&quot;&gt;[20]&lt;/a&gt;&lt;/sup&gt;: ​​See &lt;a href=&quot;https://blog.ansi.org/2018/10/sql-standard-iso-iec-9075-2016-ansi-x3-135/&quot;&gt;this&lt;/a&gt;. I found the link in &lt;a href=&quot;https://www.scattered-thoughts.net/writing/against-sql/&quot;&gt;“Against SQL”&lt;/a&gt;. I loved the thoughtfulness in the essay.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-21&quot;&gt;&lt;a href=&quot;#user-content-fn-21&quot;&gt;[21]&lt;/a&gt;&lt;/sup&gt;: Here’s &lt;a href=&quot;https://github.com/threatgrid/asami&quot;&gt;asami&lt;/a&gt;, a schemaless implementation. Now, I think at the very least you should distinguish between attributes and references. But you don’t need to.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-22&quot;&gt;&lt;a href=&quot;#user-content-fn-22&quot;&gt;[22]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://www.instantdb.com/essays/datalogjs&quot;&gt;We wrote a tutorial to do it!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-23&quot;&gt;&lt;a href=&quot;#user-content-fn-23&quot;&gt;[23]&lt;/a&gt;&lt;/sup&gt;: ​​It gets more complicated, but honestly not much more complicated. If you’re curious, &lt;a href=&quot;https://github.com/juji-io/datalevin/blob/query/doc/query.md&quot;&gt;this doc&lt;/a&gt; links into great research.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-24&quot;&gt;&lt;a href=&quot;#user-content-fn-24&quot;&gt;[24]&lt;/a&gt;&lt;/sup&gt;: ​​The syntax for logic-based datalog can be expressed in &lt;a href=&quot;https://en.wikipedia.org/wiki/Datalog#Syntax&quot;&gt;8 lines&lt;/a&gt;. Edn-style datalog doesn’t have a spec, but it’s simpler than SparQL. SparQL is a competitive graph-based query language, and the spec there is &lt;a href=&quot;https://www.w3.org/TR/2013/REC-sparql11-query-20130321/&quot;&gt;less than a hundred pages long&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-25&quot; href=&quot;#user-content-fnref-25&quot;&gt;[25]&lt;/a&gt;  ​​Every mutation is either an assertion or a retraction of a triple.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-26&quot;&gt;&lt;a href=&quot;#user-content-fn-26&quot;&gt;[26]&lt;/a&gt;&lt;/sup&gt;: ​​Here’s a &lt;a href=&quot;https://github.com/stopachka/datalogJS/blob/main/src/index.test.js#L110-L135&quot;&gt;query of similar complexity&lt;/a&gt;, tested over a datalog engine that’s less than a hundred lines.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-27&quot;&gt;&lt;a href=&quot;#user-content-fn-27&quot;&gt;[27]&lt;/a&gt;&lt;/sup&gt;: ​​Cmd +F for (ObjectID, Property, Value)​ in this &lt;a href=&quot;https://www.figma.com/blog/how-figmas-multiplayer-technology-works/&quot;&gt;essay&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-28&quot;&gt;&lt;a href=&quot;#user-content-fn-28&quot;&gt;[28]&lt;/a&gt;&lt;/sup&gt;: ​​At this point, you may be thinking…last-write-wins? C’mone — what about more serious CRDTs or OTs? ​​I think last-write-wins is a great 80/20, and gets us the same level as Notion, Figma, and Linear. Buut, there’s a lot of exciting research (&lt;a href=&quot;https://martin.kleppmann.com/2018/02/26/dagstuhl-data-consistency.html&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://fission.codes/blog/fission-reactor-dialog-first-look/&quot;&gt;2&lt;/a&gt;). It’s reassuring though that a lot of this research centers around triples and Datalog. We can do what Figma does today, and when the research is more mature, integrate it down the road.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-29&quot;&gt;&lt;a href=&quot;#user-content-fn-29&quot;&gt;[29]&lt;/a&gt;&lt;/sup&gt;: ​​Datalog launched in &lt;a href=&quot;https://en.wikipedia.org/wiki/Datalog&quot;&gt;1986&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-30&quot;&gt;&lt;a href=&quot;#user-content-fn-30&quot;&gt;[30]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;http://ceur-ws.org/Vol-2368/paper6.pdf&quot;&gt;Differential Datalog&lt;/a&gt; is interesting&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-31&quot;&gt;&lt;a href=&quot;#user-content-fn-31&quot;&gt;[31]&lt;/a&gt;&lt;/sup&gt;: &lt;a href=&quot;https://www.usenix.org/system/files/conference/atc13/atc13-bronson.pdf&quot;&gt;​​Here’s the paper&lt;/a&gt;. They store objects instead of triples, and store associations differently. They support 1 Billion (!!) reads / sec, and 1 Million writes / sec&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-32&quot;&gt;&lt;a href=&quot;#user-content-fn-32&quot;&gt;[32]&lt;/a&gt;&lt;/sup&gt;: ​​If you’re curious, here’s an &lt;a href=&quot;https://paper.dropbox.com/doc/InstaQL--BgBK88TTiSE9OV3a17iCwDjCAg-yVxntbv98aeAovazd9TNL&quot;&gt;expanded spec&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-33&quot; href=&quot;#user-content-fnref-33&quot;&gt;[33]&lt;/a&gt;  ​​You may be wondering — what about the details on the backend? We’re already 4000 words. Let us know if you’re interested and we’ll write a follow-on essay!&lt;/p&gt;
</description>
      <pubDate>Thu, 25 Aug 2022 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Datalog in Javascript</title>
      <link>https://instantdb.com/essays/datalogjs</link>
      <guid isPermaLink="true">https://instantdb.com/essays/datalogjs</guid>
      <description>&lt;p&gt;Query engines make me feel like a wizard. I cast my incantation: “Give me all the directors and the movies where Arnold Schwarzenegger was a cast member”. Then charges zip through wires, algorithms churn on CPUs, and voila, an answer bubbles up.&lt;/p&gt;
&lt;p&gt;How do they work? In this essay, we will build a query engine from scratch and find out. In 100 lines of Javascript, we’ll supports joins, indexes, &lt;em&gt;and&lt;/em&gt; find our answer for Arnold! Let’s get into it.&lt;/p&gt;
&lt;h1&gt;Choice&lt;/h1&gt;
&lt;p&gt;Our first step is to choose which language we’ll support. SQL is the most popular, but we wouldn’t get far in 100 lines. I suggest we amble off the beaten path and make Datalog instead.&lt;/p&gt;
&lt;p&gt;If you haven’t heard of Datalog, you’re in for a treat. It’s a logic-based query language that’s as powerful as SQL. We won’t cover it completely, but we’ll cover enough to fit a good weekend’s worth of hacking.&lt;/p&gt;
&lt;p&gt;To grok Datalog, we need to understand three ideas:&lt;/p&gt;
&lt;h1&gt;Data&lt;/h1&gt;
&lt;p&gt;The first idea is about how we store data.&lt;/p&gt;
&lt;h2&gt;SQL Tables&lt;/h2&gt;
&lt;p&gt;SQL databases store data in different tables:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_query_example.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here we have a &lt;code&gt;movie&lt;/code&gt; table, which stores one movie per row. The record with the id &lt;code&gt;200&lt;/code&gt; is &lt;code&gt;&amp;quot;The Terminator&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Notice the &lt;code&gt;director_id&lt;/code&gt;. This points to a row in yet another &lt;code&gt;person&lt;/code&gt; table, which keeps the director’s name, and so on.&lt;/p&gt;
&lt;h2&gt;Datalog Triples&lt;/h2&gt;
&lt;p&gt;In Datalog databases, there are no tables. Or really everything is just stored in one table, the &lt;code&gt;triple&lt;/code&gt; table:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_graph_example.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;triple&lt;/code&gt; is a row with an &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;attribute&lt;/code&gt;, and &lt;code&gt;value&lt;/code&gt;. Triples have a curious property; with just these three columns, they can describe any kind of information!&lt;/p&gt;
&lt;p&gt;How? Imagine describing a movie to someone:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&amp;#39;s called &amp;quot;The Terminator&amp;quot;
It was released in 1987&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those sentences conveniently translate to triples:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[200, movie / title, &amp;#39;The Terminator&amp;#39;][(200, movie / year, 1987)];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And those sentences have a general structure; if you can describe a movie this way, you can describe tomatoes or airplanes just as well.&lt;/p&gt;
&lt;h1&gt;Queries&lt;/h1&gt;
&lt;p&gt;The second idea is about how we search for information.&lt;/p&gt;
&lt;h2&gt;SQL Algebra&lt;/h2&gt;
&lt;p&gt;SQL has roots in relational algebra. You give the query engine a combination of clauses and statements, and it gets you back your data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT id FROM movie WHERE year = 1987
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This returns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[{ id: 202 }, { id: 203 }, { id: 204 }];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Voila, the movie ids for Predator, Lethal Weapon, and RoboCop.&lt;/p&gt;
&lt;h2&gt;Datalog Pattern Matching&lt;/h2&gt;
&lt;p&gt;Datalog databases rely on pattern matching. We create “patterns” that match against triples. For example, to find all the movies released in 1987, we could use this pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[?id, movie/year, 1987]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;?id&lt;/code&gt; is a variable: we’re telling the query engine that it can be &lt;em&gt;any&lt;/em&gt; value. But, the &lt;code&gt;attribute&lt;/code&gt; &lt;em&gt;must&lt;/em&gt; be &lt;code&gt;movie/year&lt;/code&gt;, and the &lt;code&gt;value&lt;/code&gt; &lt;em&gt;must&lt;/em&gt; be &lt;code&gt;1987&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_triples.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Our query engine runs through triple after triple. Since &lt;code&gt;?id&lt;/code&gt; can be anything, this matches every triple. But, the attribute &lt;code&gt;movie/year&lt;/code&gt; and the value &lt;code&gt;1987&lt;/code&gt; filter us down to &lt;em&gt;just&lt;/em&gt; the triples we care about:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[
  [202, movie / year, 1987],
  [203, movie / year, 1987],
  [204, movie / year, 1987],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;?id&lt;/code&gt; portion; those are the ids for Predator, Lethal Weapon, and RoboCop!&lt;/p&gt;
&lt;h2&gt;Datalog &lt;code&gt;find&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;In SQL, we &lt;em&gt;just&lt;/em&gt; got back ids though, while our query engine returned more. How can we support returning ids only? Let’s adjust our syntax; here’s &lt;code&gt;find&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{ find: [?id],
  where: [
    [?id, movie/year, 1987]
  ] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our query engine can now use the &lt;code&gt;find&lt;/code&gt; section to return what we care about. If we implement this right, we should get back:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[[202], [203], [204]];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And now we’re as dandy as SQL.&lt;/p&gt;
&lt;h1&gt;Joins&lt;/h1&gt;
&lt;p&gt;The third idea is about how joins work. Datalog and SQL’s magic comes from them.&lt;/p&gt;
&lt;h2&gt;SQL clauses&lt;/h2&gt;
&lt;p&gt;In SQL, if we wanted to find “The Terminator’s” director, we could write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;SELECT
  person.name
FROM movie
JOIN person ON movie.director_id = person.id
WHERE movie.title = &amp;quot;The Terminator&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which gets us:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[{ name: &amp;#39;James Cameron&amp;#39; }];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty cool. We used the &lt;code&gt;JOIN&lt;/code&gt; clause to connect the movie table with the person table, and bam, we got our director’s name.&lt;/p&gt;
&lt;h2&gt;Datalog…Pattern Matching&lt;/h2&gt;
&lt;p&gt;In Datalog, we still rely on pattern matching. The trick is to match &lt;em&gt;multiple&lt;/em&gt; patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  find: [?directorName],
  where: [
    [?movieId, movie/title, &amp;quot;The Terminator&amp;quot;],
    [?movieId, movie/director, ?directorId],
    [?directorId, person/name, ?directorName],
  ],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we tell the query engine to match &lt;em&gt;three&lt;/em&gt; patterns. The first pattern produces a list of successful triples. For each successful triple, we search again with the &lt;em&gt;second&lt;/em&gt; pattern, and so on. Notice how the &lt;code&gt;?movieId&lt;/code&gt; and &lt;code&gt;?directorId&lt;/code&gt; are repeated; this tells our query engine that for a successful match, those values would need to be the &lt;em&gt;same&lt;/em&gt; across our different searches.&lt;/p&gt;
&lt;p&gt;What do I mean? Let’s make this concrete; here’s how our query engine could find The Terminator’s director:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_pattern_matching.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The first pattern finds:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[200, movie/title, &amp;quot;The Terminator&amp;quot;].
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We bind &lt;code&gt;?movieId&lt;/code&gt; to &lt;code&gt;200&lt;/code&gt;. Now we start searching for the second pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[?movieId, movie/director, ?directorName].
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since &lt;code&gt;?movieId&lt;/code&gt; needs to be &lt;code&gt;200&lt;/code&gt;, this finds us&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[200, movie / director, 100];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we can now bind &lt;code&gt;?directorId&lt;/code&gt; to &lt;code&gt;100&lt;/code&gt;. Time for the third pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[?directorId, person/name, ?directorName]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;?directorId&lt;/code&gt; has to be &lt;code&gt;100&lt;/code&gt;, our engine finds us:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[100, person / name, &amp;#39;James Cameron&amp;#39;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And perfecto, the &lt;code&gt;?directorName&lt;/code&gt; is now bound to &lt;code&gt;&amp;quot;James Cameron&amp;quot;&lt;/code&gt;! The &lt;code&gt;find&lt;/code&gt; section would then return &lt;code&gt;[&amp;quot;James Cameron&amp;quot;]&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Oky doke, now we grok the basics of Datalog! Let’s get to the code.&lt;/p&gt;
&lt;h1&gt;Syntax&lt;/h1&gt;
&lt;p&gt;First things first, we need a way to represent this syntax. If you look at:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{ find: [?id],
  where: [
    [?id, movie/year, 1987]
  ] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could &lt;em&gt;almost&lt;/em&gt; write this in Javascript. We use objects and arrays, but &lt;code&gt;?id&lt;/code&gt; and &lt;code&gt;movie/year&lt;/code&gt; get in the way; they would throw an error. We can fix this with a hack: let’s turn them into strings.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{ find: [&amp;quot;?id&amp;quot;],
  where: [
    [&amp;quot;?id&amp;quot;, &amp;quot;movie/year&amp;quot;, 1987]
  ] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s less pretty, but we can now express our queries without fanfare. If a string begins with a question mark, it’s a variable. An attribute is just a string; it’s a good idea to include a namespace like &lt;code&gt;&amp;quot;movie/*&amp;quot;&lt;/code&gt;, but we won’t force our users.&lt;/p&gt;
&lt;h1&gt;Sample Data&lt;/h1&gt;
&lt;p&gt;The next thing we’ll need is sample data to play with. There’s a great datalog tutorial &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;, which has the movie dataset we’ve been describing. I’ve taken it and adapted it to Javascript. &lt;a href=&quot;https://github.com/stopachka/datalogJS/blob/main/src/exampeTriples.js&quot;&gt;Here’s the file&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// exampleTriples.js
export default [
  [100, &amp;#39;person/name&amp;#39;, &amp;#39;James Cameron&amp;#39;],
  [100, &amp;#39;person/born&amp;#39;, &amp;#39;1954-08-16T00:00:00Z&amp;#39;],
  // ...
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s plop this in and require it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import exampleTriples from &amp;#39;./exampleTriples&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now for our query engine!&lt;/p&gt;
&lt;h1&gt;matchPattern&lt;/h1&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Our first goal is to match &lt;em&gt;one&lt;/em&gt; pattern with &lt;em&gt;one&lt;/em&gt; triple. Here’s an example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_joins.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;We have some variable bindings: &lt;code&gt;{&amp;quot;?movieId&amp;quot;: 200}&lt;/code&gt;. Let’s call this a &lt;code&gt;context&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Our goal is to take a pattern, a triple, and a context. We’ll either return a new context:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{&amp;quot;?movieId&amp;quot;: 200, &amp;quot;?directorId&amp;quot;: 100}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or a failure. We can just say &lt;code&gt;null&lt;/code&gt; means failure.&lt;/p&gt;
&lt;p&gt;This could be the test we play with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(
  matchPattern(
    [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/director&amp;#39;, &amp;#39;?directorId&amp;#39;],
    [200, &amp;#39;movie/director&amp;#39;, 100],
    { &amp;#39;?movieId&amp;#39;: 200 },
  ),
).toEqual({ &amp;#39;?movieId&amp;#39;: 200, &amp;#39;?directorId&amp;#39;: 100 });
expect(
  matchPattern(
    [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/director&amp;#39;, &amp;#39;?directorId&amp;#39;],
    [200, &amp;#39;movie/director&amp;#39;, 100],
    { &amp;#39;?movieId&amp;#39;: 202 },
  ),
).toEqual(null);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;Nice, we have a plan. Let’s write the larger function first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function matchPattern(pattern, triple, context) {
  return pattern.reduce((context, patternPart, idx) =&amp;gt; {
    const triplePart = triple[idx];
    return matchPart(patternPart, triplePart, context);
  }, context);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We take our pattern, and compare each part to the corresponding one in our triple:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_where_clause.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So, we’d compare &lt;code&gt;&amp;quot;?movieId&amp;quot;&lt;/code&gt; with &lt;code&gt;200&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;h2&gt;matchPart&lt;/h2&gt;
&lt;p&gt;We can delegate this comparison to &lt;code&gt;matchPart&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function matchPart(patternPart, triplePart, context) {
  if (!context) return null;
  if (isVariable(patternPart)) {
    return matchVariable(patternPart, triplePart, context);
  }
  return patternPart === triplePart ? context : null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First we address &lt;code&gt;context&lt;/code&gt;; if &lt;code&gt;context&lt;/code&gt; was &lt;code&gt;null&lt;/code&gt; we must have failed before, so we just return early.&lt;/p&gt;
&lt;h2&gt;isVariable&lt;/h2&gt;
&lt;p&gt;Next, we check if we’re looking at a variable. &lt;code&gt;isVariable&lt;/code&gt; is simple enough:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function isVariable(x) {
  return typeof x === &amp;#39;string&amp;#39; &amp;amp;&amp;amp; x.startsWith(&amp;#39;?&amp;#39;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;matchVariable&lt;/h2&gt;
&lt;p&gt;Now, if we &lt;em&gt;are&lt;/em&gt; looking at a variable, we’d want to handle it especially:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function matchVariable(variable, triplePart, context) {
  if (context.hasOwnProperty(variable)) {
    const bound = context[variable];
    return matchPart(bound, triplePart, context);
  }
  return { ...context, [variable]: triplePart };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We would check if we &lt;em&gt;already&lt;/em&gt; have a binding for this variable. For example, when comparing &lt;code&gt;?movieId&lt;/code&gt;, we’d already have the binding: “&lt;code&gt;200&lt;/code&gt;”. In this case, we just compare the bound value with what’s in our triple.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ...
if (context.hasOwnProperty(variable)) {
  const bound = context[variable];
  return matchPart(bound, triplePart, context);
}
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we compare &lt;code&gt;?directorId&lt;/code&gt; though, we’d see that this variable wasn’t bound. In this case, we’d want to &lt;em&gt;expand&lt;/em&gt; our context. We’d attach &lt;code&gt;?directorId&lt;/code&gt; to the corresponding part in our triple (&lt;code&gt;100&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;return { ...context, [variable]: triplePart };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, if we weren’t looking at a variable, we would have skipped this and just checked for equality. If the pattern part and the triple part match, we keep the context; otherwise we return null:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// ...
return patternPart === triplePart ? context : null;
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that, &lt;code&gt;matchPattern&lt;/code&gt; works as we like!&lt;/p&gt;
&lt;h1&gt;querySingle&lt;/h1&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Now for our second goal. We can already match one pattern with one triple. Let’s now match &lt;em&gt;one&lt;/em&gt; pattern with &lt;em&gt;multiple&lt;/em&gt; triples. Here’s the idea:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_find_clause.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;We’ll have &lt;em&gt;one&lt;/em&gt; pattern and a database of triples. We’ll want to return the contexts for all the successful matches. Here’s the test we can play with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(
  querySingle([&amp;#39;?movieId&amp;#39;, &amp;#39;movie/year&amp;#39;, 1987], exampleTriples, {}),
).toEqual([{ &amp;#39;?movieId&amp;#39;: 202 }, { &amp;#39;?movieId&amp;#39;: 203 }, { &amp;#39;?movieId&amp;#39;: 204 }]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;Well, much of the work comes down to &lt;code&gt;matchPattern&lt;/code&gt;. Here’s all &lt;code&gt;querySingle&lt;/code&gt; needs to do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function querySingle(pattern, db, context) {
  return db
    .map((triple) =&amp;gt; matchPattern(pattern, triple, context))
    .filter((x) =&amp;gt; x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We go over each triple and run &lt;code&gt;matchPattern&lt;/code&gt;. This would return either a &lt;code&gt;context&lt;/code&gt; (it’s a match!), or &lt;code&gt;null&lt;/code&gt; (it’s a failure). We &lt;code&gt;filter&lt;/code&gt; to remove the failures, and querySingle works like a charm!&lt;/p&gt;
&lt;h1&gt;queryWhere&lt;/h1&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Closer and closer. Now to support joins. We need to handle &lt;em&gt;multiple&lt;/em&gt; patterns:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/essays/datalog_queries_final.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;So we go pattern by pattern, and find successful triples. For each successful triple, we apply the next pattern. At the end, we’ll have produced progressively larger contexts.&lt;/p&gt;
&lt;p&gt;Here’s the test we can play with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(
  queryWhere(
    [
      [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/title&amp;#39;, &amp;#39;The Terminator&amp;#39;],
      [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/director&amp;#39;, &amp;#39;?directorId&amp;#39;],
      [&amp;#39;?directorId&amp;#39;, &amp;#39;person/name&amp;#39;, &amp;#39;?directorName&amp;#39;],
    ],
    exampleTriples,
    {},
  ),
).toEqual([
  { &amp;#39;?movieId&amp;#39;: 200, &amp;#39;?directorId&amp;#39;: 100, &amp;#39;?directorName&amp;#39;: &amp;#39;James Cameron&amp;#39; },
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;This too, is not so difficult. Here’s queryWhere:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function queryWhere(patterns, db) {
  return patterns.reduce(
    (contexts, pattern) =&amp;gt; {
      return contexts.flatMap((context) =&amp;gt; querySingle(pattern, db, context));
    },
    [{}],
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We start off with one empty context. We then go pattern by pattern; for each pattern, we find all the successful contexts. We then take those contexts, and use them for the next pattern. By the end, we’ll have all the expanded contexts, and &lt;code&gt;queryWhere&lt;/code&gt; works like a charm too!&lt;/p&gt;
&lt;h1&gt;Query&lt;/h1&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;And now we’ve just about built ourselves the whole query engine! Next let’s handle &lt;code&gt;where&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This could be the test we can play with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;expect(
  query(
    {
      find: [&amp;#39;?directorName&amp;#39;],
      where: [
        [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/title&amp;#39;, &amp;#39;The Terminator&amp;#39;],
        [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/director&amp;#39;, &amp;#39;?directorId&amp;#39;],
        [&amp;#39;?directorId&amp;#39;, &amp;#39;person/name&amp;#39;, &amp;#39;?directorName&amp;#39;],
      ],
    },
    exampleTriples,
  ),
).toEqual([[&amp;#39;James Cameron&amp;#39;]]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;Here’s &lt;code&gt;query&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function query({ find, where }, db) {
  const contexts = queryWhere(where, db);
  return contexts.map((context) =&amp;gt; actualize(context, find));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our &lt;code&gt;queryWhere&lt;/code&gt; returns all the successful contexts. We can then map those, and &lt;code&gt;actualize&lt;/code&gt; our &lt;code&gt;find&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function actualize(context, find) {
  return find.map((findPart) =&amp;gt; {
    return isVariable(findPart) ? context[findPart] : findPart;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All &lt;code&gt;actualize&lt;/code&gt; does is handle variables; if we see a variable in find, we just replace it with its bound value. &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h1&gt;Play&lt;/h1&gt;
&lt;p&gt;And voila! We have a query engine. Let’s see what we can do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When was Alien released?&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;query(
  {
    find: [&amp;#39;?year&amp;#39;],
    where: [
      [&amp;#39;?id&amp;#39;, &amp;#39;movie/title&amp;#39;, &amp;#39;Alien&amp;#39;],
      [&amp;#39;?id&amp;#39;, &amp;#39;movie/year&amp;#39;, &amp;#39;?year&amp;#39;],
    ],
  },
  exampleTriples,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[[1979]];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;What do I know about the entity with the id &lt;code&gt;200&lt;/code&gt; ?&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;query(
  {
    find: [&amp;#39;?attr&amp;#39;, &amp;#39;?value&amp;#39;],
    where: [[200, &amp;#39;?attr&amp;#39;, &amp;#39;?value&amp;#39;]],
  },
  exampleTriples,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[
  [&amp;#39;movie/title&amp;#39;, &amp;#39;The Terminator&amp;#39;],
  [&amp;#39;movie/year&amp;#39;, 1984],
  [&amp;#39;movie/director&amp;#39;, 100],
  [&amp;#39;movie/cast&amp;#39;, 101],
  [&amp;#39;movie/cast&amp;#39;, 102],
  [&amp;#39;movie/cast&amp;#39;, 103],
  [&amp;#39;movie/sequel&amp;#39;, 207],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, last by not least…&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which directors shot Arnold for which movies?&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;query(
  {
    find: [&amp;#39;?directorName&amp;#39;, &amp;#39;?movieTitle&amp;#39;],
    where: [
      [&amp;#39;?arnoldId&amp;#39;, &amp;#39;person/name&amp;#39;, &amp;#39;Arnold Schwarzenegger&amp;#39;],
      [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/cast&amp;#39;, &amp;#39;?arnoldId&amp;#39;],
      [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/title&amp;#39;, &amp;#39;?movieTitle&amp;#39;],
      [&amp;#39;?movieId&amp;#39;, &amp;#39;movie/director&amp;#39;, &amp;#39;?directorId&amp;#39;],
      [&amp;#39;?directorId&amp;#39;, &amp;#39;person/name&amp;#39;, &amp;#39;?directorName&amp;#39;],
    ],
  },
  exampleTriples,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;🤯&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[
  [&amp;#39;James Cameron&amp;#39;, &amp;#39;The Terminator&amp;#39;],
  [&amp;#39;John McTiernan&amp;#39;, &amp;#39;Predator&amp;#39;],
  [&amp;#39;Mark L. Lester&amp;#39;, &amp;#39;Commando&amp;#39;],
  [&amp;#39;James Cameron&amp;#39;, &amp;#39;Terminator 2: Judgment Day&amp;#39;],
  [&amp;#39;Jonathan Mostow&amp;#39;, &amp;#39;Terminator 3: Rise of the Machines&amp;#39;],
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now this is cool!&lt;/p&gt;
&lt;h1&gt;Indexes&lt;/h1&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;Okay, but you may have already been thinking, “Our query engine will get slow”.&lt;/p&gt;
&lt;p&gt;Let’s remember &lt;code&gt;querySingle&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function querySingle(pattern, db, context) {
  return db
    .map((triple) =&amp;gt; matchPattern(pattern, triple, context))
    .filter((x) =&amp;gt; x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is fine and dandy, but consider this query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;querySingle([200, &amp;quot;movie/title&amp;quot;, ?movieTitle], db, {})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to find the movie title for the entity with the id &lt;code&gt;200&lt;/code&gt;. SQL would have used an index to quickly nab this for us.&lt;/p&gt;
&lt;p&gt;But what about our query engine? It’ll have to search every single triple in our database!&lt;/p&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;Let’s solve that. We shouldn’t need to search &lt;em&gt;every&lt;/em&gt; triple for a query like this; it’s time for indexes.&lt;/p&gt;
&lt;p&gt;Here’s what we can do; Let’s create &lt;code&gt;entity&lt;/code&gt;, &lt;code&gt;attribute&lt;/code&gt;, and &lt;code&gt;value&lt;/code&gt; indexes. Something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  entityIndex: {
    200: [
      [200, &amp;quot;movie/title&amp;quot;, &amp;quot;The Terminator&amp;quot;], [200, &amp;quot;movie/year&amp;quot;, 1984],
      //...
    ],
    // ...
  },
  attrIndex: {
    &amp;quot;movie/title&amp;quot;: [
      [200, &amp;quot;movie/title&amp;quot;, &amp;quot;The Terminator&amp;quot;],
      [202, &amp;quot;movie/title&amp;quot;, &amp;quot;Predator&amp;quot;],
      // ...
    ],
    // ...
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, if we had a pattern like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[200, &amp;quot;movie/title&amp;quot;, ?movieTitle]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could be smart about how to get all the relevant triples: since &lt;code&gt;200&lt;/code&gt; isn’t a variable, we could just use the &lt;code&gt;entityIndex&lt;/code&gt;. We’d grab &lt;code&gt;entityIndex[200]&lt;/code&gt; , and voila we’d have reduced our search to just 7 triples!&lt;/p&gt;
&lt;p&gt;We can do more, but with this we’d already have a big win.&lt;/p&gt;
&lt;h2&gt;createDB&lt;/h2&gt;
&lt;p&gt;Okay, let’s turn this into reality. We can start with a proper &lt;code&gt;db&lt;/code&gt; object. We were just using &lt;code&gt;exampleTriples&lt;/code&gt; before; now we’ll want to keep track of indexes too. Here’s what we can do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function createDB(triples) {
  return {
    triples,
    entityIndex: indexBy(triples, 0),
    attrIndex: indexBy(triples, 1),
    valueIndex: indexBy(triples, 2),
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’ll take our triples, and start to index them.&lt;/p&gt;
&lt;h2&gt;indexBy&lt;/h2&gt;
&lt;p&gt;And &lt;code&gt;indexBy&lt;/code&gt; will handle that. It can just take the triples and create a mapping:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function indexBy(triples, idx) {
  return triples.reduce((index, triple) =&amp;gt; {
    const k = triple[idx];
    index[k] = index[k] || [];
    index[k].push(triple);
    return index;
  }, {});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here &lt;code&gt;idx&lt;/code&gt; represents the position in the triple; 0 would be &lt;code&gt;entity&lt;/code&gt;, 1 would be &lt;code&gt;attribute&lt;/code&gt;, 2 would be &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;querySingle, updated&lt;/h2&gt;
&lt;p&gt;Now that we have indexes, we can use them in querySingle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function querySingle(pattern, db, context) {
  return relevantTriples(pattern, db)
    .map((triple) =&amp;gt; matchPattern(pattern, triple, context))
    .filter((x) =&amp;gt; x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only change is &lt;code&gt;relevantTriples&lt;/code&gt;. We’ll lean on it to figure out which index to use.&lt;/p&gt;
&lt;h2&gt;relevantTriples&lt;/h2&gt;
&lt;p&gt;Here’s all relevantTriples does:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function relevantTriples(pattern, db) {
  const [id, attribute, value] = pattern;
  if (!isVariable(id)) {
    return db.entityIndex[id];
  }
  if (!isVariable(attribute)) {
    return db.attrIndex[attribute];
  }
  if (!isVariable(value)) {
    return db.valueIndex[value];
  }
  return db.triples;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We take the pattern. We check the id, attribute, and the value. If &lt;em&gt;any&lt;/em&gt; of them aren’t variables, we can safely use the corresponding index.&lt;/p&gt;
&lt;p&gt;With that, we’ve made our query engine faster 🙂&lt;/p&gt;
&lt;h1&gt;Fin&lt;/h1&gt;
&lt;p&gt;I hope you had a blast making this and got a sense of how query engines work to boot. If you’d like to see the source in one place, &lt;a href=&quot;https://github.com/stopachka/datalogJS/blob/main/src/index.js&quot;&gt;here it is&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;More&lt;/h2&gt;
&lt;p&gt;This is just the beginning. How about functions like “greater than” or “smaller than”? How about an “or” query? Let’s not forget aggregate functions. If you’re curious about this, I’d suggest three things:&lt;/p&gt;
&lt;p&gt;First go through the &lt;a href=&quot;http://www.learndatalogtoday.org/&quot;&gt;Learn Datalog&lt;/a&gt; website; that’ll give you a full overview Datalog. Next, I’d suggest you go through the &lt;a href=&quot;https://sarabander.github.io/sicp/html/4_002e4.xhtml#g_t4_002e4&quot;&gt;SICP chapter on logic programming&lt;/a&gt;. They go much further than this essay. Finally, you can look at Nikita Tonsky’s &lt;a href=&quot;https://tonsky.me/blog/datascript-internals/&quot;&gt;datascript internals&lt;/a&gt;, for what a true production version could look like.&lt;/p&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;p&gt;Huge credit goes to SICP. When I completed their logic chapter, I realized that query languages didn&amp;#39;t have to be so daunting. This essay is just a simplification of their chapter, translated into Javascript. The second credit needs to go to Nikita Tonsky’s essays. His &lt;a href=&quot;https://tonsky.me/blog/unofficial-guide-to-datomic-internals/&quot;&gt;Datomic&lt;/a&gt; and &lt;a href=&quot;https://tonsky.me/blog/datascript-internals/&quot;&gt;Datascript&lt;/a&gt; internals essays are a goldmine. Finally, I really enjoyed &lt;a href=&quot;http://www.learndatalogtoday.org/&quot;&gt;Learn Datalog&lt;/a&gt;, and used their dataset for this essay.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=31154039&quot;&gt;Discussion on HN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to Joe Averbukh, Irakli Safareli, Daniel Woelfel, Mark Shlick, Alex Reichert, Ian Sinnott, for reviewing drafts of this essay.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;: ​​&lt;a href=&quot;http://www.learndatalogtoday.org/&quot;&gt;Learn Datalog Today&lt;/a&gt; — very fun!&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;: You may be wondering, won’t &lt;code&gt;find&lt;/code&gt; always have variables? Well, not always. You could include some constant, like &lt;code&gt;{find: [&amp;quot;movie/title&amp;quot;, &amp;quot;?title&amp;quot;]}&lt;/code&gt;&lt;/p&gt;
</description>
      <pubDate>Mon, 25 Apr 2022 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
    <item>
      <title>Database in the Browser, a Spec</title>
      <link>https://instantdb.com/essays/db_browser</link>
      <guid isPermaLink="true">https://instantdb.com/essays/db_browser</guid>
      <description>&lt;p&gt;How will we build web applications in the future?&lt;/p&gt;
&lt;p&gt;If progress follows it&amp;#39;s usual strategy, then whatever is difficult and valuable to do today will become easy and normal tomorrow. I imagine we&amp;#39;ll discover new abstractions, which will make writing Google Docs as easy as the average web app is today.&lt;/p&gt;
&lt;p&gt;This begs the question — what will those abstractions look like? Can we discover them today? One way to find out, is to look at all the schleps we have to go through in building web applications, and see what we can do about it.&lt;/p&gt;
&lt;p&gt;Dear reader, this essay is my attempt to follow that plan. We’ll take a tour of what it&amp;#39;s like to build a web application today: we&amp;#39;ll go over the problems we face, assess solutions like Firebase, Supabase, Hasura and friends, and see what&amp;#39;s left to do. I think by the end, you&amp;#39;ll agree with me that one of the most useful abstractions looks like a database in the browser. I&amp;#39;m getting ahead of myself though, let&amp;#39;s start at the beginning:&lt;/p&gt;
&lt;h1&gt;Client&lt;/h1&gt;
&lt;p&gt;The journey begins with Javascript in the browser&lt;/p&gt;
&lt;h2&gt;A. Data Plumbing&lt;/h2&gt;
&lt;p&gt;The first job we have is to fetch information and display it in different places. For example, we may display a friends list, a friends count, a modal with a specific group of friends, etc&lt;/p&gt;
&lt;p&gt;The problem we face, is that all components need to see consistent information. If one component sees different data for friends, it’s possible that you’ll get the wrong &amp;quot;count&amp;quot; showing up, or a different nickname in one view versus another.&lt;/p&gt;
&lt;p&gt;To solve for this, we need to have a central source of truth. So, whenever we fetch anything, we normalize it and plop it in one place (often a &lt;em&gt;store&lt;/em&gt;). Then, each component reads and transforms the data it needs (using a &lt;em&gt;selector&lt;/em&gt;), It’s not uncommon to see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// normalise [posts] -&amp;gt; {[id]: post}
fetchRelevantPostsFor(user).then((posts) =&amp;gt; {
  posts.forEach((post) =&amp;gt; {
    store.addPost(post);
  });
});

// see all posts by author:
store.posts.values().reduce((res, post) =&amp;gt; {
  res[post.authorId] = res[post.authorId] || [];
  res[post.authorId].push(post);
  return res;
}, {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The question here is, &lt;em&gt;why&lt;/em&gt; should we need to do all this work? We write custom code to massage this data, while databases have solved this problem for a long time now. We should be able to &lt;em&gt;query&lt;/em&gt; for our data. Why can’t we just do:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-SQL&quot;&gt;SELECT posts WHERE post.author_id = ?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;on the information that we have &lt;em&gt;inside&lt;/em&gt; the browser?&lt;/p&gt;
&lt;h2&gt;B. Change&lt;/h2&gt;
&lt;p&gt;The next problem is keeping data up to date. Say we remove a friend — what should happen?&lt;/p&gt;
&lt;p&gt;We send an API request, wait for it to complete, and write some logic to &amp;quot;remove&amp;quot; all the information we have about that friend. Something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;deleteFriend(user, friend.id).then((res) =&amp;gt; {
  userStore.remove(friend.id);
  postStore.removeUserPosts(friend.id);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, this can get hairy to deal with quick: we have to remember every place in our store that could possibly be affected by this change. It’s like playing garbage collector in our heads. Our heads are not good at this.&lt;/p&gt;
&lt;p&gt;One way folks avoid it, is to skip the problem and just re-fetch the whole world:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;deleteFriend(user, id).then((res) =&amp;gt; {
  fetchFriends(user);
  fetchPostsRelevantToTheUser(user);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Neither solutions are very good. In both cases, there are implicit invariants we need to be aware of (based on this change, what other changes do we need to be aware of?) and we introduce lag in our application.&lt;/p&gt;
&lt;p&gt;The rub is, whenever we make a change to the database, it does it’s job without us having to be so prescriptive. Why can’t this just happen automatically for us in the browser?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-SQL&quot;&gt;DELETE FROM friendships WHERE friend_one_id = ? AND friend_two_id = ?
-- Browser magically updates with all the friend and post information removed
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C. Optimistic Updates&lt;/h2&gt;
&lt;p&gt;The problem you may have noticed with B., was that we had to &lt;em&gt;wait&lt;/em&gt; for friendship removal to update our browser state.&lt;/p&gt;
&lt;p&gt;In most cases, we can make the experience snappier with an optimistic update — after all, we know that the call will likely be a success. To do this, we do something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;friendPosts = userStore.getFriendPosts(friend);
userStore.remove(friend.id);
postStore.removeUserPosts(friend.id);
deleteFriend(user, id).catch((e) =&amp;gt; {
  // undo
  userStore.addFriend(friend);
  postStore.addPosts(friendPosts);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is even more annoying. Now we need to manually update the success operation, &lt;em&gt;and&lt;/em&gt; the failure operation.&lt;/p&gt;
&lt;p&gt;Why is that? On the backend, a database is able to do optimistic updates &lt;sup id=&quot;user-content-fnref-1&quot;&gt;&lt;a href=&quot;#user-content-fn-1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; — why can’t we do that in the browser?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-SQL&quot;&gt;DELETE friendship WHERE friend_one_id = ? AND friend_two_id = ?
-- local store optimistically updated, if operation fails we undo
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;D. Reactivity&lt;/h2&gt;
&lt;p&gt;And data doesn’t just change from our own actions. Sometimes we need to connect to changes that other users make. For example, someone could unfriend us, or someone could send us a message.&lt;/p&gt;
&lt;p&gt;To make this work, we need to do the same work that we did in our API endpoints, but this time on our websocket connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;ws.listen(`${user.id}/friends-removed`, friend =&amp;gt; {
  userStore.remove(friend.id);
  postStore.removeUserPosts(friend.id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, this introduces two problems. First, we need to play garbage collector again, and remember every place that could be affected by an event.&lt;/p&gt;
&lt;p&gt;Second, if we do optimistic updates, we have race conditions. Imagine you run an optimistic update, setting the color of a shape to &lt;code&gt;blue&lt;/code&gt;, while a stale reactive update comes in, saying it’s &lt;code&gt;red&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Optimistic Update: `Blue`
2. Stale reactive update: `Red`
3. Successful Update, comes in through socket: `Blue`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you’ll see a flicker. The optimistic update will come in to &lt;code&gt;blue&lt;/code&gt;, a reactive update will change it to &lt;code&gt;red&lt;/code&gt;, but once the optimistic update succeeds, a new reactive update will turn it back to blue again. &lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Solving stuff like this has you dealing with consistency issues, scouring literature on…databases.&lt;/p&gt;
&lt;p&gt;It doesn’t have to be that way though. What if each query was &lt;em&gt;reactive&lt;/em&gt;?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-SQL&quot;&gt;SELECT friends.* FROM users as friends JOIN friendships on friendship.user_one_id ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, any change in friendships would automatically update the view subscribed to this query. You wouldn’t have to manage what changes, and your local database could figure out what the &amp;quot;most recent update&amp;quot; is, removing much of the complexity.&lt;/p&gt;
&lt;h1&gt;Server&lt;/h1&gt;
&lt;p&gt;It only gets harder on the server.&lt;/p&gt;
&lt;h2&gt;E. Endpoints&lt;/h2&gt;
&lt;p&gt;Much of backend development ends up being a sort of glue between the database and the frontend.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// db.js
function getRelevantPostsFor(userId) {
  db.exec(&amp;#39;SELECT * FROM posts WHERE ...&amp;#39;);
}

// api.js
app.get(&amp;#39;relevantPosts&amp;#39;, (req, res) =&amp;gt; {
  res.status(200).send(getRelevantPosts(req.userId));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is so repetitive that we end up creating scripts to generate these files. But why do we need to do this at all? They are often coupled very closely to the client anyways. Why can’t we just expose the database to the client?&lt;/p&gt;
&lt;h2&gt;F. Permissions&lt;/h2&gt;
&lt;p&gt;Well, the reason we don’t, is because we need to make sure permissions are correctly set. You should only see posts by your friends, for example. To do this, we add middleware to our API endpoints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.put(&amp;quot;user&amp;quot;, auth, (req, res) =&amp;gt; {
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But, this ends up getting more and more confusing. What about websockets? New code changes sometimes introduce ways to update database objects that you didn’t expect. All of a sudden, you’re in trouble.&lt;/p&gt;
&lt;p&gt;The question to ask here, is why is authentication at the API level? Ideally, we should have something &lt;em&gt;very close&lt;/em&gt; to the database, making sure any data access passes permission checks. There’s row-level security on databases like Postgres, but that can get hairy quick &lt;sup id=&quot;user-content-fnref-3&quot;&gt;&lt;a href=&quot;#user-content-fn-3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;. What if you could &amp;quot;describe&amp;quot; entities near the database?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;User {
  view: [
    IAllowIfAdmin(),
    IAllowIfFriend(),
    IAllowIfSameUser(),
  ]
  write: [
    IAllowIfAdmin(),
    IAllowIfSameUser(),
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we compose authentication rules, and make sure that &lt;em&gt;any&lt;/em&gt; way you try to write too and update a user entity, you are guaranteed to that you are permitted. All of a sudden, instead of most code changes affecting permissions, only a few do.&lt;/p&gt;
&lt;h2&gt;G. Audits, Undo / Redo&lt;/h2&gt;
&lt;p&gt;And at some point, we get requirements that blow up complexity for us.&lt;/p&gt;
&lt;p&gt;For example, say we need to support &amp;quot;undo / redo&amp;quot;, for friendship actions. A user deletes a friend, and then they press &amp;quot;undo&amp;quot; — how could we support this?&lt;/p&gt;
&lt;p&gt;We can’t just delete the friendship relation, because if we did, then we wouldn’t know if this person was &amp;quot;already friends&amp;quot;, or was just asking now to become friends. In the latter case we may need to send a friend request.&lt;/p&gt;
&lt;p&gt;To solve this, we’d evolve our data model. Instead of a single friendship relation, we’d have &amp;quot;friendship facts&amp;quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;[
  { status: &amp;#39;friends&amp;#39;, friend_one_id: 1, friend_two_id: 2, at: 1000 },
  { status: &amp;#39;disconnected&amp;#39;, friend_one_id: 1, friend_two_id: 2, at: 10001 },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then the &amp;quot;latest fact&amp;quot; would represent whether there is a friendship or not.&lt;/p&gt;
&lt;p&gt;This works, but most databases weren’t designed for it: the queries don’t work as we expect, optimizations are harder than we expect. We end up having to be &lt;em&gt;very&lt;/em&gt; careful about how we do updates, in case we end up accidentally deleting records.&lt;/p&gt;
&lt;p&gt;All of a sudden, we become &amp;quot;sort of database engineers&amp;quot;, devouring literature on query optimization.&lt;/p&gt;
&lt;p&gt;This kind of requirement seems unique, but it’s getting more common. If you deal with financial transactions, you need something like this for auditing purposes. Undo / Redo is a necessity in lots of apps.&lt;/p&gt;
&lt;p&gt;And god forbid an error happens and we accidentally delete data. In a world of facts there would be no such thing — you can just undo the deletions. But alas, this is not the world most of us live in.&lt;/p&gt;
&lt;p&gt;There &lt;em&gt;are&lt;/em&gt; models that treat facts as a first class citizen (Datomic, which we’ll talk about soon), but right now they’re so foreign that it’s rarely what engineers reach too. What if it wasn&amp;#39;t so foreign?&lt;/p&gt;
&lt;h2&gt;H. Offline Mode&lt;/h2&gt;
&lt;p&gt;There’s more examples of difficulty. What about offline mode? Many apps are long-running and can go for periods without internet connection. How can we support this?&lt;/p&gt;
&lt;p&gt;We would have to evolve our data model again, but this time &lt;em&gt;really&lt;/em&gt; keep just about everything as a &amp;quot;fact&amp;quot;, and have a client-side database that evolve it’s internal state based on them. Once a connection is made, we should be able to reconcile changes.&lt;/p&gt;
&lt;p&gt;This gets extremely hard to do. In essence, anyone who implements this becomes a database engineer full-stop. But, if we had a database in the browser, and it acted like a &amp;quot;node&amp;quot; in a distributed database, wouldn’t this just happen automatically for us?&lt;/p&gt;
&lt;p&gt;Turns out, fact-based systems in fact make this much, much easier. Many think we need to resort to operational transforms to do stuff like this, but as figma showed, as long as we’re okay with having a single leader, and are fine with last-write-wins kind of semantics, we can drastically simplify this and just facts are enough. When time for even more serious resolution comes, you can open up the OT rabbit hole.&lt;/p&gt;
&lt;p&gt;Imagine…offline mode off the bat. What would the most applications feel like after this?&lt;/p&gt;
&lt;h2&gt;I. Reactivity&lt;/h2&gt;
&lt;p&gt;We talked about reactivity from the client. On the server it’s worrying too. We have to ensure that &lt;em&gt;all&lt;/em&gt; the relevant clients are updated when data changes. For example, if a &amp;quot;post&amp;quot; is added, we &lt;em&gt;need&lt;/em&gt; to make sure that all possible subscriptions related to this post are notified.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function addPost(post) {
  db.addPost(post);
  getAllFriends(post).forEach(notifyNewPost);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can get hairy. It’s hard to know &lt;em&gt;all&lt;/em&gt; the topics that could be related. It could also be easy to miss: if a database is updated with a query outside of &lt;code&gt;addPost&lt;/code&gt;, we’d never know. This work is up to the developer to figure out. It starts off easy, but gets ever more complex.&lt;/p&gt;
&lt;p&gt;Yet, the database &lt;em&gt;could&lt;/em&gt; be aware of all these subscriptions too, and &lt;em&gt;could&lt;/em&gt; just handle updating the relevant queries. But most don’t. RethinkDB is the shining example that did this well. What if this was possible with the query language of your choice?&lt;/p&gt;
&lt;h2&gt;J. Derived Data&lt;/h2&gt;
&lt;p&gt;Eventually, we end up needing to put our data in different places: either caches (Redis), search indexes (ElasticSearch), or analytics engines (Hive). Doing this becomes pretty daunting. You may need to introduce some sort of a queue (Kafka), so all of these derived sources are kept up to date. Much of this involves provisioning machines, introducing service discovery, and the whole shebang.&lt;/p&gt;
&lt;p&gt;Why is this so complicated though? In a normal database you can do something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-SQL&quot;&gt;CREATE INDEX ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why can’t we do that, for other services? Martin Kleppman, in his Data Intensive Applications, suggests a language like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;db |&amp;gt; ElasticSearch;
db |&amp;gt; Analytics;
db.user |&amp;gt; Redis;
// Bam, we&amp;#39;ve connected elastic search, analytics, and redis to our db
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Monkey Wrenches&lt;/h1&gt;
&lt;p&gt;Wow, we’ve gone up to &lt;strong&gt;J.&lt;/strong&gt; But these are only issues you start to face once you start building your application. What about before?&lt;/p&gt;
&lt;h2&gt;K. TTP — Time to Prototype&lt;/h2&gt;
&lt;p&gt;Perhaps the most restrictive problem for developers today is how hard it is to get started. If you want to store user information and display a page, what do you do?&lt;/p&gt;
&lt;p&gt;Before, it was a matter of &lt;code&gt;index.html&lt;/code&gt; and FTP. Now, it’s webpack, typescript, build processes galore, often multiple services. There are so many moving pieces that it’s hard to take a step.&lt;/p&gt;
&lt;p&gt;This can seem like a problem only inexperienced people need to contend with, and if they just spent some time they’ll get faster. I think it’s more important than that. Most projects live on the fringe — they aren’t stuff you do as a day job. This means that even a few minutes delay in prototyping could kill a magnitude more projects.&lt;/p&gt;
&lt;p&gt;Making this step easier would dramatically increase the number of applications we get to use. What if it was &lt;em&gt;easier&lt;/em&gt; than &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;FTP&lt;/code&gt;?&lt;/p&gt;
&lt;h1&gt;Current Solutions&lt;/h1&gt;
&lt;p&gt;Wow, that’s a lot of problems. It may seem bleak, but if you just look a few years back, it’s surprising how much has improved. After all, we don’t need to roll our own racks anymore. Many great folks are working on solutions to these problems. What are some of them?&lt;/p&gt;
&lt;h2&gt;1) Firebase&lt;/h2&gt;
&lt;p&gt;I think Firebase has done some of the most innovative work in moving web application development forward. The most important thing they got right, was a &lt;strong&gt;database on the browser.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With firebase, you query your data the same way you would on the server. By creating this abstraction, they solved &lt;strong&gt;A-E.&lt;/strong&gt; Firebase handles optimistic updates, and is reactive by default. It obviates the need for endpoints by providing support for permissions.&lt;/p&gt;
&lt;p&gt;They’re strength also stems for &lt;strong&gt;K:&lt;/strong&gt; I think it still has the &lt;em&gt;best&lt;/em&gt; time-to-prototype in the market. You can just start with index.html!&lt;/p&gt;
&lt;p&gt;However, it has two problems:&lt;/p&gt;
&lt;p&gt;First, query strength. Firebase’s choice of a document model makes the abstraction simpler to manage, but it destroys your query capability. Very often you’ll fall into a place where you have to de-normalize data, or querying for it becomes tricky. For example, to record a many-to-many relationship like a friendship, you’d need to do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;userA: friends: userBId: true;
userB: friends: userAId: true;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You de-normalize friendships across two different paths (userA/friends/userBId) and (userB/friends/userAId). Grabbing the full data requires you to manually replicate a join:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;1. get `userA/friends`
2. for each id, get `/${id}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These kind of relationships sprout up very quickly in your application. It would be great if a solution helped you handle it.&lt;/p&gt;
&lt;p&gt;Second, permissions. Firebase lets you write permissions using a limited language. In practice, these rules get hairy quickly — to the point that folks resort to writing some higher-level language themselves and compiling down to Firebase rules.&lt;/p&gt;
&lt;p&gt;We experimented a lot on this at Facebook, and came to the conclusion that you need a &lt;em&gt;real language&lt;/em&gt; to express permissions. If Firebase had that, it would be much more powerful.&lt;/p&gt;
&lt;p&gt;With the remaining items (audits, Undo / Redo, Derived Data) — Firebase hasn’t tackled them yet.&lt;/p&gt;
&lt;h2&gt;2) Supabase&lt;/h2&gt;
&lt;p&gt;Supabase is trying to do what Firebase did for Mongo, but for Postgres. If they did this, it would be quite an attractive option, as it would solve Firebase’s biggest problem: query strength.&lt;/p&gt;
&lt;p&gt;Supabase has some great wins so far. Their auth abstraction is great, which makes it one of the few platforms that are as easy to get started with as firebase was.&lt;/p&gt;
&lt;p&gt;Their realtime option allows you to subscribe to row-level updates. For example, if we wanted to know whenever a friendship gets created, updated, or changed, we could write this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const friendsChange = supabase
  .from(&amp;#39;friendships:friend_one_id=eq.200&amp;#39;)
  .on(&amp;#39;*&amp;#39;, handleFriendshipChange)
  .subscribe();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This in practice can get you far. It can get hairy though. For example, if a friend is created, we may not have the user information and we’d have to fetch it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function handleFriendshipChange(friendship) {
  if (!userStore.get(friendship.friend_two_id)) {
      fetchUser(...)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This points to Supabase’s main weakness: it doesn’t have a &amp;quot;database on the browser&amp;quot; abstraction. Though you can make queries, you are responsible for normalizing and massaging data. This means that they can’t do optimistic updates automatically, reactive queries, etc.&lt;/p&gt;
&lt;p&gt;Their permission model is also similar to Firebase, in that they defer to Postgres’ row-level security. This can be great to start out, like Firebase gets hairy quickly. Often these rules can slow down the query optimizer, and the SQL itself gets harder and harder to reason about.&lt;/p&gt;
&lt;h2&gt;3) GraphQL + Hasura&lt;/h2&gt;
&lt;p&gt;GraphQL is an excellent way to declaratively define data you want from the client. services like Hasura can take a database like Postgres, and do smart things like give you a GraphQL API out of it.&lt;/p&gt;
&lt;p&gt;Hasura is very compelling for reads. They do a smart job of figuring joins, and can get you a good view for your data. With a flip, you can turn any query into a subscription. When I first tried turning a query into a subscription, it certainly felt magical.&lt;/p&gt;
&lt;p&gt;The big issue today with GraphQL tools in general, is their time-to-prototype. You often need multiple different libraries and build steps. Their write-story is less compelling too. Optimistic Updates don’t just happen automatically — you have to bust caches yourself.&lt;/p&gt;
&lt;h2&gt;Lay of the Land&lt;/h2&gt;
&lt;p&gt;We’ve looked at the three most promising solutions. Right now, Firebase solves the most problems off the bat. Supabase gives you query strength at the expense of more client-side support. Hasura gives you more powerful subscriptions and more powerful local state, at the expense of time-to-prototype. As far as I can see, none are handling conflict resolution, undo / redo, powerful reactive queries on the client yet.&lt;/p&gt;
&lt;h1&gt;Future&lt;/h1&gt;
&lt;p&gt;Now the question: what will the evolution of these tools look like?&lt;/p&gt;
&lt;p&gt;In some ways, the future is happening now. I think Figma, for example, is an app from the future: it handles handle offline-mode, undo / redo and multiplayer beautifully.&lt;/p&gt;
&lt;p&gt;If we wanted to make an app like that, what would an ideal abstraction for data look like?&lt;/p&gt;
&lt;h2&gt;Requirements&lt;/h2&gt;
&lt;h3&gt;1) A database on the client, with a &lt;em&gt;powerful&lt;/em&gt; query language&lt;/h3&gt;
&lt;p&gt;From the browser, this abstraction would have to be like firebase, &lt;em&gt;but with a strong query language.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You should be able to query your local data, and it should be as powerful as SQL. Your queries should be reactive, and update automatically if there are changes. It should handle optimistic updates for you too.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;user = useQuery(&amp;#39;SELECT * FROM users WHERE id = ?&amp;#39;, 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) A real permission language&lt;/h3&gt;
&lt;p&gt;Next up, we’d need a composable permission language. FB’s EntFramework is the example I keep going back too, because of how powerful it was. We should be able to define rules on entities, and should just be guaranteed that we won’t accidentally see something we’re not allowed to see.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;User {
  view: [
    IAllowIfAdmin(),
    IAllowIfFriend(),
    IAllowIfSameUser(),
  ]
  write: [
    IAllowIfAdmin(),
    IAllowIfFriend(),
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3) Offline Mode &amp;amp; Undo / Redo&lt;/h3&gt;
&lt;p&gt;Finally, this abstraction should make it easy for us to implement offline mode, or undo redo. If a local write happens, and there’s a conflicting write on the server, there should be a reconciler which does the right thing most of the time. If there are issues, we should be able to nudge it along in the right direction.&lt;/p&gt;
&lt;p&gt;Whatever abstraction we choose, it should give us the ability to run writes while we’re offline.&lt;/p&gt;
&lt;h3&gt;4) The Next Cloud&lt;/h3&gt;
&lt;p&gt;Finally, we should be able to express data dependencies without having to spin anything up. With a simple&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;db.user |&amp;gt; Redis;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;all queries to users would magically be cached by Redis.&lt;/p&gt;
&lt;h2&gt;Sketch of an Implementation&lt;/h2&gt;
&lt;p&gt;Okay, those requirements sound magical. What would an implementation look like today?&lt;/p&gt;
&lt;h3&gt;Datomic &amp;amp; Datascript&lt;/h3&gt;
&lt;p&gt;In the Clojure world, folks have long been fans of Datomic, a facts-based database that lets you &amp;quot;see every change over time&amp;quot;. Nikita Tonsky also implemented datascript, &lt;em&gt;a client-side database and query engine&lt;/em&gt; with the same semantics as Datomic!&lt;/p&gt;
&lt;p&gt;They’ve been used to build offline-enabled applications like Roam, or collaborative applications like Precursor. If we were to package up a Datomic-like database on the backend, and datascript-like database on the frontend, it &lt;em&gt;could&lt;/em&gt; become &amp;quot;database on the client with a powerful query language&amp;quot;!&lt;/p&gt;
&lt;h3&gt;Reactivity&lt;/h3&gt;
&lt;p&gt;Datomic makes it easy for you to subscribe to new committed facts to the database. What if we made a service on top if, which kept queries and listened to these facts. From a change, we would update the relevant query. All of a sudden, our database becomes realtime!&lt;/p&gt;
&lt;h3&gt;Permission Language&lt;/h3&gt;
&lt;p&gt;Our server could accept code fragments, which it runs when fetching data. These fragments would be responsible for permissions, giving us a powerful permission language!&lt;/p&gt;
&lt;h3&gt;Pipe&lt;/h3&gt;
&lt;p&gt;Finally, we can write up some DSL, which lets you pipe data to Elastic Search, Redis, etc, all according to the user’s preferences.&lt;/p&gt;
&lt;p&gt;With that, we have a compelling offering.&lt;/p&gt;
&lt;h2&gt;Considerations&lt;/h2&gt;
&lt;p&gt;So, why doesn’t this exist yet? Well...&lt;/p&gt;
&lt;h3&gt;Datalog is unfamiliar&lt;/h3&gt;
&lt;p&gt;If we were to use a Datomic-like database, we wouldn’t use SQL anymore. Datomic uses a logic-based query language called Datalog. Now, it is just as, if not more, powerful than SQL. The only gotcha is that for the uninitiated it looks very daunting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-clojure&quot;&gt;[:find [(pull ?c [:conversation/user :conversation/message]) ...]
 :where [?e :session/thread ?thread-id]
        [?c :conversation/thread ?thread-id]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This query would find all messages, alongside with the user information, for the active thread in this current &amp;quot;session&amp;quot;. Not bad!&lt;/p&gt;
&lt;p&gt;Once you get to know it, it’s an unbelievably elegant language. However, I don’t think that’s enough. Time-to-prototype needs to be blazing fast, and having to learn this may be too much.&lt;/p&gt;
&lt;p&gt;There have been some fun experiments in making this easier. Dennis Heihoff tried &lt;a href=&quot;https://twitter.com/denik/status/1290415892367540227&quot;&gt;using natural language&lt;/a&gt; for example. This points to an interesting solution: Could we write a slightly more verbose, but more natural query language that compiles to Datalog? I think so.&lt;/p&gt;
&lt;p&gt;The other problem, is that data modeling is also different from what people are used too. Firebase is the gold-standard, where you can write your first mutation without specifying any schema.&lt;/p&gt;
&lt;p&gt;Though it will be hard, I think we should aim to be as close to &amp;quot;easy&amp;quot; as possible. Datascript only requires you to indicate references and multi-valued attributes. Datomic requires a schema, but perhaps if we used an open-source, datalog-based database, we could enhance it to do something similar. Either as little schema as possible, or a &amp;quot;magically detectable schema&amp;quot;.&lt;/p&gt;
&lt;h3&gt;Datalog would be hard to make reactive&lt;/h3&gt;
&lt;p&gt;A big problem with both SQL and Datalog, is that based on some new change, it’s hard to figure out &lt;em&gt;which&lt;/em&gt; queries need to be updated.&lt;/p&gt;
&lt;p&gt;I don’t think it’s impossible though. Hasura does polling and it scaled &lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;. We &lt;em&gt;could&lt;/em&gt; try having a specific language for subscriptions as well, similar to Supabase. If we can prove certain queries can only change by some subset of facts, we can move them out of polling.&lt;/p&gt;
&lt;p&gt;This is a hard problem, but I think it’s a tractable one.&lt;/p&gt;
&lt;h3&gt;A permission language would slow things down&lt;/h3&gt;
&lt;p&gt;One problem with making permission checks a full-blown language, is that we’re liable to overfetch data.&lt;/p&gt;
&lt;p&gt;I think this is a valid concern, but with a database like Datomic, we could handle it. Reads are easy to scale and cache. Because everything’s a fact, we could create an interface that guides people to only fetch the values they need.&lt;/p&gt;
&lt;p&gt;Facebook was able to do it. It will be hard, but it’s possible.&lt;/p&gt;
&lt;h3&gt;It may be too large of an abstraction&lt;/h3&gt;
&lt;p&gt;Frameworks often fail to generalize. For example, what if we wanted to share mouse position? This is ephemeral state and doesn’t fit in a database, but we do need to make it realtime — where would we keep it? There’s a lot of these-kinds-of-things that are going to pop up if you build an abstraction like this, and you’re likely to get it wrong.&lt;/p&gt;
&lt;p&gt;I do think this is a problem. If someone were to tackle this, the best bet would be to go the Rails approach: Build a production app using it, and extract the internals out as a product. I think they’d have a good shot at finding the right abstraction.&lt;/p&gt;
&lt;h3&gt;It will only be used for toys&lt;/h3&gt;
&lt;p&gt;The common issue with these kind of products, is that people will only use them for hobby projects, and there won’t be a lot of money in it. I think Heroku and Firebase point to a bright future here.&lt;/p&gt;
&lt;p&gt;Large companies start as side-projects. Older engineers may look at Firebase like a toy, but many a successful startup now runs on it. Instead of being a just a database, perhaps it’ll become a whole new platform — the successor to AWS.&lt;/p&gt;
&lt;h3&gt;The Market is very competitive&lt;/h3&gt;
&lt;p&gt;The market is competitive and the users are fickle. Slava’s &lt;a href=&quot;https://www.defmacro.org/2017/01/18/why-rethinkdb-failed.html&quot;&gt;Why RethinkDB Failed&lt;/a&gt; paints a picture for how hard it is to win in the developer tools market. I don’t think he is wrong. Doing this would require a compelling answer to how you’ll build a moat, and expand towards &lt;em&gt;The Next AWS&lt;/em&gt;.&lt;/p&gt;
&lt;h1&gt;Fin&lt;/h1&gt;
&lt;p&gt;Well, we covered the pains, covered the competitors, covered an ideal solution, and went through the considerations. Thank you for walking with me on this journey!&lt;/p&gt;
&lt;h2&gt;Like-Minded Folks&lt;/h2&gt;
&lt;p&gt;These ideas are not new. My friends Sean Grove and Daniel Woelfel’s built &lt;a href=&quot;https://www.youtube.com/watch?v=BiplJ4AFwCc&quot;&gt;Dato&lt;/a&gt;, a framework that integrated a bunch of these ideas. Nikita Tonsky wrote &lt;a href=&quot;https://tonsky.me/blog/the-web-after-tomorrow/&quot;&gt;Web After Tomorrow&lt;/a&gt; an essay with a very similar spirit.&lt;/p&gt;
&lt;p&gt;It may require some iteration to figure out the interface, but the there’s an interesting road ahead.&lt;/p&gt;
&lt;h2&gt;Next Up&lt;/h2&gt;
&lt;p&gt;I’m toying with some ideas in this direction. The big problem to solve here, is how important this is for people, and whether a good abstraction can work. To solve the first, I wrote this essay. Is this a hair-on-fire problem that you’re facing? If it is, to the point that you’re actively looking for solutions, please reach out to me on &lt;a href=&quot;https://twitter.com/stopachka&quot;&gt;Twitter&lt;/a&gt;! I’d love to learn your use case 🙂. As I create applications, I’ll certainly keep this back of mind — who knows, maybe a good abstraction can be pulled out.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks Joe Averbukh, Sean Grove, Ian Sinnott, Daniel Woelfel, Dennis Heihoff, Mark Shlick, Alex Reichert, Alex Kotliarskyi, Thomas Schranz, for reviewing drafts of this essay&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-1&quot; href=&quot;#user-content-fnref-1&quot;&gt;[1]&lt;/a&gt;  You may not notice this as Postgres gives a consistency guarantee. However, for them to support multiple concurrent transactions, they in effect need to be able to keep &amp;quot;temporary alterations&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-2&quot;&gt;&lt;a href=&quot;#user-content-fn-2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;: Figma mentions this problem in &lt;a href=&quot;https://www.figma.com/blog/how-figmas-multiplayer-technology-works/&quot;&gt;their multiplayer essay&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id=&quot;user-content-fn-3&quot; href=&quot;#user-content-fnref-3&quot;&gt;[3]&lt;/a&gt;  Plain SQL and boolean logic is hard to reuse, and can slow down the query planner. Many folks who have medium-sized apps experience this quickly.&lt;/p&gt;
&lt;p&gt;&lt;sup id=&quot;user-content-fnref-4&quot;&gt;&lt;a href=&quot;#user-content-fn-4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;: Take a look at Hasura’s &lt;a href=&quot;https://github.com/hasura/graphql-engine/blob/master/architecture/live-queries.md&quot;&gt;notes&lt;/a&gt;&lt;/p&gt;
</description>
      <pubDate>Thu, 29 Apr 2021 00:00:00 GMT</pubDate>
      <author>Stepan Parunashvili</author>
    </item>
  </channel>
</rss>