<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max S.</title>
    <description>The latest articles on DEV Community by Max S. (@max4nhost).</description>
    <link>https://dev.to/max4nhost</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3968007%2Ffbdd8440-4254-4e4a-a5b0-e90ae4ff72b9.jpg</url>
      <title>DEV Community: Max S.</title>
      <link>https://dev.to/max4nhost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/max4nhost"/>
    <language>en</language>
    <item>
      <title>AI Agent Memory: Most of It Should Be a Database Table</title>
      <dc:creator>Max S.</dc:creator>
      <pubDate>Fri, 30 Oct 2026 11:07:20 +0000</pubDate>
      <link>https://dev.to/max4nhost/ai-agent-memory-most-of-it-should-be-a-database-table-32h9</link>
      <guid>https://dev.to/max4nhost/ai-agent-memory-most-of-it-should-be-a-database-table-32h9</guid>
      <description>&lt;p&gt;I don't trust AI agent memory. It keeps facts that stopped being true and can't tell which version is current.&lt;/p&gt;

&lt;p&gt;Every memory demo I've seen goes the same way. You tell the agent your favorite color, it saves "User's favorite color is blue" somewhere, and a week later it proudly tells you your favorite color is blue. Cool. Now change your favorite color.&lt;/p&gt;

&lt;p&gt;The agent ends up with two memories that disagree, a vector search that returns both, and a model that has to guess which one is still true. Run that for a few months and you have a pile of half-true sentences about your users that nobody can clean up.&lt;/p&gt;

&lt;p&gt;My take: anything that can be declarative should be declarative. If a fact has one correct value, it goes in a column. The agent is an operator. It reads and updates data. It shouldn't be a warehouse for everything a user ever said.&lt;/p&gt;

&lt;p&gt;I wanted to see if that holds up outside my own head, so I built a small demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Store facts about users (diet, city, plan, language) as plain table columns that the agent reads and updates. Keep vector memory for stuff that has no structure, like notes and past conversations. Scope both to the user in the database, so the agent can't recall what someone else told it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AI agent memory?
&lt;/h2&gt;

&lt;p&gt;AI agent memory is whatever an agent carries over from earlier interactions. Most guides split it in two: short-term memory is the current conversation sitting in the context window, and long-term memory is whatever survives between sessions.&lt;/p&gt;

&lt;p&gt;Long-term memory usually gets three labels borrowed from cognitive science. Here's the standard list, plus one column most guides skip:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Where I'd store it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short-term (working)&lt;/td&gt;
&lt;td&gt;The current conversation&lt;/td&gt;
&lt;td&gt;"Make that function async"&lt;/td&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic&lt;/td&gt;
&lt;td&gt;Facts about the user&lt;/td&gt;
&lt;td&gt;"User is vegetarian"&lt;/td&gt;
&lt;td&gt;A table column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Episodic&lt;/td&gt;
&lt;td&gt;Things that happened&lt;/td&gt;
&lt;td&gt;"Refund issued on March 3"&lt;/td&gt;
&lt;td&gt;An events table with timestamps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Procedural&lt;/td&gt;
&lt;td&gt;How to do things&lt;/td&gt;
&lt;td&gt;"Always confirm before deleting"&lt;/td&gt;
&lt;td&gt;Your prompt or your code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most of what people call long-term memory already has a natural home in a database. Only the leftovers, like free-form notes or "what did we talk about last time," need embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "the agent will remember"
&lt;/h2&gt;

&lt;p&gt;I built a small demo on Nhost and stored the same user two ways in one Postgres database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vector memory:&lt;/strong&gt; an &lt;code&gt;agent_memories&lt;/code&gt; table where the agent saves sentences from conversations, with embeddings for semantic search. This is roughly how most memory tools work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declarative memory:&lt;/strong&gt; a &lt;code&gt;user_profile&lt;/code&gt; table with one row per user and plain columns like &lt;code&gt;diet&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt; and &lt;code&gt;preferred_language&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I fed in a year of a fictional user's life. January: steak every week, apartment in Berlin. By March they'd gone vegetarian, and in June they moved to Lisbon. On top of that, about ten unrelated memories about work and hobbies.&lt;/p&gt;

&lt;h3&gt;
  
  
  "What does this user eat?"
&lt;/h3&gt;

&lt;p&gt;Here are the top 5 results from vector memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Jan 8   I eat steak pretty much every week
2. Mar 12  I went vegetarian last month
3. Jan 22  I work as a product designer at a fintech startup
4. Jan 15  I live in Berlin
5. Aug 9   I prefer meetings before lunch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outdated fact came first. Vector search compares meaning and ignores dates, so "steak every week" beats "went vegetarian" for a question about food. The agent gets both and has to guess which one is still true, with the wrong one on top.&lt;/p&gt;

&lt;p&gt;It happened again with "where does this user live?" "I live in Berlin" from January ranked first, and "I moved to Lisbon" from June came second.&lt;/p&gt;

&lt;p&gt;A top-5 search also returns five results whether or not five are relevant, so "I prefer meetings before lunch" counts as an answer about food. You can add a similarity cutoff and a recency boost, but then you're tuning a ranking formula to imitate what a single UPDATE already does. And "I went vegetarian last month" only makes sense if you know it was saved in March. Pull it out a year later and "last month" means nothing.&lt;/p&gt;

&lt;p&gt;The table returns one row: &lt;code&gt;diet = vegetarian&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Forget where I live"
&lt;/h3&gt;

&lt;p&gt;In the table, that's &lt;code&gt;city = null&lt;/code&gt;. One field, done.&lt;/p&gt;

&lt;p&gt;With vector memory, you have to find every sentence that says where the user lives and delete each one. Here's what a semantic search for "where I live" found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Jan 15  I live in Berlin
2. Jun 3   I moved to Lisbon
3. Feb 5   I'm training for a half marathon in the spring
4. Apr 10  I spent Easter hiking in the Dolomites
5. Oct 3   I joined a pottery class on Thursday evenings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two right rows are at the top, and the search didn't miss any. But it can't tell you where "about where I live" stops, so if the agent deletes the top five, the user also loses their half marathon, their Easter hike and their pottery class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another user asks about steak
&lt;/h3&gt;

&lt;p&gt;I added a second user, B, who lives in Madrid and has a steak memory of their own: a medium-rare steak is their favorite dinner.&lt;/p&gt;

&lt;p&gt;When B searched for "steak," B got their own steak memory first and then the rest of their own memories. When A searched for "steak," A got A's. A plain read of both tables showed the same thing: A sees 14 memories and one profile, all A's, and B sees 5 memories and one profile, all B's.&lt;/p&gt;

&lt;p&gt;Both approaches passed this one, because both tables have the same rule: a user can only read their own rows. The rule lives in the database, so it holds no matter what the agent decides to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI agent memory architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhgrjrdpgice9bo5yd3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhhgrjrdpgice9bo5yd3h.png" alt="AI agent memory architecture diagram: the agent reads and updates user data tables with the user's permissions, and vector memory holds only unstructured notes" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the setup I'd build in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent acts as the user. It calls your API with the user's session, so it gets the same permissions that user has and can't read anything they couldn't.&lt;/li&gt;
&lt;li&gt;Facts live in tables: profile, preferences, plan, settings. The agent reads them and changes them with normal updates.&lt;/li&gt;
&lt;li&gt;Events go in an events table. Orders, tickets, refunds, each with a timestamp. That's your episodic memory, and SQL already knows how to query it.&lt;/li&gt;
&lt;li&gt;Vectors cover the rest: notes, chat summaries, documents. Searchable by meaning and scoped to the user like everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent decides what to read and what to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent memory management: rules I'd follow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If a fact has one correct value, make it a column.&lt;/li&gt;
&lt;li&gt;When a fact changes, update it. Appending a new sentence leaves the old one behind.&lt;/li&gt;
&lt;li&gt;"Forget this" should be a delete or an update you can point to.&lt;/li&gt;
&lt;li&gt;Put permissions in the database, so every memory is scoped to its owner before the agent sees it.&lt;/li&gt;
&lt;li&gt;Save events with timestamps, so "what happened last month" becomes a plain query.&lt;/li&gt;
&lt;li&gt;Use embeddings only for text that doesn't fit a schema, and expect fuzzy results there.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where tools like Mem0 fit
&lt;/h2&gt;

&lt;p&gt;Memory tools like Mem0 pull "memories" out of conversations with an LLM, then use another LLM call to decide whether to add, update or delete them. For the messy leftovers, like notes and chat history, that's a reasonable approach.&lt;/p&gt;

