<?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: XNeuronal</title>
    <description>The latest articles on DEV Community by XNeuronal (@xneuronal).</description>
    <link>https://dev.to/xneuronal</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%2F4093542%2Fd7e5425e-66b1-48ea-a4d2-739e8571f6cd.png</url>
      <title>DEV Community: XNeuronal</title>
      <link>https://dev.to/xneuronal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xneuronal"/>
    <language>en</language>
    <item>
      <title>I put reminders, contacts and recipes in the same Postgres table. Here is the SQL, and what it cost.</title>
      <dc:creator>XNeuronal</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:07:20 +0000</pubDate>
      <link>https://dev.to/xneuronal/i-put-reminders-contacts-and-recipes-in-the-same-postgres-table-here-is-the-sql-and-what-it-cost-5go</link>
      <guid>https://dev.to/xneuronal/i-put-reminders-contacts-and-recipes-in-the-same-postgres-table-here-is-the-sql-and-what-it-cost-5go</guid>
      <description>&lt;p&gt;Every database course tells you to give each kind of thing its own table. Customers here, orders there, products over here. I did the opposite, on purpose, and four months later I still would.&lt;/p&gt;

&lt;p&gt;Here is the schema, the query that justifies it, and the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: you do not know what a memory is when it arrives
&lt;/h2&gt;

&lt;p&gt;I am building an assistant whose whole job is remembering. Someone says a sentence, it keeps what matters and can find it again later.&lt;/p&gt;

&lt;p&gt;Take a perfectly ordinary sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Saw Dr Guy on Thursday, need another blood test in three months."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What is it?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;reminder&lt;/strong&gt;, because there is a deadline.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;contact&lt;/strong&gt;, because there is a doctor's name.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;fact&lt;/strong&gt;, because the appointment happened.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;task&lt;/strong&gt;, because someone has to book the next one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four. And which one matters depends on the &lt;em&gt;next&lt;/em&gt; sentence, not this one.&lt;/p&gt;

&lt;p&gt;With one table per kind, you must decide at write time. Decide early and you are wrong often: half of what people tell a memory fits no box cleanly, and the other half fits several.&lt;/p&gt;

&lt;p&gt;There is a worse consequence. Splitting into tables splits your search. Looking for "doctor" becomes four queries against four tables, then you merge and rank the results yourself. A memory never searches inside a category. It searches everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema
&lt;/h2&gt;

&lt;p&gt;One table. One row per memory. A &lt;code&gt;type&lt;/code&gt; column tells them apart.&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;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;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;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&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;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="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;        &lt;span class="nb"&gt;text&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;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'reminder_short'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reminder_long'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'task'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'memo'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'contact'&lt;/span&gt;
              &lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;metadata&lt;/span&gt;    &lt;span class="n"&gt;jsonb&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="s1"&gt;'{}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt;   &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;due_at&lt;/span&gt;      &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;      &lt;span class="nb"&gt;text&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="s1"&gt;'active'&lt;/span&gt;
              &lt;span class="k"&gt;check&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;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'archived'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;created_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="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="k"&gt;constraint&lt;/span&gt; &lt;span class="n"&gt;neurons_owner_present&lt;/span&gt;
    &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;is&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;or&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&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;Deliberately poor. &lt;code&gt;content&lt;/code&gt; is the memory in the speaker's own words, never rewritten. &lt;code&gt;metadata&lt;/code&gt; is a bag whose shape depends on &lt;code&gt;type&lt;/code&gt;: checklist items with their checked state, a phone number, an address, the sources of a web search.&lt;/p&gt;

&lt;p&gt;Note &lt;code&gt;neurons_owner_present&lt;/code&gt;. A row belongs to an account &lt;strong&gt;or&lt;/strong&gt; to a device, never to neither. That single constraint is what makes anonymous use real rather than a demo mode: memories created before signing up are stored in the same table, with the same columns and the same search. Signing up does not import anything, it swaps a device id for a user id on rows that already exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The column that pays for everything
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;embedding&lt;/code&gt; is why the whole bet works.&lt;/p&gt;

&lt;p&gt;It holds 1536 floats produced by an embedding model. Individually meaningless; together they place the text in a space where distance means difference in meaning. "The doctor" lands near "Dr Guy" without sharing a single character.&lt;/p&gt;

&lt;p&gt;Now the important part: &lt;strong&gt;the embedding is computed for every row, whatever its type&lt;/strong&gt;. So semantic search crosses categories for free:&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="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&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="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
  &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;user_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;2&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;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query. One index. It returns a reminder, a contact and a recipe, ranked by relevance, and nothing in the code had to decide where to look.&lt;/p&gt;

