<?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: Rahil Pirani</title>
    <description>The latest articles on DEV Community by Rahil Pirani (@rahil_pirani_c48446facc8c).</description>
    <link>https://dev.to/rahil_pirani_c48446facc8c</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%2F3922851%2F562316ad-6070-4ae8-9c4c-772035064295.png</url>
      <title>DEV Community: Rahil Pirani</title>
      <link>https://dev.to/rahil_pirani_c48446facc8c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rahil_pirani_c48446facc8c"/>
    <language>en</language>
    <item>
      <title>The challenges of creating a semantic memory layer on Cloudflare Workers, D1, and Vectorize.</title>
      <dc:creator>Rahil Pirani</dc:creator>
      <pubDate>Sat, 06 Jun 2026 11:23:52 +0000</pubDate>
      <link>https://dev.to/rahil_pirani_c48446facc8c/the-challenges-of-creating-a-semantic-memory-layer-on-cloudflare-workers-d1-and-vectorize-3c7a</link>
      <guid>https://dev.to/rahil_pirani_c48446facc8c/the-challenges-of-creating-a-semantic-memory-layer-on-cloudflare-workers-d1-and-vectorize-3c7a</guid>
      <description>&lt;p&gt;The main concept is straightforward: embed text, store the vector, and query it later. The time-consuming part was everything else. &lt;/p&gt;

&lt;p&gt;I created a memory layer that maintains context across AI tools using Cloudflare Workers, D1, Vectorize, and Workers AI. All this operates on the free tier. Here’s what I didn’t realize at first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two stores, kept strictly separate
&lt;/h2&gt;

&lt;p&gt;D1 stores structured entry data, including content, tags, timestamps, importance scores, and the exact vector IDs put into Vectorize. Vectorize holds the embeddings, linked by UUID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;D1Database&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;VECTORIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VectorizeIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;AI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ai&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;AUTH_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Don’t consider Vectorize your source of truth for content. It functions as a lookup index. D1 is the database. This distinction becomes crucial when updating or deleting data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chunk at sentence boundaries, not character counts
&lt;/h2&gt;

&lt;p&gt;Long entries split before embedding. Breaking on character counts can lose semantic context. The solution is to look back for the nearest sentence or newline break before deciding on a split point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;chunkText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;overlapChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastPeriod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastNewline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;breakPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastPeriod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastNewline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;breakPoint&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;breakPoint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlapChars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;Each chunk receives its own Vectorize vector with a &lt;code&gt;parentId&lt;/code&gt; in the metadata linking back to the D1 entry. Keep track of the exact vector IDs returned and save them in D1 since Vectorize doesn’t support a "delete where parentId = x" operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&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;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`UPDATE entries SET vector_ids = ? WHERE id = ?`&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vectorIds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  One Vectorize query, three decisions
&lt;/h2&gt;

&lt;p&gt;With each write, a single embed and Vectorize query manage duplicate detection, contradiction detection, and merge decisions in one round trip. Three score bands dictate the next steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DUPLICATE_BLOCK_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DUPLICATE_FLAG_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CANDIDATE_SCORE_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.45&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;&amp;gt;= 0.95:&lt;/strong&gt; Exact or near-exact duplicate. Block it and skip the LLM entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0.85 to 0.95:&lt;/strong&gt; Similar enough to require a decision. A combined prompt goes to the LLM asking for one of four actions: &lt;code&gt;contradiction&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt;, or &lt;code&gt;keep_both&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;0.45 to 0.85:&lt;/strong&gt; These are candidates for contradiction only. A lighter prompt is used, with no merge logic.&lt;/p&gt;

&lt;p&gt;The combined prompt in the flagged band looks like this:&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="err"&gt;Choose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exactly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;action.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Prioritise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;order:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contradiction"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DIRECTLY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CONFLICTS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;existing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;memory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;clearly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;supersedes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;existing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merge"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;both&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;memories&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;complementary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;better&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;one&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;combined&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entry&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"keep_both"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;memories&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;are&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;different&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;enough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;coexist&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Respond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;only.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"keep_both"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"contradiction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"conflicting_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;10 words max&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"target_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;OR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"merge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"target_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"merged_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;text&amp;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;You must validate the returned ID against the candidate set before taking action. LLMs can generate incorrect IDs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stale vector cleanup is not optional
&lt;/h2&gt;