&lt;p&gt;For facts with one correct value, I'd still pick a column in my own database. I can read it, change it and delete it without asking a model which sentence wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it on Nhost
&lt;/h2&gt;

&lt;p&gt;The whole demo runs on one &lt;a href="https://nhost.io/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-agent-memory" rel="noopener noreferrer"&gt;Nhost&lt;/a&gt; project. It's a &lt;a href="https://nhost.io/product/database?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-agent-memory" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; database with auth and permission rules on top, so both kinds of memory sit in one database under the same rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tables
&lt;/h3&gt;

&lt;p&gt;Declarative memory is a regular table with one row per user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;user_profile&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;diet&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;preferred_language&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vector memory is a table too. &lt;code&gt;agent_memories&lt;/code&gt; stores the sentences, and &lt;a href="https://nhost.io/product/ai?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=ai-agent-memory" rel="noopener noreferrer"&gt;Graphite&lt;/a&gt;, the AI service in Nhost, fills in an embeddings column for every row. When the text of a row changes, a small trigger marks it as outdated and Graphite embeds it again on its next sync. I didn't write any embedding code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions
&lt;/h3&gt;

&lt;p&gt;Both tables get the same rule for the &lt;code&gt;user&lt;/code&gt; role: you only see rows where &lt;code&gt;user_id&lt;/code&gt; is your own ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"_eq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-Hasura-User-Id"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47xjmx3b77gvck7iw0mf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47xjmx3b77gvck7iw0mf.png" alt="The permission rule on the agent_memories table in the Nhost dashboard: user_id equals X-Hasura-User-Id" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the demo, users can only read. In a real app you'd also let them update their own profile row, so "forget where I live" goes through the same rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search
&lt;/h3&gt;

&lt;p&gt;Graphite adds a semantic search query for the table, and it runs through the same permission rule as any other query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Recall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;graphiteSearchAgentMemories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcoba36nlrjh1x3x9zobp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcoba36nlrjh1x3x9zobp.png" alt="Graphite semantic search for &amp;quot;steak&amp;quot; run as Demo Customer A in the Nhost dashboard: only A's memories come back, with the outdated steak memory ranked first" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywb5rxzeqs8t8e40dwqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywb5rxzeqs8t8e40dwqr.png" alt="The same search run as Demo Customer B: B gets their own steak memory and nothing of A's" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's why user B never saw user A's steak. Nobody had to remember a WHERE clause in the search code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forgetting
&lt;/h3&gt;

&lt;p&gt;On the declarative side, "forget where I live" is one update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Forget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;update_user_profile_by_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk_columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Graphite's API is evolving, so check the &lt;a href="https://docs.nhost.io/products/ai" rel="noopener noreferrer"&gt;Nhost docs&lt;/a&gt; for the current setup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does an AI agent need a vector database for memory?
&lt;/h3&gt;

&lt;p&gt;For most of it, no. Facts about a user fit in table columns, and events fit in a table with timestamps. You only need embeddings for unstructured text, and pgvector inside Postgres handles that without a second database.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between short-term and long-term agent memory?
&lt;/h3&gt;

&lt;p&gt;Short-term memory is the current conversation in the model's context window, and it's gone when the session ends. Long-term memory is stored outside the model and loaded back in when it's relevant, across sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you make an AI agent forget something?
&lt;/h3&gt;

&lt;p&gt;If the fact lives in a column, you update or delete that field. If it only lives in vector memory, you have to search for every sentence that mentions it and delete those, and you can't be fully sure the search found them all.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you stop an agent from remembering other users' data?
&lt;/h3&gt;

&lt;p&gt;Scope memory in the database. If every memory row has an owner and the agent queries with the user's permissions, it never gets other users' rows back. An instruction in the prompt can be ignored, and a filter in the agent's code only works if every query remembers to add it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your agent needs less memory than you think
&lt;/h2&gt;

&lt;p&gt;Most of what an agent "remembers" is data your app should already own: profile fields, settings, events. Give the agent access to that, scoped to the user, and keep the fuzzy memory small.&lt;/p&gt;

&lt;p&gt;What does your agent remember right now that should've been a column? Tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>llm</category>
      <category>database</category>
    </item>
    <item>
      <title>pgvector is enough. Here are the two bugs you'll ship anyway.</title>
      <dc:creator>Max S.</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:04:31 +0000</pubDate>
      <link>https://dev.to/max4nhost/pgvector-is-enough-here-are-the-two-bugs-youll-ship-anyway-2g1i</link>
      <guid>https://dev.to/max4nhost/pgvector-is-enough-here-are-the-two-bugs-youll-ship-anyway-2g1i</guid>
      <description>&lt;p&gt;"Do we need Pinecone for this?"&lt;/p&gt;

&lt;p&gt;If you're adding AI search to an app in 2026, someone on your team has asked this. The internet's answer is pretty settled by now: no, keep your embeddings in Postgres with pgvector.&lt;/p&gt;

&lt;p&gt;I agree. I also work at &lt;a href="https://nhost.io/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pgvector-two-bugs" rel="noopener noreferrer"&gt;Nhost&lt;/a&gt;, a backend built on Postgres, so yes, I'm biased.&lt;/p&gt;

&lt;p&gt;But most pgvector tutorials I've read stop at the same spot. Add a vector column, insert embeddings, run a similarity query, ship it. So I built a support ticket search and kept going past that spot. Two bugs showed up almost right away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; pgvector handles the search fine. It won't keep your embeddings fresh when the data changes, and it won't make sure search results respect who's allowed to see what. Both fail silently. At the end I'll show how Nhost Graphite takes care of both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: stale embeddings
&lt;/h2&gt;

&lt;p&gt;Support tickets change. A customer adds details. An agent fixes the title. Someone finally pastes the actual error message.&lt;/p&gt;

&lt;p&gt;Here's the usual setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// creating a ticket&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO tickets (customer_id, title, body, embedding) VALUES ($1, $2, $3, $4)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// updating a ticket, somewhere else in the codebase&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE tickets SET body = $1 WHERE id = $2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;newBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The insert creates an embedding. The update doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt; nothing. No errors, no failing tests. Search quietly gets worse, because it's matching against what your tickets said last week.&lt;/p&gt;

&lt;p&gt;The fix is some kind of sync job. Mark changed rows (a trigger, an &lt;code&gt;updated_at&lt;/code&gt; column, an outbox table), then have a background worker pick them up, call the embeddings API, retry failures and write the new vectors back. That's a sync pipeline. You skipped the second database to avoid exactly this, and now you're building it anyway, just inside Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: search that ignores permissions
&lt;/h2&gt;

&lt;p&gt;Your ticket list endpoint filters by customer. Of course it does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then someone adds search a month later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tickets&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; customer A types "billing problem" and gets customer B's refund dispute.&lt;/p&gt;

&lt;p&gt;One missing WHERE clause. Postgres row-level security can catch it, but only if your setup passes the current user to the database on every request. Some platforms do that for you. Plenty of hand-rolled backends don't, and most pgvector tutorials skip the topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Graphite handles both
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://nhost.io/product/ai?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pgvector-two-bugs" rel="noopener noreferrer"&gt;Graphite&lt;/a&gt; is the AI service in Nhost. It keeps embeddings in your &lt;a href="https://nhost.io/product/database?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pgvector-two-bugs" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt; table with pgvector, so this is still the "just use Postgres" setup. The difference is who does the boring parts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Graphite's API is evolving, so check the &lt;a href="https://docs.nhost.io/products/ai" rel="noopener noreferrer"&gt;Nhost docs&lt;/a&gt; for the current setup. The results below come from a test project in October 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Fresh embeddings
&lt;/h3&gt;

&lt;p&gt;You add a vector column to the table and point Graphite at it with one config call: which table, which columns to embed and which model to use. You also add a small trigger (the docs have an example) that marks a ticket as outdated when its title or body changes. Graphite picks up those rows on its next sync, calls the embeddings API and writes the new vectors back. The sync pipeline I described for stale embeddings still exists, but Graphite runs it, not you.&lt;/p&gt;

&lt;p&gt;To test it, I edited one of user A's tickets. "Feature request: export to CSV" became "Charts invisible in dark mode." Then I searched for "graphs can't be seen with the dark theme."&lt;/p&gt;

&lt;p&gt;This is a small demo: 36 tickets, two users, OpenAI's &lt;code&gt;text-embedding-3-small&lt;/code&gt;. It shows the behavior, not performance at scale.&lt;/p&gt;