&lt;p&gt;With four tables you need four vector indexes, four queries, and a hand-written merge over scores that are not comparable across tables. That is not just more code. It ranks worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  The indexes tell you the real access pattern
&lt;/h2&gt;

&lt;p&gt;Four indexes, and three of them are partial:&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;index&lt;/span&gt; &lt;span class="n"&gt;neurons_user_active_idx&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt; &lt;span class="p"&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="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="k"&gt;is&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;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;neurons_device_active_idx&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="k"&gt;is&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;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;neurons_due_at_idx&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;due_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;due_at&lt;/span&gt; &lt;span class="k"&gt;is&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;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;neurons_embedding_idx&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;ivfflat&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A partial index is a confession about usage: it says most queries only ever touch that slice. Done and archived rows still exist and are still findable, just slower, and that is the correct trade.&lt;/p&gt;

&lt;p&gt;The fourth one is different in kind. &lt;code&gt;ivfflat&lt;/code&gt; clusters vectors into 100 lists, finds the nearest lists, and only compares inside them. It is an index that agrees to be slightly wrong in exchange for being roughly a hundred times faster. For exact lookups that would be unacceptable. For similarity, where "right answer" is already a matter of degree, the loss is not perceptible.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing: &lt;code&gt;ivfflat&lt;/code&gt; needs rows to exist before it can build meaningful clusters. Creating it on an empty table and never reindexing gives you a bad index that silently degrades recall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Be honest about the bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database stops guarding meaning.&lt;/strong&gt; A contact and a reminder have identical columns, so Postgres cannot require a phone number on contacts or a &lt;code&gt;due_at&lt;/code&gt; on reminders. Those rules now live in application code, which means they hold exactly as long as your code is correct. That is a real loss, and the usual reason to keep one table per kind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;metadata&lt;/code&gt; is where the debt accrues.&lt;/strong&gt; Anything fits, so anything goes in. Six months later the same fact exists in three shapes depending on which code path wrote it. Ask me how I know.&lt;/p&gt;

&lt;p&gt;The only discipline left is that &lt;code&gt;check&lt;/code&gt; on &lt;code&gt;type&lt;/code&gt;. It is not much. It is also the thing that stops the whole table dissolving into a soup.&lt;/p&gt;

&lt;p&gt;The honest framing: I traded per-kind rigour for whole-set search. For a memory, search &lt;em&gt;is&lt;/em&gt; the product. For an invoicing system the answer would flip, and it would be just as correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  A thirty-eight minute detour: get out of &lt;code&gt;public&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Thirty-eight minutes after creating the table I moved it, unchanged, into a schema named after the product.&lt;/p&gt;

&lt;p&gt;If you run Postgres behind PostgREST, anything in &lt;code&gt;public&lt;/code&gt; is &lt;strong&gt;exposed automatically&lt;/strong&gt;. Row-level security still filters who reads what, and you absolutely still need it, but the table's existence is discoverable at a predictable URL. In the default configuration, creating a table publishes its name.&lt;/p&gt;

&lt;p&gt;That is not a vulnerability, it is a design choice that helps most of the time. The problem is the direction of the default: doing nothing exposes you, and you have to act to be private.&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;schema&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neurons&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt; &lt;span class="n"&gt;xneuronal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then expose &lt;code&gt;xneuronal&lt;/code&gt; explicitly and leave &lt;code&gt;public&lt;/code&gt; empty. A table someone forgets in a corner is now invisible from outside. On the client side you pay for it by naming the schema on every call:&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xneuronal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;neurons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to be clear, because the confusion is common: &lt;strong&gt;a private schema is not a substitute for row-level security&lt;/strong&gt;. It is not a lock, it is not writing the address on the door. You want both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four months on
&lt;/h2&gt;

&lt;p&gt;The table grew. It gained columns I did not foresee, neighbours, rules. Nothing that was added has challenged the noon decision.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;type&lt;/code&gt; accepts more than six values now. &lt;code&gt;metadata&lt;/code&gt; became a genuine subject with its own rules and accidents. What has not moved: one row per memory, one table for all of it, and a search that crosses categories because it never had to care about them.&lt;/p&gt;




&lt;p&gt;If you are designing something similar: the question is not "one table or many". It is &lt;strong&gt;whether your product's core operation is per-kind or across-kinds&lt;/strong&gt;. Answer that first, and the schema follows.&lt;/p&gt;

&lt;p&gt;I write about how this assistant gets built, mistakes included, at &lt;a href="https://www.xneuronal.com" rel="noopener noreferrer"&gt;xneuronal.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