&lt;p&gt;When merging occurs, write the new canonical entry and delete both originals. Deleting from D1 is straightforward. Deleting from Vectorize requires the exact IDs you saved earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVectorIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VECTORIZE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteByIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldVectorIds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you skip this, orphaned vectors will silently accumulate. They will appear in recall results, inflate scores, and create matches pointing to entries that no longer exist in D1. This issue is hard to diagnose but easy to prevent.&lt;/p&gt;

&lt;p&gt;For updates, the safe approach is to insert new vectors first, confirm success, and then delete the old ones. Reversing that order can lead to lost data if the insert fails after the delete succeeds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cosine similarity is not enough for reranking
&lt;/h2&gt;

&lt;p&gt;Raw Vectorize scores are based on cosine similarity. Once entries vary in age and access frequency, this becomes a poor measure of relevance. The reranker applies three multipliers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recency:&lt;/strong&gt; Exponential decay based on tag-aware half-lives. Tasks decay in 7 days, work entries in 3 months, and context entries in 6 months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frequency:&lt;/strong&gt; Uses &lt;code&gt;log1p&lt;/code&gt; of recall count, allowing frequently accessed entries to surface higher without overshadowing newer ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Importance:&lt;/strong&gt; A score from 1 to 5 based on a separate LLM pass during write, scaled to a multiplier of 0.88 to 1.20 so high-importance entries can surpass the recency cap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;ageMs&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;halfLifeMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frequencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log1p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importanceMultiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imp&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;recencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;frequencyMultiplier&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;importanceMultiplier&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short appends and rolled-up entries will receive a penalty to keep noise out of the top results.&lt;/p&gt;




&lt;h2&gt;
  
  
  The topK multiplier problem
&lt;/h2&gt;

&lt;p&gt;Multiply your topK by at least 3 before deduplicating by parentId. If an entry has 4 chunks and you query &lt;code&gt;topK: 5&lt;/code&gt;, those 4 chunks can take most of your result budget before you have seen enough unique parents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VECTORIZE_TOP_K_MULTIPLIER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query wider and then deduplicate by parentId in your application code.&lt;/p&gt;




&lt;p&gt;The full implementation is open source if you want to explore any of these topics: &lt;a href="https://github.com/rahilp/second-brain-cloudflare" rel="noopener noreferrer"&gt;github.com/rahilp/second-brain-cloudflare&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>vectordatabase</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I launched an open source tool on Product Hunt with no budget, no team, and no audience. Here's what actually happened.</title>
      <dc:creator>Rahil Pirani</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:29:37 +0000</pubDate>
      <link>https://dev.to/rahil_pirani_c48446facc8c/i-launched-an-open-source-tool-on-product-hunt-with-no-budget-no-team-and-no-audience-heres-57c3</link>
      <guid>https://dev.to/rahil_pirani_c48446facc8c/i-launched-an-open-source-tool-on-product-hunt-with-no-budget-no-team-and-no-audience-heres-57c3</guid>
      <description>&lt;p&gt;Three weeks before launch, second-brain-cloudflare had 0 stars on GitHub. &lt;/p&gt;

&lt;p&gt;Yesterday it reached 168. &lt;/p&gt;

&lt;p&gt;This is an honest account of how that happened: what was planned, what was improvised, and what I'd advise someone doing this for the first time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built and why
&lt;/h2&gt;

&lt;p&gt;Every AI session starts from zero. Projects, decisions, preferences go away the moment the window closes. I grew frustrated enough to build a solution: a memory layer that persists across Claude, ChatGPT, Cursor, and any MCP-compatible tool. I self-hosted it on Cloudflare's free tier. It uses semantic search instead of keyword matching.&lt;/p&gt;

&lt;p&gt;I built it for myself. Then I open-sourced it because this problem wasn’t mine alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three weeks before launch
&lt;/h2&gt;

&lt;p&gt;Most Product Hunt launch advice focuses on launch day. The three weeks before it matter more.&lt;/p&gt;

&lt;p&gt;I posted on Reddit, participated in Discord communities, released four versions (v1.3 to v1.6), created an Obsidian plugin, wrote technical articles, and directly messaged builders. None of this was organized around a launch date. I was just trying to show it to people who might use it.&lt;/p&gt;