&lt;p&gt;Right after the edit, the ticket ranked 5th. The title was new, but the embedding was still the old one. After the next sync, it ranked 1st.&lt;/p&gt;

&lt;p&gt;Sync runs on a schedule, every 5 minutes in my project, which is plenty for support tickets. Apart from that one trigger, I didn't write any sync code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search that knows who's asking
&lt;/h3&gt;

&lt;p&gt;Permissions in Nhost live on the table. Here's the rule for regular users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"_eq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-Hasura-User-Id"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypk5xaug7a7elyp8kip8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypk5xaug7a7elyp8kip8.png" alt="The same rule in the Nhost dashboard: customer_id equals X-Hasura-User-Id" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Graphite adds a search query to the &lt;a href="https://nhost.io/product/graphql?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=pgvector-two-bugs" rel="noopener noreferrer"&gt;GraphQL API&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;graphiteSearchTickets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That query goes through the same permission rule as everything else. I ran one search, "I can't log in to my account," three times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Admin (no rules)&lt;/th&gt;
&lt;th&gt;User A&lt;/th&gt;
&lt;th&gt;User B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[B] Unable to sign in&lt;/td&gt;
&lt;td&gt;Can't log in after update&lt;/td&gt;
&lt;td&gt;Unable to sign in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[A] Can't log in after update&lt;/td&gt;
&lt;td&gt;Locked out of my account&lt;/td&gt;
&lt;td&gt;Google login stopped working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[A] Locked out of my account&lt;/td&gt;
&lt;td&gt;Two-factor code not accepted&lt;/td&gt;
&lt;td&gt;Logged out every few minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;[A] Two-factor code not accepted&lt;/td&gt;
&lt;td&gt;Login fails on mobile app&lt;/td&gt;
&lt;td&gt;Password reset page shows error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;[B] Google login stopped working&lt;/td&gt;
&lt;td&gt;Reset link says it expired&lt;/td&gt;
&lt;td&gt;Forgot password, no reset email&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvapk0224g9ymsnsmp4j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvapk0224g9ymsnsmp4j4.png" alt="Searching as admin: results from both customers" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7d3xjtjqzhwnkosic6e9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7d3xjtjqzhwnkosic6e9.png" alt="Searching as Demo Customer A: every result has the same customer_id" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As admin, the top 5 mixes both customers. As A or B, every result belongs to the person asking. The rule is defined once, on the table, and search follows it like any other query. Nobody had to remember a WHERE clause in the search code.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should use a dedicated vector DB
&lt;/h2&gt;

&lt;p&gt;Fair is fair. pgvector has limits.&lt;/p&gt;

&lt;p&gt;Most 2026 write-ups I found put the comfortable range at around &lt;a href="https://selfhost.dev/pgvector-in-production-2026-critiques-failures/" rel="noopener noreferrer"&gt;10 million vectors&lt;/a&gt; for typical 768 to 1536 dimension embeddings. Your number will depend on dimensions, RAM and how much you filter. Above that you're tuning memory and indexes, or adding pgvectorscale. A dedicated vector DB starts making sense with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hundreds of millions or billions of vectors&lt;/li&gt;
&lt;li&gt;Sub-20ms p99 latency targets at high query volume&lt;/li&gt;
&lt;li&gt;Heavy re-embedding, which leaves Postgres with a lot of dead rows to vacuum&lt;/li&gt;
&lt;li&gt;A search product with its own team and needs like multi-vector retrieval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that describes your app? Keep the vectors next to the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, do you need Pinecone?
&lt;/h2&gt;

&lt;p&gt;Probably not. You do need a plan for stale embeddings and permissions. You can build that plan yourself, or let Graphite handle it.&lt;/p&gt;

&lt;p&gt;How are you keeping embeddings in sync with your data right now? Triggers, cron jobs, hoping for the best? Tell me in the comments.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ai</category>
      <category>database</category>
      <category>pgvector</category>
    </item>
    <item>
      <title>Supabase vs Appwrite vs Nhost: I built two apps with Claude Code</title>
      <dc:creator>Max S.</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:50:02 +0000</pubDate>
      <link>https://dev.to/max4nhost/supabase-vs-appwrite-vs-nhost-i-built-two-apps-with-claude-code-34oa</link>
      <guid>https://dev.to/max4nhost/supabase-vs-appwrite-vs-nhost-i-built-two-apps-with-claude-code-34oa</guid>
      <description>&lt;p&gt;If you let Claude Code build an app on your backend and never mention security, where does it put the access rules? And does it get them right? I tried it by building two apps on &lt;a href="https://supabase.com" rel="noopener noreferrer"&gt;Supabase&lt;/a&gt;, &lt;a href="https://appwrite.io" rel="noopener noreferrer"&gt;Appwrite&lt;/a&gt; and &lt;a href="https://nhost.io" rel="noopener noreferrer"&gt;Nhost&lt;/a&gt; through their CLIs, with the same prompts on each.&lt;/p&gt;

&lt;p&gt;The first is a small polling app with one tricky condition: nobody should be able to see how other users voted.&lt;/p&gt;

&lt;p&gt;The second is a more complex app, an incident logger with file uploads, team membership and a server function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Every run used the same setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code running Opus 5.5 on medium reasoning&lt;/li&gt;
&lt;li&gt;A new session, an empty folder and an empty cloud project for every run&lt;/li&gt;
&lt;li&gt;Everything ran against the cloud, with no local stack and no Docker&lt;/li&gt;
&lt;li&gt;Email confirmation switched off on all three, so test accounts could sign in right away&lt;/li&gt;
&lt;li&gt;No plugins, skills or notes from earlier work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither prompt mentions permissions, roles or backends. The main thing I watched was where each platform leads the agent to put the access rules when nobody tells it to. I also tracked how smoothly it worked with each platform, how often I had to step in, and how long each run took.&lt;/p&gt;

&lt;h2&gt;
  
  
  First app: a poll with a secret ballot
&lt;/h2&gt;

&lt;p&gt;The first line of each prompt named the platform and the project. The rest was the same in all three:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a polling app.&lt;/p&gt;

&lt;p&gt;Users sign up and log in with email and password. Different users log in from different browsers and share the same data.&lt;/p&gt;

&lt;p&gt;Anyone logged in can create a poll with a question and a few options, and can vote once per poll.&lt;/p&gt;

&lt;p&gt;Everyone sees the total votes for each option. Each user sees which option they voted for. Nobody can find out how another specific user voted.&lt;/p&gt;

&lt;p&gt;Only the person who created a poll can delete it.&lt;/p&gt;

&lt;p&gt;Keep it minimal, styling does not matter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I thought the hardest part would be keeping each vote secret. If people can read the votes to count them, they can also see who voted for what. So the votes need to be hidden, and the totals need to come from somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poll app results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All three produced a working app with the access rules enforced in the data.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Nhost&lt;/th&gt;
&lt;th&gt;Supabase&lt;/th&gt;
&lt;th&gt;Appwrite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;2:46&lt;/td&gt;
&lt;td&gt;3:11&lt;/td&gt;
&lt;td&gt;4:43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$0.56&lt;/td&gt;
&lt;td&gt;$0.70&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens read&lt;/td&gt;
&lt;td&gt;705K&lt;/td&gt;
&lt;td&gt;1.3M&lt;/td&gt;
&lt;td&gt;1.7M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistakes it had to fix&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times it stopped to ask me something&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs pages opened&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the rules ended up&lt;/td&gt;
&lt;td&gt;row rules in the database&lt;/td&gt;
&lt;td&gt;row-level policies in the database&lt;/td&gt;
&lt;td&gt;database closed, everything through a server function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it handed back&lt;/td&gt;
&lt;td&gt;a local page&lt;/td&gt;
&lt;td&gt;a local page&lt;/td&gt;
&lt;td&gt;a deployed site on a public URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Nhost: fastest, but with an extra setup step
&lt;/h2&gt;

&lt;p&gt;On Nhost, Claude Code finished in 2 minutes 46 seconds. It created three tables and a view, set the &lt;a href="https://docs.nhost.io/products/graphql/permissions" rel="noopener noreferrer"&gt;permissions&lt;/a&gt;, wrote the page, then ran a two-user test against the live project and reported each rule as passing.&lt;/p&gt;

&lt;p&gt;Only the person who voted can see their own vote. In the dashboard, the rule for the &lt;code&gt;user&lt;/code&gt; role is &lt;code&gt;user_id _eq X-Hasura-User-Id&lt;/code&gt;, and the &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;anonymous&lt;/code&gt; roles can't access the table at all. Update and delete are closed too, so nobody can change their vote.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjernkwyj60hqig36fcs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjernkwyj60hqig36fcs.png" alt="Nhost: the select rule on the votes table" width="799" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The totals come from a separate view, &lt;code&gt;poll_option_totals&lt;/code&gt;, which counts the votes without showing who cast them. Claude Code came up with this view by itself.&lt;/p&gt;

&lt;p&gt;The only mistake was in the test script: Claude Code used single quotes in a GraphQL query, which GraphQL doesn't accept. It caught the error, fixed it, and the test passed. The schema and permissions worked on the first try.&lt;/p&gt;

&lt;p&gt;There was one extra step before I started the clock. The CLI couldn't read the admin secret, so I had to get it from the dashboard and give it to the agent in the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase: no questions, two wrong turns
&lt;/h2&gt;

&lt;p&gt;Claude Code needed 3 minutes 11 seconds on Supabase and didn't ask me a single question. It applied the schema, tested it with two throwaway users, then deleted them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Row-level security&lt;/a&gt; is on for all three tables, and every policy applies only to signed-in users (the &lt;code&gt;authenticated&lt;/code&gt; role). The votes table has &lt;code&gt;cast own vote&lt;/code&gt; for inserts and &lt;code&gt;read own votes&lt;/code&gt; for reads. Polls have &lt;code&gt;read polls&lt;/code&gt; and &lt;code&gt;creator deletes poll&lt;/code&gt;, and options have &lt;code&gt;read options&lt;/code&gt;. Anonymous visitors get no access.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy6kn7ydotcwrvocubvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy6kn7ydotcwrvocubvt.png" alt="Supabase: the policies on the polling app" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both of Claude Code's mistakes came from the CLI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It used two flags together that the CLI doesn't allow. The error message explained the fix, and the next attempt worked.&lt;/li&gt;
&lt;li&gt;The admin key it saved was incomplete. The &lt;a href="https://supabase.com/docs/guides/local-development/cli/getting-started" rel="noopener noreferrer"&gt;Supabase CLI&lt;/a&gt; shows only part of a secret key unless you add a reveal flag, and it doesn't tell you the key is cut off. With a broken key, the test users were never created. The first error Claude Code saw was &lt;code&gt;permission denied for function create_poll&lt;/code&gt;, so it spent two debugging steps looking for a permissions bug when the real problem was the key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Code also knew about a Supabase quirk: loading data inside the handler that runs when a user logs in or out can freeze the client. It delayed that call on purpose, though it only told me about it when I asked.&lt;/p&gt;

&lt;p&gt;By default, Supabase doesn't turn on row-level security for new tables, and I left that setting alone. Claude Code turned it on by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appwrite: the slowest, with the database closed
&lt;/h2&gt;

&lt;p&gt;Building on Appwrite took the longest, 4 minutes 43 seconds. That's because Claude Code did two extra things: it wrote a server function and deployed the app to a public URL without being asked.&lt;/p&gt;

&lt;p&gt;The votes table has no &lt;a href="https://appwrite.io/docs/advanced/platform/permissions" rel="noopener noreferrer"&gt;permissions&lt;/a&gt; at all: the Security tab is empty, and row-level security is on. No user can read or write the table directly, signed in or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmjs2z0ezbhmrg5aehge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmjs2z0ezbhmrg5aehge.png" alt="Appwrite: no table permissions on votes, row security on" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Nhost and Supabase, a signed-in user's browser reads the votes table directly, and a permission rule filters what comes back. On Appwrite, the browser only talks to a server function that checks who's asking, which makes this the strictest setup of the three.&lt;/p&gt;

&lt;p&gt;When I asked why it went this way, Claude Code explained that in Appwrite, client-side permissions can't stop someone from writing a vote with another person's ID. The platform doesn't check that the ID belongs to the user who created the row, so it moved every write to the server to block fake votes.&lt;/p&gt;

&lt;p&gt;Along the way, it made three mistakes. The first two were small: it typed &lt;code&gt;appwrite projects&lt;/code&gt; instead of &lt;code&gt;project&lt;/code&gt;, and its first check of the SDK version failed. The third came after deploying. It grabbed the first URL the tool printed, but that turned out to be a private preview link that sends visitors to an Appwrite sign-in page. After that, it created a public domain and admitted that a real user would have run into that login page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling app recap
&lt;/h2&gt;

&lt;p&gt;On Nhost, Claude Code closed the votes table to everyone except the person who voted and added a view that returns only the counts.&lt;/p&gt;

&lt;p&gt;The Supabase version has a policy that lets each user read only their own votes, plus a function with extra privileges that returns the totals.&lt;/p&gt;

&lt;p&gt;Appwrite got the strictest setup: the table is closed to every user, and all reads and writes go through one server function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second app: a team incident logger
&lt;/h2&gt;

&lt;p&gt;Each platform got a fresh project and this prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a team incident logger.&lt;/p&gt;

&lt;p&gt;Users sign up and log in with email and password.&lt;/p&gt;

&lt;p&gt;Anyone logged in can create a team and invite other users to it by email address. A user can belong to several teams.&lt;/p&gt;

&lt;p&gt;Incidents belong to a team. Anyone on that team can report one with a title, a description, and a severity of low, medium, high or critical, and can attach a .txt log file to it.&lt;/p&gt;

&lt;p&gt;People on a team can see that team's incidents and open their attachments. People outside the team cannot see either, including the attached files.&lt;/p&gt;

&lt;p&gt;Only the person who reported an incident can edit it, delete it, or mark it resolved.&lt;/p&gt;

&lt;p&gt;When an incident is reported as critical, the platform itself should react to that and run a server function that records an alert was sent.&lt;/p&gt;

&lt;p&gt;Keep it minimal, styling does not matter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compared to the polling app, the incident logger adds four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;File storage. Files need their own permissions, separate from the incident records. A common leak is when the incident itself is locked down, but the attached file sits at a URL anyone can open.&lt;/li&gt;
&lt;li&gt;Access based on team membership. In the polling app, the rule was "is this vote mine". Here it's "am I on the team this incident belongs to", so the check has to go through a third table with the team members.&lt;/li&gt;
&lt;li&gt;A server function that the agent has to deploy to the platform.&lt;/li&gt;
&lt;li&gt;A trigger. The function should run on its own when a critical incident shows up in the database, without the app calling it. Each platform does this differently.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing I wanted to check most was whether someone outside a team could open another team's attachment from a direct link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident logger results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Supabase&lt;/th&gt;
&lt;th&gt;Appwrite&lt;/th&gt;
&lt;th&gt;Nhost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;4:48&lt;/td&gt;
&lt;td&gt;6:22&lt;/td&gt;
&lt;td&gt;8:20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;$2.21&lt;/td&gt;
&lt;td&gt;$2.64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens read&lt;/td&gt;
&lt;td&gt;1.0M&lt;/td&gt;
&lt;td&gt;3.3M&lt;/td&gt;
&lt;td&gt;5.0M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistakes it had to fix&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Times it stopped to ask me something&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs pages opened&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;several&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Its own end-to-end checks&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where the file rule ended up&lt;/td&gt;
&lt;td&gt;a policy on the storage path&lt;/td&gt;
&lt;td&gt;row permissions granted to a team&lt;/td&gt;
&lt;td&gt;a relationship chain in the database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What it handed back&lt;/td&gt;
&lt;td&gt;a local page&lt;/td&gt;
&lt;td&gt;a deployed site on a public URL&lt;/td&gt;
&lt;td&gt;a local page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the incident logger, Supabase was the fastest, Appwrite came second, and Nhost, which was the fastest on the polling app, came third.&lt;/p&gt;

&lt;p&gt;Time and cost line up with how many tokens Claude Code read on each platform: 1M on Supabase, 3.3M on Appwrite and 5M on Nhost. The more the agent had to read to figure out a platform, the longer and more expensive the build got.&lt;/p&gt;

&lt;p&gt;All three apps got the access rules right again. I checked the permissions in each project myself, so the code below is what's actually deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supabase: the fastest this time, with no questions asked
&lt;/h2&gt;

&lt;p&gt;Claude Code got project access through my existing CLI login, applied the migration, deployed the Edge Function from the CLI and was done before I'd read half of its output.&lt;/p&gt;

&lt;p&gt;To test the permission rules, it ran them inside a transaction that it rolled back at the end, so none of the test data was saved. Then, without being asked, it ran Supabase's security advisor and fixed two of the issues it found: a trigger function that could be called over RPC, and a function without a fixed &lt;code&gt;search_path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the end, it told me to turn off email confirmation for testing. I had already done that, but Claude Code never checked the project's auth settings, and nothing in Supabase prompted it to.&lt;/p&gt;