&lt;p&gt;Stars grew slowly: 50 by May 13, 80 by May 23, and 90 by May 29.&lt;/p&gt;

&lt;p&gt;That slow buildup matters. The Product Hunt launch didn’t create momentum; it boosted the momentum that already existed. I don’t believe a launch on day one would have resulted in even a third of those numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding the right hunter
&lt;/h2&gt;

&lt;p&gt;I got lucky here, and I'm honest about that.&lt;/p&gt;

&lt;p&gt;fmerian has 67K followers on Product Hunt. He hunted Mastra, which eventually got 20K GitHub stars. When I reached out, he asked one question: "What's your goal with this launch?" Not "what does your product do" but what's your goal.&lt;/p&gt;

&lt;p&gt;That question forced me to be specific: I wanted awareness among developers frustrated with siloed AI memory, GitHub stars, and a contributor community. I sought feedback from builders using it across various tools, not monetization—community first.&lt;/p&gt;

&lt;p&gt;He suggested launching on a Sunday. Less competition means more developers on Product Hunt during weekends. That advice alone was worth more than any paid promotion.&lt;/p&gt;

&lt;p&gt;If you're planning a Product Hunt launch, find someone who has done it recently, understands your space, and will engage with your product. The relationship matters more than the number of followers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Launch day
&lt;/h2&gt;

&lt;p&gt;I was up at 3 AM.&lt;/p&gt;

&lt;p&gt;The first few hours were mostly waiting—ranks are hidden during a randomization period. By 4 AM, we had 9 upvotes and a few notable early voters. By mid-morning, the comments began coming in.&lt;/p&gt;

&lt;p&gt;Here’s a piece of advice that differs from most launch guides: reply to every comment as if it were a one-on-one conversation. Don’t treat it like a support ticket or just say thank you. Provide a real response.&lt;/p&gt;

&lt;p&gt;Builders asked sharp questions about conflict resolution, data ownership, multi-client memory, and temporal recall. Those weren’t support requests; they were the real product conversation I had been trying to have for three weeks. Taking each question seriously resulted in better follow-up engagement than any post I made that day.&lt;/p&gt;

&lt;p&gt;Final result: #3 Product of the Day. 253 upvotes. 46 comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the star chart actually shows
&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%2F0ctcn2x1sx8cn0e79pbx.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%2F0ctcn2x1sx8cn0e79pbx.png" alt=" " width="800" height="804"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That near-vertical line on May 31 represents roughly 80 stars in a single day. But the shape before it is what I keep examining: three weeks of slow, steady, organic growth. The launch caused a spike. The work before it laid the foundation.&lt;/p&gt;

&lt;p&gt;Without that foundation, the spike probably wouldn’t happen. Or it might happen and then immediately flatline with no retention.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;I would start community conversations earlier and lead with questions instead of announcements.&lt;/p&gt;

&lt;p&gt;Most of my pre-launch posts were “here's what I built.” The Product Hunt comment threads showed me what people really wanted to discuss—conflict resolution, data ownership, whether this works across accounts and devices. If I had led with those questions earlier, I would have built a more engaged audience by launch day.&lt;/p&gt;

&lt;p&gt;Another thing: don’t treat launch day as the finish line. The comments kept coming in the next morning. The GitHub stars kept rising. The launch opened a door—what happens after it depends entirely on what you do in the first 48 hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it goes from here
&lt;/h2&gt;

&lt;p&gt;168 stars. 28 forks. Two roadmap features came directly from Product Hunt comments.&lt;/p&gt;

&lt;p&gt;Building in public. Everything is open source at github.com/rahilp/second-brain-cloudflare.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>programming</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI memory has a contradiction problem nobody is talking about</title>
      <dc:creator>Rahil Pirani</dc:creator>
      <pubDate>Mon, 25 May 2026 06:53:45 +0000</pubDate>
      <link>https://dev.to/rahil_pirani_c48446facc8c/ai-memory-has-a-contradiction-problem-nobody-is-talking-about-8me</link>
      <guid>https://dev.to/rahil_pirani_c48446facc8c/ai-memory-has-a-contradiction-problem-nobody-is-talking-about-8me</guid>
      <description>&lt;p&gt;Most discussions about AI memory focus on a few main concerns: whether it lasts across sessions, how quickly it retrieves information, and whether it can scale. These are important questions. However, there’s a simpler issue that often gets overlooked, and it slowly worsens memory systems over time.&lt;/p&gt;