&lt;p&gt;The app has eight row-level policies across five tables, and all of them apply only to signed-in users. Members can read everything that belongs to their team and add incidents, and only the reporter can edit or delete one. The &lt;code&gt;teams&lt;/code&gt; and &lt;code&gt;team_invites&lt;/code&gt; tables have no write policies at all, because creating a team and inviting someone go through functions with elevated privileges.&lt;/p&gt;

&lt;p&gt;For files, Claude Code stored each attachment in a folder named after the team's ID, and the read policy checks that folder against the user's teams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'incident-logs'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_team_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;try_uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;foldername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the reporter can upload or delete a file. For critical incidents, a database trigger calls the Edge Function through &lt;code&gt;pg_net&lt;/code&gt;, and the shared secret is stored in Vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appwrite: built-in teams and another unrequested deploy
&lt;/h2&gt;

&lt;p&gt;As with the polling app, Claude Code put the app on Appwrite's hosting without being asked. This time it ran into a problem. Appwrite only lets an app connect to a project from domains added in the project settings, and Claude got an error while adding one.&lt;/p&gt;

&lt;p&gt;It also left a &lt;code&gt;setup.sh&lt;/code&gt;, but the script is just a list of the commands it ran and won't work on a fresh project. The first run of those commands failed halfway, because optional columns in Appwrite need an explicit flag.&lt;/p&gt;

&lt;p&gt;Claude checked that someone outside a team can't list its incidents, open one by ID, view or download a file, or add an incident to that team. It didn't click through the finished app itself, because that would have created a real account on a live public site.&lt;/p&gt;

&lt;p&gt;For teams, it used Appwrite's built-in &lt;a href="https://appwrite.io/docs/products/auth/teams" rel="noopener noreferrer"&gt;Teams&lt;/a&gt; feature, which already handles invitations, membership and invite emails. On Nhost and Supabase, Claude had to build all of this by hand.&lt;/p&gt;

&lt;p&gt;Each incident gives access to its own team, and at the table level, signed-in users can only create incidents. Appwrite combines table and row permissions with OR, so any read access on the table would let everyone see every incident, whatever the row says. Appwrite also won't let a user give access to a team they aren't in, so nobody can share an incident with someone else's team.&lt;/p&gt;

&lt;p&gt;One limitation is that only the team's creator can invite people, because that's Appwrite's default for team owners. Letting every member invite would have needed a server function, and Claude pointed this out itself as something I might want to change.&lt;/p&gt;

&lt;p&gt;The alert function is connected to Appwrite's database events, so Appwrite runs it whenever an incident is created or updated, with no extra code in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nhost: file rules three relationships deep
&lt;/h2&gt;

&lt;p&gt;By Claude's own count, about two-thirds of the run went into deployment and figuring out how Nhost works.&lt;/p&gt;

&lt;p&gt;The Nhost CLI has no command that applies migrations and metadata to a cloud project. So Claude Code pushed the schema and metadata through Hasura's HTTP API instead. It also created the migration-tracking table by hand and added a row to it, so the later deploy wouldn't run the same migration a second time.&lt;/p&gt;

&lt;p&gt;Then it found that &lt;code&gt;nhost init&lt;/code&gt; leaves the metadata folder empty, but a deploy applies whatever is in that folder. To avoid wiping the built-in auth and storage tables, Claude wrote out metadata for every table, including those.&lt;/p&gt;

&lt;p&gt;Functions on Nhost deploy only from a connected GitHub repo, so the alert function stayed untested until Claude did a full push and deploy near the end.&lt;/p&gt;

&lt;p&gt;The app has five tables, and everything below comes from the deployed metadata. A user can read a file if they uploaded it, or if it's attached to an incident from one of their teams. To check the second part, the rule goes from the file to the incident, then to the team, then to the team's members:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;_or&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uploaded_by_user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;_eq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;X-Hasura-User-Id&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;incidents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;members&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;_eq&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;X-Hasura-User-Id&lt;/span&gt;&lt;span class="pi"&gt;}}}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftupqacj1ngz0qvwzigzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftupqacj1ngz0qvwzigzz.png" alt="Nhost: the file read rule follows three relationships to the team" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Uploads are checked by bucket, file name and MIME type, so a binary file renamed to &lt;code&gt;.txt&lt;/code&gt; still gets rejected. The uploader's ID comes from the user's token, so the app can't fake it. Only the reporter can edit an incident, and a separate check stops anyone from attaching a file someone else uploaded.&lt;/p&gt;

&lt;p&gt;For critical incidents, Claude set up a Hasura &lt;a href="https://docs.nhost.io/products/events/event-triggers" rel="noopener noreferrer"&gt;event trigger&lt;/a&gt;. It fires when an incident is created, and on updates only when the severity changes. It retries three times and sends a shared secret in the header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing file access myself
&lt;/h2&gt;

&lt;p&gt;File access was the main thing I wanted to test, so I checked it by hand on each platform. I made an account, created a team, reported an incident with a &lt;code&gt;.txt&lt;/code&gt; attached, and then tried to open that file three ways: signed out, signed in as someone outside the team, and signed in as a team member.&lt;/p&gt;

&lt;p&gt;On all three platforms, only the team member could open the file. The difference was in how each platform handled the link.&lt;/p&gt;

&lt;p&gt;Appwrite is the only one where an attachment has a URL you could paste into a chat. The login session is stored in a browser cookie, so the same link works differently depending on who opens it: a team member gets the file, and everyone else gets a 404.&lt;/p&gt;

&lt;p&gt;Supabase and Nhost don't give you a link at all. The app downloads the file through the SDK and attaches the user's token to the request, so there's nothing to copy and send to someone. If you open the storage URL directly in the browser, you get an error even as a team member, because the browser doesn't send that token.&lt;/p&gt;

&lt;p&gt;Here's what each one returns to an anonymous request:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Anonymous request to the file&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Appwrite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;The requested file could not be found&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bucket not found&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nhost&lt;/td&gt;
&lt;td&gt;&lt;code&gt;you are not authorized&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Appwrite and Supabase respond as if the file doesn't exist, while Nhost says you aren't authorized. Later, when I asked Claude about common pitfalls on Appwrite, it named this exact behavior as something that makes debugging harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude almost never opened the docs
&lt;/h2&gt;

&lt;p&gt;In five of the six runs, Claude Code didn't open a single documentation page. It got what it needed from the CLI's &lt;code&gt;--help&lt;/code&gt; output and the type definitions in the installed SDK. On Supabase, the help text is how it found the &lt;code&gt;--reveal&lt;/code&gt; flag for the cut-off admin key, and later the &lt;code&gt;--use-api&lt;/code&gt; flag that let it deploy a function without Deno installed. On Appwrite, it used &lt;code&gt;--help&lt;/code&gt;, &lt;code&gt;npm view&lt;/code&gt; to check SDK versions, and the error messages themselves.&lt;/p&gt;

&lt;p&gt;The only exception was Nhost on the incident logger. Before writing anything, Claude read the Nhost docs on functions, event triggers and storage permissions. My guess is it didn't know those parts of Nhost well enough from training.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's easy to get wrong on each platform
&lt;/h2&gt;

&lt;p&gt;When I asked afterward what's easy to get wrong about permissions on each platform, Claude gave the same answer every time: showing vote totals without revealing who voted for what.&lt;/p&gt;

&lt;p&gt;The Nhost agent explained it best. Permissions there work row by row, so letting everyone read the votes table to count votes also lets them read who cast each one. Its fix was a separate view. As it put it, you have to come up with that yourself, because it's not the obvious first move.&lt;/p&gt;

&lt;p&gt;Supabase's fix was a function running with elevated privileges, and Claude warned that writing that function carelessly leaks exactly what you were protecting. For Appwrite, it pointed out that table and row permissions are combined with OR, so unless the table grants no read at all, row-level privacy silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they knew was wrong with their own work
&lt;/h2&gt;

&lt;p&gt;I also asked each one where its own work was most likely to be broken in a way its tests wouldn't catch. All three came back with long lists.&lt;/p&gt;

&lt;p&gt;The worst was Supabase's. Invites there are applied the moment the user row is created, which is before the email address has been confirmed. That's fine as long as confirmation is on, since an unconfirmed account can't log in. But on Supabase, Claude was the one that told me to turn confirmation off for testing. Do that, and anyone who guesses an invited address can sign up with it and land inside that team, incidents and files included. The agent's own advice: until that's fixed, don't turn confirmation off.&lt;/p&gt;

&lt;p&gt;On Nhost, Claude ran into a smaller invite problem, but caught it while building. Its invite trigger ran before the permission check, so someone outside a team could use invites to test whether an email had an account. It added a membership check and reran its tests. Supabase's build has this hole too, and Claude mentioned it there only when I asked.&lt;/p&gt;

&lt;p&gt;Other problems Claude listed, none of which its tests covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Only &lt;code&gt;.txt&lt;/code&gt;" is weaker than it sounds on Supabase and Appwrite.&lt;/strong&gt; Both check the file extension and the content type the browser declares, so neither looks at what's actually in the file. Nhost detects the type from the file's contents, which is why a renamed binary gets refused there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts can vanish silently.&lt;/strong&gt; Supabase's &lt;code&gt;pg_net&lt;/code&gt; call fires once with no retry; if the function is down, nothing is recorded and nothing says so. Nhost retries three times and then gives up just as quietly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphaned files.&lt;/strong&gt; On Supabase and Appwrite, a failure between uploading a file and saving the incident leaves the file in storage with nothing pointing at it, and in one case nobody can delete it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Membership changes.&lt;/strong&gt; On all three, someone removed from a team keeps edit rights on incidents they reported. None of them built a way to remove a member, so it doesn't come up yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak assertions in their own tests.&lt;/strong&gt; On Appwrite, Claude admitted that any 404 counted as "access denied" in its suite, so a typo in an ID would have passed too, and that its "no alert for a low-severity incident" check simply waited six seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the rules cost
&lt;/h2&gt;

&lt;p&gt;Then I asked how the file permission check would hold up as data grows. The answers varied a lot in how honest they were.&lt;/p&gt;

&lt;p&gt;The Supabase answer was the most specific and the most self-critical. Opening one file is two indexed lookups. Listing them is where it degrades: the membership check is a function Postgres can't fold into the query, so it runs once per row, and &lt;code&gt;try_uuid&lt;/code&gt; opens a mini-transaction on every call. It offered a cheaper pattern the planner could evaluate once per query, then said plainly that it had run no &lt;code&gt;EXPLAIN&lt;/code&gt;, no load test and no volume test.&lt;/p&gt;

&lt;p&gt;For Appwrite, Claude noted that each team a user joins lengthens the list of roles checked on every query, that the app stops at a hundred incidents with no paging beyond that, and that viewing an attachment costs an extra rate-limited request. It added an index and measured nothing.&lt;/p&gt;

&lt;p&gt;On Nhost, Claude added indexes on the columns its file permission rule checks, and said it couldn't measure anything without direct database access.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away from this
&lt;/h2&gt;

&lt;p&gt;Going in, I was pretty sure all three backends would handle both apps. They did, though getting there was a lot rougher on some than others.&lt;/p&gt;

&lt;p&gt;Claude Code worked very differently on each platform, and I think most of that comes down to how well it already knows each one, which also varies by task.&lt;/p&gt;

&lt;p&gt;One visit to the docs across six builds still surprises me. Everything else came from the CLI's help text, the SDK's types, and whatever the model already knew. If that's how people are building now, then help output and type definitions are doing work most teams think their documentation site is doing.&lt;/p&gt;

&lt;p&gt;The other thing I noticed is that Claude kept most of the risks to itself until I asked. On all three platforms, it had a list ready about where its own work might break, and almost none of it came up on its own.&lt;/p&gt;

&lt;p&gt;Both of these are still small apps. What happens on something real, with a schema that keeps changing and features piling up, is another question. That's the next test.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>ai</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Supabase Alternatives in 2026</title>
      <dc:creator>Max S.</dc:creator>
      <pubDate>Mon, 15 Jun 2026 09:26:09 +0000</pubDate>
      <link>https://dev.to/max4nhost/supabase-alternatives-in-2026-6of</link>
      <guid>https://dev.to/max4nhost/supabase-alternatives-in-2026-6of</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclaimer: Supabase is a great platform, with by far the largest ecosystem and community.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, if you're looking to leave, the reasons usually come down to three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The pricing&lt;/strong&gt; gets non-linear at the exact moment a project starts working&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgREST doesn't scale&lt;/strong&gt; gracefully to complex relational frontends&lt;/li&gt;
&lt;li&gt;You have requirements (self-hosting mandates, a reactive data model, compliance regimes, or &lt;strong&gt;AI agent workflows&lt;/strong&gt;) that the platform doesn't model well&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of that is your situation, stop reading.&lt;/p&gt;

&lt;p&gt;What follows is Nhost, Neon, Appwrite, Convex, Firebase, PocketBase. Not Railway, not Render. Those are infra platforms and comparing them to Supabase doesn't make sense, like comparing a toolbox to a hammer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nhost
&lt;/h2&gt;

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

&lt;p&gt;&lt;a href="https://nhost.io/" rel="noopener noreferrer"&gt;Nhost&lt;/a&gt; is, structurally, very similar to Supabase: Postgres, auth, storage, serverless functions. The difference is &lt;strong&gt;the API layer&lt;/strong&gt;. GraphQL instead of PostgREST, powered by Constellation, their own open-source GraphQL engine written in Go.&lt;/p&gt;

&lt;p&gt;If you have a dashboard that needs data from six related tables, a GraphQL query gives you exactly that in one round trip. PostgREST gives you a series of choices: chain multiple requests, write RPC functions that fake the join, or start questioning your data model. It gets unwieldy fast.&lt;/p&gt;

&lt;p&gt;The permission system compounds this: role-based row and column access is defined inside the query engine itself, so you're not maintaining separate RLS policies in SQL that drift from your actual auth logic.&lt;/p&gt;

&lt;p&gt;Nhost fits naturally for &lt;strong&gt;AI agent workflows&lt;/strong&gt;. Agents touching shared Postgres state get real-time updates via subscriptions, and the permission model means no separate access control layer bolted around your database calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Neon
&lt;/h2&gt;

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

&lt;p&gt;&lt;a href="https://neon.com/" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; is not a full Supabase replacement. &lt;strong&gt;It's a database.&lt;/strong&gt; That's it. No auth, no storage, no functions, no realtime. If you pick Neon, you're assembling the rest of the backend yourself.&lt;/p&gt;

&lt;p&gt;Why would you do that? Because what Neon actually built is genuinely impressive: they separated compute from storage at the Postgres level, which makes &lt;strong&gt;database branching&lt;/strong&gt; possible. A full copy-on-write snapshot of your database in seconds. One branch per pull request, against real data, gone when the PR closes. Databricks acquired them last year and it's continued to develop. It's become the thing other providers benchmark against.&lt;/p&gt;

&lt;p&gt;Neon is a good fit if the database itself is your unsolved problem and the rest of your infrastructure is already handled: platforms provisioning a database per customer, CI pipelines that need isolated environments per test suite, teams with existing auth that just want better Postgres without ripping everything else out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appwrite
&lt;/h2&gt;

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

&lt;p&gt;The thing &lt;a href="https://appwrite.io/" rel="noopener noreferrer"&gt;Appwrite&lt;/a&gt; does well, genuinely well in a way the other platforms kind of half-ass, is &lt;strong&gt;self-hosting&lt;/strong&gt;. One &lt;code&gt;docker-compose&lt;/code&gt; command and you have a full working stack. Not "full working stack but you need to configure eight more things." Actually full, actually working.&lt;/p&gt;

&lt;p&gt;This matters more than people give it credit for. Air-gapped environments, healthcare compliance, regions where AWS and GCP are politically complicated, enterprise contracts with data residency clauses that say the words "within the EU" in boldface. Appwrite handles all of this without treating self-hosting as a community project that the founders are vaguely embarrassed by.&lt;/p&gt;

&lt;p&gt;Here's the catch, and it's a real one: &lt;strong&gt;Appwrite is not Postgres.&lt;/strong&gt; The data layer is its own collection and document model. Which means no SQL, no &lt;code&gt;pg_dump&lt;/code&gt;, no &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; when a query gets slow, no connecting a BI tool to it. One of the quiet reasons Postgres-backed platforms feel safe is that the escape hatches are all there if you need them. Appwrite closes most of them.&lt;/p&gt;

&lt;p&gt;If your team thinks in SQL, you'll notice this within the first week. There's no workaround for it.&lt;/p&gt;

&lt;p&gt;If you're building something mobile-first, especially on Flutter, Appwrite's Flutter SDK is one of the best in this space and the SQL thing might genuinely not matter. Different problem, different tradeoffs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Convex
&lt;/h2&gt;

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