&lt;p&gt;What happens when two stored memories conflict?&lt;/p&gt;

&lt;p&gt;You tell your AI assistant that you prefer short, direct answers. A month later, you mention wanting more detailed explanations with examples. Both preferences get stored. Now, every recall brings up both. The system tries to accommodate both, but neither aligns with what you actually want at that moment.&lt;/p&gt;

&lt;p&gt;This isn’t just a hypothetical situation. It happens with any memory system that only adds information over time. Your preferences change. Your situation evolves. But the earlier version of you is still there, pushing in the opposite direction.&lt;/p&gt;




&lt;p&gt;Most people default to using a review inbox. It identifies conflicts and lets the user decide. It sounds good in theory but is frustrating in practice.&lt;/p&gt;

&lt;p&gt;No one wants to manage their AI's memory manually. The goal is for it to work in the background. A review inbox turns memory management into a task that often gets ignored, leading to a buildup of contradictions anyway.&lt;/p&gt;

&lt;p&gt;Another common method is timestamp-based overwriting: when new information comes in, it checks for similarities and replaces the old. But similarity doesn’t equal contradiction. "I work best in the mornings" and "I do my best thinking late at night" may be very different but share low similarity. A vector search won’t catch this. Both get stored and recalled.&lt;/p&gt;




&lt;p&gt;The right question isn’t "how do we find similarities?" It should be "how do we identify logical incompatibility?"&lt;/p&gt;

&lt;p&gt;This is a semantic reasoning challenge, not just a retrieval one. Two memories might not seem similar, yet can still contradict each other. The only way to recognize this is with a language model, not through distance metrics.&lt;/p&gt;

&lt;p&gt;When we integrated contradiction detection into &lt;a href="https://github.com/rahilp/second-brain-cloudflare" rel="noopener noreferrer"&gt;second-brain&lt;/a&gt;, our key design choice was to use a large language model (LLM) to check if new memories contradict any of the most recently recalled ones. We inquire not only "is this similar?" but "can both of these be true at the same time?"&lt;/p&gt;

&lt;p&gt;When a conflict arises, the new memory prevails. The old one gets deleted entirely, from both storage and the vector index. It's gone. The new memory is the only version that exists.&lt;/p&gt;




&lt;p&gt;There’s a real trade-off worth noting. Conditional preferences can be tricky.&lt;/p&gt;

&lt;p&gt;For example, "I want short responses when I’m coding, long ones when I’m strategizing" isn’t a contradiction. Those statements can coexist. An unsophisticated LLM check might flag them as conflicting. To get this right, enough context needs to be passed through the check so the model can distinguish between real conflicts and situational variations.&lt;/p&gt;

&lt;p&gt;This is a more complex issue, and the current implementation doesn't address it entirely. It handles clear cases well: factual contradictions, changes in preferences, updated decisions. The conditional cases represent a known gap.&lt;/p&gt;

&lt;p&gt;However, catching the clear cases already makes a significant difference. A memory system that sometimes overlooks nuanced conditions is still better than one that continuously accumulates contradictions without end.&lt;/p&gt;




&lt;p&gt;Storage is the easy part of AI memory; everyone can provide it. What truly matters for long-term usefulness is coherence over time, not just a lot of noise. To achieve coherence, contradictions must be treated as a primary issue, not just a task to clean up later.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built persistent AI memory for Claude on Cloudflare's free tier</title>
      <dc:creator>Rahil Pirani</dc:creator>
      <pubDate>Wed, 20 May 2026 04:45:51 +0000</pubDate>
      <link>https://dev.to/rahil_pirani_c48446facc8c/i-built-persistent-ai-memory-for-claude-on-cloudflares-free-tier-12kc</link>
      <guid>https://dev.to/rahil_pirani_c48446facc8c/i-built-persistent-ai-memory-for-claude-on-cloudflares-free-tier-12kc</guid>
      <description>&lt;p&gt;Every Claude session starts fresh. You copy context, explain your setup, reintroduce your project, and then do it all over again the next day. I got tired of this and created a solution.&lt;/p&gt;