&lt;p&gt;Almost every comparison article gets &lt;a href="https://www.convex.dev/" rel="noopener noreferrer"&gt;Convex&lt;/a&gt; wrong, usually by fitting it into a row in a feature table next to Firebase and calling it a document database alternative. &lt;em&gt;It's not that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The actual model: your backend is TypeScript functions. The data layer is document-relational. The important part, the part that sounds like a detail but isn't, is that &lt;strong&gt;queries are reactive subscriptions by default&lt;/strong&gt;. When data changes, every subscribed client updates. Automatically. Without you writing cache invalidation logic. Without you managing subscription channels. Without you thinking about whether two parts of the UI are looking at stale data from the same table.&lt;/p&gt;

&lt;p&gt;End-to-end TypeScript from the schema to the React component means the types aren't generated artifacts you have to remember to regenerate. They're the same code.&lt;/p&gt;

&lt;p&gt;When does this matter? Collaborative editors. Live multiplayer state. AI agent applications where multiple processes are writing to shared state concurrently and the UI has to reflect it immediately. Convex makes a whole class of synchronization bugs structurally impossible. Not hard to write — the runtime doesn't let them exist.&lt;/p&gt;

&lt;p&gt;When does it not matter? Analytical stuff. Reporting. Anything that needs direct SQL access. Convex's document model won't bend to accommodate an ad-hoc &lt;code&gt;GROUP BY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The learning curve is also real: the first two weeks are genuinely disorienting. The mental model is different enough from "database + API" that your existing instincts are wrong in subtle ways. Developers who get through that describe the next few months as feeling like they have superpowers. I believe both descriptions. Whether the trade is worth it depends on whether realtime consistency is your actual hard problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Firebase
&lt;/h2&gt;

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

&lt;p&gt;Hard to imagine cases when people move from Supabase to &lt;a href="https://firebase.google.com/" rel="noopener noreferrer"&gt;Firebase&lt;/a&gt;, but I can't skip this.&lt;/p&gt;

&lt;p&gt;If you outgrow Supabase or Nhost and need to leave, your data is Postgres. You run &lt;code&gt;pg_dump&lt;/code&gt;, you load it somewhere else, you update your connection strings. It's a project, not a crisis.&lt;/p&gt;

&lt;p&gt;If you outgrow Firestore, you're transforming a data model, not migrating rows. The access patterns you built around Firestore's model (the denormalization, the collection structure, the places where you stuffed data into subcollections to avoid a join) don't translate. It's months of work under pressure, usually when the thing that forced the move isn't waiting for you.&lt;/p&gt;

&lt;p&gt;That's not a dealbreaker for the right use cases. Offline-first mobile sync where the app needs to work through genuinely unreliable networks and reconcile cleanly when connectivity returns: Firebase is still the best answer here, and it's not close. Simple read-heavy access patterns at scale without relational complexity. Google ecosystem products where auth, analytics, and ML are already in Cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  PocketBase
&lt;/h2&gt;

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

&lt;p&gt;I'll just say it: &lt;a href="https://pocketbase.io/" rel="noopener noreferrer"&gt;PocketBase&lt;/a&gt; is one of my favorite pieces of software that exists right now.&lt;/p&gt;

&lt;p&gt;It's one Go binary. SQLite, realtime subscriptions, auth, file storage, admin UI. Runs on a $5 VPS. ~60k GitHub stars. &lt;strong&gt;One maintainer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That last part is both the appeal and the risk. The total system fits in one person's head, which means deployment is copying a file, the admin UI is actually good, and there are no abstractions hiding from you. For internal tools, admin panels, side projects, prototypes, hobby things that are more complex than a static site: PocketBase is more elegantly suited to these than anything else on this list by a significant margin.&lt;/p&gt;

&lt;p&gt;The ceiling is real too. SQLite means single node. There's no path to horizontal scaling, no managed hosting tier for when self-managing gets annoying. There was a meaningful auth-bypass CVE earlier this year, patched quickly and responsibly disclosed, handled well by the maintainer. But &lt;em&gt;"handled well by a single person"&lt;/em&gt; is a different risk profile than &lt;em&gt;"handled by a security team at a funded company,"&lt;/em&gt; and that's a real input to a production decision depending on what you're building.&lt;/p&gt;

&lt;p&gt;The failure mode I've watched happen: someone uses PocketBase as a placeholder while the project proves out, then migrates to something "real" after it does. That migration always happens under deadline, always takes longer than anyone estimated, and is always more painful than if they'd made the decision upfront. Better to either commit to it for what it actually is (a complete backend for things that fit on one node, which is honestly a bigger category than people admit) or pick something else from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  So How Do You Actually Pick
&lt;/h2&gt;

&lt;p&gt;Start with the data model. This is the decision that matters most and people skip over it to compare pricing tiers instead.&lt;/p&gt;

&lt;p&gt;If you need GraphQL, a strong permission layer, or your backend will serve AI agents: &lt;strong&gt;Nhost&lt;/strong&gt;. If you want the most mature REST-based Postgres BaaS with the largest ecosystem: go back to Supabase. If the database is your only unsolved problem and everything else is handled: &lt;strong&gt;Neon&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Document-shaped data with stable, simple access patterns: &lt;strong&gt;Firebase&lt;/strong&gt; or &lt;strong&gt;Appwrite&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then self-hosting. If it's a hard requirement, not a nice-to-have: Appwrite or PocketBase. Self-hosted Supabase exists but the full service stack is a real ops commitment. Not a one-afternoon project.&lt;/p&gt;

&lt;p&gt;Then realtime. If live synchronization is the core mechanic of what you're building and not just a feature checkbox, actually evaluate &lt;strong&gt;Convex&lt;/strong&gt;. Don't just put it in the table and move on.&lt;/p&gt;

&lt;p&gt;Before you commit to anything: run your actual schema through the top two options. Real tables, real auth flow, real queries from the app. Not a tutorial, not a hello world. The places where something breaks or needs a workaround are the information you need, and you won't get it from any comparison article including this one.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>database</category>
      <category>postgres</category>
    </item>
    <item>
      <title>We Built an Open-Source GraphQL Engine to Replace Hasura</title>
      <dc:creator>Max S.</dc:creator>
      <pubDate>Thu, 04 Jun 2026 15:13:37 +0000</pubDate>
      <link>https://dev.to/max4nhost/we-built-an-open-source-graphql-engine-to-replace-hasura-56l</link>
      <guid>https://dev.to/max4nhost/we-built-an-open-source-graphql-engine-to-replace-hasura-56l</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://github.com/nhost/nhost/tree/main/services/constellation" rel="noopener noreferrer"&gt;Constellation&lt;/a&gt; — an open-source GraphQL engine written in Go that is a near drop-in replacement for Hasura Community Edition. It reads the same metadata, generates the same schema, and serves the same GraphQL API. In production it uses &lt;strong&gt;~90% less memory than Hasura&lt;/strong&gt; with comparable or better latency, before any dedicated performance work.&lt;/p&gt;

&lt;p&gt;If you run Hasura today, this post is for you. If you build GraphQL infrastructure, care about open-source sustainability, or just want to see real production memory numbers from a fresh Go rewrite — keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with depending on someone else's engine
&lt;/h2&gt;

&lt;p&gt;Hasura v2 has been the backbone of Nhost's GraphQL layer for years. It's a genuinely good piece of engineering, and we built a lot on top of it.&lt;/p&gt;

&lt;p&gt;But three things changed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hasura v2 is winding down.&lt;/strong&gt; Development has shifted to Hasura v3, which takes a different approach to open source — the engine core is Apache-licensed, but the CLI, web console, and developer tooling are proprietary. That's a meaningful shift from v2, where the full stack was open. Continuing to depend on an engine that's winding down toward end-of-life, or migrating our GraphQL and permissions layer to one where the tooling is closed, weren't paths we were willing to take.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory was consistently our heaviest service per project.&lt;/strong&gt; A GraphQL engine sits between every client request and your database. It should be fast and lean. Instead, it was the most expensive thing we ran per project — and memory in particular is hard to throttle gracefully under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We didn't own a critical piece of our infrastructure.&lt;/strong&gt; The API and permissions layer sits at the center of every Nhost application. When something goes wrong, or when you want to ship something new, depending on someone else's roadmap is expensive in ways that don't show up in dashboards.&lt;/p&gt;

&lt;p&gt;So we built our own — while keeping full compatibility with Hasura on the GraphQL request path.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Constellation actually is
&lt;/h2&gt;