&lt;p&gt;second-brain-cloudflare is a self-hosted MCP server that provides Claude, ChatGPT, Cursor, and any MCP-compatible client with persistent memory across sessions. It operates entirely on Cloudflare's free tier. Here’s how it works.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Workers&lt;/strong&gt;: MCP server, REST API, and web UI, all from one &lt;code&gt;wrangler deploy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D1 (SQLite)&lt;/strong&gt;: stores entry content, tags, source, timestamps, and vector chunk IDs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vectorize&lt;/strong&gt;: the vector index (bge-small-en-v1.5, 384 dimensions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers AI&lt;/strong&gt;: &lt;code&gt;bge-small-en-v1.5&lt;/code&gt; for embeddings,
&lt;code&gt;@cf/meta/llama-4-scout-17b-16e-instruct&lt;/code&gt; for web UI synthesis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One deployment. No external databases. No API keys needed beyond your Cloudflare account token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tag-based time-decay reranking
&lt;/h2&gt;

&lt;p&gt;Pure vector similarity has a drawback. A memory from three months ago can outrank something you saved yesterday if it’s semantically closer. The solution is to fetch three times more candidates than needed (topK=5 pulls 15), then score each using a tag-aware half-life:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tasks: 7-day half-life&lt;/li&gt;
&lt;li&gt;Work: 3-month half-life&lt;/li&gt;
&lt;li&gt;Context: 6-month half-life&lt;/li&gt;
&lt;li&gt;Default: 30-day half-life&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;adjusted_score = cosine_similarity × e^(-age_in_days / half_life)&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Duplicate detection
&lt;/h2&gt;

&lt;p&gt;Before storing anything, embed the incoming content and query Vectorize for its nearest neighbor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Score ≥ 95%: block&lt;/li&gt;
&lt;li&gt;Score 85–94%: store with &lt;code&gt;duplicate-candidate&lt;/code&gt; tag&lt;/li&gt;
&lt;li&gt;Score &amp;lt; 85%: store normally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this step, Claude creates 20–30 nearly identical entries for the same decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart chunking
&lt;/h2&gt;

&lt;p&gt;Long notes split at sentence ends, with a 200-character overlap. Each chunk receives its own vector. Chunk IDs are stored in D1, so forget() reliably removes all related vectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Temporal recall (v1.2.0)
&lt;/h2&gt;

&lt;p&gt;Queries now support time limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recall("API decisions", after="7 days ago")&lt;/li&gt;
&lt;li&gt;recall("standup notes", after="2026-05-12")
Supports: "today", "yesterday", "last week", "this month", ISO dates, and epoch timestamps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI synthesis in the web UI
&lt;/h2&gt;

&lt;p&gt;Queries flow through &lt;code&gt;@cf/meta/llama-4-scout-17b-16e-instruct&lt;/code&gt; before being rendered. Answers stream in real time, with source memories that can be collapsed underneath. You’ll find Append and Forget buttons. This runs on your own Cloudflare account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the free tier works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;D1: 5GB storage, 5 million row reads per day&lt;/li&gt;
&lt;li&gt;Vectorize: 5 million vectors, 30 million queried dimensions per month (adequate for team scale but fine for personal use)&lt;/li&gt;
&lt;li&gt;Workers AI: 10,000 Neurons per day&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Deploy: &lt;a href="https://thesecondbrain.dev" rel="noopener noreferrer"&gt;https://thesecondbrain.dev&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/rahilp/second-brain-cloudflare" rel="noopener noreferrer"&gt;https://github.com/rahilp/second-brain-cloudflare&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If this was helpful, please give it a star.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloudflare</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I gave Claude a persistent memory for $0/month using Cloudflare</title>
      <dc:creator>Rahil Pirani</dc:creator>
      <pubDate>Sun, 10 May 2026 05:35:41 +0000</pubDate>
      <link>https://dev.to/rahil_pirani_c48446facc8c/i-gave-claude-a-persistent-memory-for-0month-using-cloudflare-2e5a</link>
      <guid>https://dev.to/rahil_pirani_c48446facc8c/i-gave-claude-a-persistent-memory-for-0month-using-cloudflare-2e5a</guid>
      <description>&lt;h1&gt;
  
  
  I gave Claude a persistent memory for $0/month using Cloudflare
&lt;/h1&gt;

&lt;p&gt;Claude is great. But every time you start a new conversation, it forgets everything. Your projects, your preferences, what you decided last week — gone.&lt;/p&gt;

&lt;p&gt;The official memory feature exists, but it's vague and you can't really control it. You can't query it, tag it, or search it semantically. It's a black box that occasionally surfaces something useful.&lt;/p&gt;

&lt;p&gt;So I built my own.&lt;/p&gt;

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

&lt;p&gt;It's a self-hosted MCP server that runs on Cloudflare Workers. Four tools: &lt;code&gt;remember&lt;/code&gt;, &lt;code&gt;recall&lt;/code&gt;, &lt;code&gt;list_recent&lt;/code&gt;, &lt;code&gt;forget&lt;/code&gt;. Claude calls them automatically. You never think about it.&lt;/p&gt;

&lt;p&gt;The interesting part is how recall works — it's not keyword search. Every note gets embedded as a 384-dimensional vector using &lt;code&gt;bge-small-en-v1.5&lt;/code&gt; on Workers AI. When you ask Claude something, it searches by &lt;em&gt;meaning&lt;/em&gt;, not exact words.&lt;/p&gt;

&lt;p&gt;Store: &lt;em&gt;"users drop off at the payment step."&lt;/em&gt;&lt;br&gt;&lt;br&gt;
Query: &lt;em&gt;"onboarding problems."&lt;/em&gt;&lt;br&gt;&lt;br&gt;
It finds it. No keyword overlap needed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Cloudflare
&lt;/h2&gt;

&lt;p&gt;Honestly, cost. The whole stack — Workers, D1 (SQLite), Vectorize, Workers AI embeddings — runs on Cloudflare's free tier at personal scale. You don't even need a credit card to get started.&lt;/p&gt;

&lt;p&gt;The other reason is deployment. There's a one-click deploy button that provisions everything automatically. It takes about 3 minutes to go from zero to a running second brain connected to Claude Desktop.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to set it up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Deploy&lt;/strong&gt; — click the button in the repo, Cloudflare provisions D1 + Vectorize and deploys the Worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Run the schema&lt;/strong&gt; — one SQL snippet in the Cloudflare dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set your auth token&lt;/strong&gt; — one command with wrangler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Connect Claude Desktop&lt;/strong&gt; — add a few lines to your config JSON:&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;"mcpServers"&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;"second-brain"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&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="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;your-worker-url&amp;gt;/mcp"&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;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's it. Claude now has persistent memory across every conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually use it for
&lt;/h2&gt;

&lt;p&gt;I have Claude set up to call &lt;code&gt;recall&lt;/code&gt; at the start of every conversation, before it says anything. So when I open a new chat and say "continue the onboarding work from last week," it already knows what that means.&lt;/p&gt;

&lt;p&gt;I also capture from everywhere — there's a browser bookmarklet that saves any highlighted text or page with one click, and iOS Shortcuts for voice capture on the go. "Hey Siri, brain dump" and I can dictate a note that shows up in Claude's memory immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (yet)
&lt;/h2&gt;

&lt;p&gt;There's no UI for browsing your memory. You can hit the &lt;code&gt;/list&lt;/code&gt; endpoint, but it's raw JSON. I want to build a proper dashboard eventually — something that shows your memory visually, lets you edit or delete entries, maybe shows what Claude has recalled most often.&lt;/p&gt;

&lt;p&gt;Also, the local dev experience is slightly annoying because Vectorize and Workers AI don't run locally — you end up pointing at remote resources for real testing. Not a dealbreaker, but worth knowing.&lt;/p&gt;

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

&lt;p&gt;Everything is open source under MIT. One-click deploy, manual setup instructions, iOS Shortcuts templates, bookmarklet source — it's all there.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/rahilp/second-brain-cloudflare" rel="noopener noreferrer"&gt;github.com/rahilp/second-brain-cloudflare&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use it, I'd genuinely like to know what you end up storing in it. That's the part I'm most curious about — what people actually find worth remembering.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>cloudflare</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