&lt;p&gt;Constellation is a GraphQL engine that turns a relational database into a role-based GraphQL API. It is designed to be a &lt;strong&gt;near drop-in replacement for Hasura Community Edition&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Concretely, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same metadata format.&lt;/strong&gt; It reads your existing Hasura metadata — either from a YAML/JSON file on disk, or straight from Hasura's &lt;code&gt;hdb_catalog.hdb_metadata&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same generated schema.&lt;/strong&gt; Same types, same relationships, same permission-aware shape per role. We verify this with byte-level schema diffs (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same request surface.&lt;/strong&gt; Queries, mutations, and subscriptions over the same &lt;code&gt;/v1/graphql&lt;/code&gt; endpoint, plus remote schemas and cross-source remote relationships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a fresh Go codebase — not a fork of Hasura. PostgreSQL and SQLite are first-class backends today. The connector design is built so additional databases can plug in without touching the query planner.&lt;/p&gt;

&lt;p&gt;Where Hasura's behavior is surprising or outright wrong, we occasionally diverge — and every such divergence is documented in &lt;a href="https://github.com/nhost/nhost/blob/main/services/constellation/KNOWN_DIFFERENCES.md" rel="noopener noreferrer"&gt;&lt;code&gt;KNOWN_DIFFERENCES.md&lt;/code&gt;&lt;/a&gt;. Correctness over surface area is one of our explicit goals.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers that made us ship it
&lt;/h2&gt;

&lt;p&gt;We ran Constellation against real production workloads, side by side with Hasura, on the same database and the same traffic. We didn't tune anything special for these results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory
&lt;/h3&gt;

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

&lt;p&gt;&lt;em&gt;Constellation's memory usage sits ~90% below Hasura's serving the same traffic. In absolute numbers: Hasura at ~180 MiB, Constellation at ~15 MiB. This is not a benchmark environment — these are production dashboards.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A note on the CPU panel in that chart: Constellation's CPU looks dramatically lower, but we're talking millicpus here. Both engines are cheap on CPU in absolute terms; the gap is real but small. Memory is where the meaningful difference lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency
&lt;/h3&gt;

&lt;p&gt;Across the same window, Constellation also serves requests a little faster.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Request latency over the same period: Hasura (blue) regularly spikes toward 60–80 ms, while Constellation (purple) mostly stays under 40 ms.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To make the comparison concrete, here's the per-period delta of Constellation minus Hasura for the same traffic. Anything in the green region (below zero) means Constellation was faster; the occasional red spike above the line marks the moments it wasn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcijydz2nukxx7g26diz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbcijydz2nukxx7g26diz.png" alt="Latency delta (Constellation minus Hasura)" width="799" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Latency delta (Constellation minus Hasura). Most of the line sits in the green, below-zero region, meaning Constellation was faster, with a handful of red spikes where it briefly wasn't.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These are raw production dashboards, not a curated benchmark. That's rather the point. We didn't tune anything special to get here.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works alongside Hasura (not instead of it — yet)
&lt;/h2&gt;

&lt;p&gt;Constellation doesn't yet implement Hasura's metadata HTTP API (&lt;code&gt;POST /v1/metadata&lt;/code&gt;). It &lt;em&gt;consumes&lt;/em&gt; metadata but doesn't &lt;em&gt;author&lt;/em&gt; it. So the current deployment model is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hasura owns metadata authoring. Constellation owns request serving.&lt;/strong&gt; Both point at the same database.&lt;/p&gt;

&lt;p&gt;In practice, an Nhost project running Constellation gets two endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://&amp;lt;subdomain&amp;gt;.hasura.&amp;lt;region&amp;gt;.nhost.run&lt;/code&gt; → Hasura (metadata authoring, plus anything Constellation doesn't serve yet)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://&amp;lt;subdomain&amp;gt;.graphql.&amp;lt;region&amp;gt;.nhost.run&lt;/code&gt; → Constellation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key property: &lt;strong&gt;you can flip Constellation on, run real traffic through it, and remove it at any time to fall back to Hasura with zero migration.&lt;/strong&gt; That's the whole appeal of the side-by-side model.&lt;/p&gt;

&lt;p&gt;Once Constellation supports metadata mutations on its own, the Hasura half can be dropped entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's solid vs. what's missing
&lt;/h2&gt;

&lt;p&gt;Constellation is &lt;strong&gt;alpha&lt;/strong&gt;. That label reflects &lt;em&gt;feature coverage&lt;/em&gt; against Hasura — not the stability of what's already there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solid and in production:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ PostgreSQL and SQLite connectors&lt;/li&gt;
&lt;li&gt;✅ Full GraphQL query, mutation, and subscription pipeline&lt;/li&gt;
&lt;li&gt;✅ JWT and admin-secret auth&lt;/li&gt;
&lt;li&gt;✅ Role-based permissions&lt;/li&gt;
&lt;li&gt;✅ Remote schemas&lt;/li&gt;
&lt;li&gt;✅ Cross-source remote relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not yet implemented:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Hasura Actions&lt;/li&gt;
&lt;li&gt;❌ Event and cron triggers&lt;/li&gt;
&lt;li&gt;❌ REST endpoints&lt;/li&gt;
&lt;li&gt;❌ Allowlists and query collections&lt;/li&gt;
&lt;li&gt;❌ Inherited roles, native queries, computed fields&lt;/li&gt;
&lt;li&gt;❌ MSSQL, BigQuery, Snowflake backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app lives mostly in the query/mutation/subscription world, there's a good chance Constellation already does what you need. If you lean heavily on Actions or event triggers, keep those on Hasura for now.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next — and how you can shape it
&lt;/h2&gt;

&lt;p&gt;The near-term roadmap is focused on closing the gap: implementing the metadata HTTP API so Constellation can own the full stack without Hasura running alongside it, expanding the feature surface into Actions and triggers, and eventually supporting additional database backends.&lt;/p&gt;

&lt;p&gt;But the most valuable thing right now isn't what's on our roadmap. It's real-world usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are four concrete ways you can help:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run real traffic through it.&lt;/strong&gt; Enable Constellation next to your existing Hasura instance (see below) and point production traffic at it. That's the fastest way to find real divergences and edge cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Report schema diffs.&lt;/strong&gt; If &lt;code&gt;nhost schema diff&lt;/code&gt; turns up a divergence that isn't covered in &lt;a href="https://github.com/nhost/nhost/blob/main/services/constellation/KNOWN_DIFFERENCES.md" rel="noopener noreferrer"&gt;&lt;code&gt;KNOWN_DIFFERENCES.md&lt;/code&gt;&lt;/a&gt;, open an issue with the diff hunk and a minimal metadata snippet. That's how new issues get documented — or fixed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Report runtime bugs.&lt;/strong&gt; A query that returns different results, a subscription that misbehaves, a permission that doesn't line up — under live traffic these are the most valuable reports we can get.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Read the code and contribute.&lt;/strong&gt; It's open source and we've worked hard to make it approachable — small interfaces, golden-file tests, and per-package documentation. PRs and questions are welcome.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;The lowest-friction path is enabling Constellation on any Nhost project. Add one block to &lt;code&gt;nhost/nhost.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[experimental.constellation]&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.4.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick the latest tag from &lt;a href="https://github.com/nhost/nhost/blob/main/services/constellation/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;. With that in place, your project runs both engines. Remove the block and you're back to Hasura.&lt;/p&gt;

&lt;p&gt;To verify Constellation generates the same schema as Hasura for each role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nhost schema dump &lt;span class="nt"&gt;--role&lt;/span&gt; user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;subdomain&amp;gt;.hasura.&amp;lt;region&amp;gt;.nhost.run/v1/graphql"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; schema.hasura.user.graphqls

nhost schema dump &lt;span class="nt"&gt;--role&lt;/span&gt; user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;subdomain&amp;gt;.graphql.&amp;lt;region&amp;gt;.nhost.run/v1/graphql"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; schema.constellation.user.graphqls

nhost schema diff &lt;span class="nt"&gt;-a&lt;/span&gt; schema.hasura.user.graphqls &lt;span class="nt"&gt;-b&lt;/span&gt; schema.constellation.user.graphqls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty diff means Constellation produced a byte-equivalent schema for that role. Any non-empty diff should map to a known difference — and if it doesn't, that's exactly the kind of issue we want to hear about.&lt;/p&gt;

&lt;p&gt;Prefer to run it standalone without an Nhost project? The &lt;a href="https://github.com/nhost/nhost/tree/main/services/constellation" rel="noopener noreferrer"&gt;repository README&lt;/a&gt; has full build and run instructions.&lt;/p&gt;

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