<script data-pm-proxy="intercept"></script><?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Marco Bambini]]></title><description><![CDATA[SQLite Cloud founder (https://sqlitecloud.io). Gravity programming language author (https://github.com/marcobambini/gravity). Hacker since I was a child. Tennis player.]]></description><link>https://marcobambini.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!V0IR!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb355754d-1ac4-4b81-935c-4662e9da991d_638x638.jpeg</url><title>Marco Bambini</title><link>https://marcobambini.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 05 Sep 2026 07:26:48 GMT</lastBuildDate><atom:link href="/__u/marcobambini.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Marco Bambini]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[marcobambini@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[marcobambini@substack.com]]></itunes:email><itunes:name><![CDATA[Marco Bambini]]></itunes:name></itunes:owner><itunes:author><![CDATA[Marco Bambini]]></itunes:author><googleplay:owner><![CDATA[marcobambini@substack.com]]></googleplay:owner><googleplay:email><![CDATA[marcobambini@substack.com]]></googleplay:email><googleplay:author><![CDATA[Marco Bambini]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[The WASTE inference engine]]></title><description><![CDATA[Running Kimi K3 2.78T parameter with 29GB of RAM]]></description><link>https://marcobambini.substack.com/p/the-waste-inference-engine</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-waste-inference-engine</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Thu, 30 Jul 2026 13:18:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!V0IR!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb355754d-1ac4-4b81-935c-4662e9da991d_638x638.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today we are releasing <a href="https://github.com/sqliteai/waste">WASTE</a>, an open-source inference engine designed to run models whose weights are substantially larger than the memory available on the host machine.</p><p>The first model fully supported by WASTE is Kimi K3, a 2.78 trillion parameter mixture-of-experts model. We converted the original 1.42 TB checkpoint into a 982 GB container and ran the complete model on a MacBook Pro with 64 GB of unified memory. <strong>This is not a distilled, pruned, or reduced variant</strong><span>.</span></p><p>A typical execution currently looks like this:</p><pre><code><code>$ waste run ~/models/k3.waste "What is the capital of Italy?"

waste: no --budget, using 46.24 GB of 64.00 GB
       (expert cache 17.56 GB)

The capital of Italy is Rome.

[16 tokens, 49.31 s, 0.32 tok/s]
</code></code></pre><p>The current performance, approximately one third of a token per second, is not yet suitable for most interactive applications. However, the result demonstrates that available RAM does not necessarily have to impose a hard limit on the total size of a model that can be executed locally.</p><p>WASTE is an initial step in a broader effort to make increasingly capable models available on hardware controlled by the people and organizations using them.</p><p>The goal is to provide greater control over infrastructure costs, data privacy, availability and deployment.</p><h2>Moving the memory boundary</h2><p>Kimi K3 is a mixture-of-experts model. Although it contains 896 routed experts, only 16 experts are selected in each layer for a given token.</p><p>Most of the model weights are therefore inactive at any particular point in the computation.</p><p>WASTE takes advantage of this property by separating the model into two main parts.</p><p>The model trunk remains resident in memory. The expert weights remain on NVMe storage and are read when selected by the router. Any remaining RAM is used as a bounded expert cache.</p><p>This changes the primary constraint. Instead of requiring the entire model to fit in memory, the system needs to retrieve the active portion of the model within an acceptable amount of time.</p><p>For K3, each token accesses approximately 17 GB of expert data. This makes storage bandwidth and cache behavior central to performance.</p><p>WASTE stores experts in a layout designed around this access pattern. The matrices required for one expert are adjacent and can be loaded with a single positional read. The engine bypasses the operating system page cache and manages expert caching directly, so its behavior remains representative even when the complete model is much larger than memory.</p><p>The runtime is written in C and has no external inference dependencies. Python is used for conversion and validation, but not during inference.</p><p>The project currently includes:</p><ul><li><p>an embeddable C API;</p></li><li><p>a command-line client;</p></li><li><p>an OpenAI-compatible HTTP server;</p></li><li><p>CPU implementations for ARM and x86 systems;</p></li><li><p>support for macOS, Linux and Windows;</p></li><li><p>text and image input for Kimi K3.</p></li></ul><p>The same engine also runs Kimi-Linear 48B from a 19 GB container at approximately 8.9 tokens per second on the same machine.</p><h2>Developing through measurements</h2><p>One of the most useful parts of this project has been the development process itself.</p><p>The repository includes a document called <a href="https://github.com/sqliteai/waste/blob/main/docs/LEARNED.md">What we know, and how we know it</a>. It records not only successful optimizations, but also assumptions and proposed designs that were later disproved by measurements.</p><p>We adopted a simple working rule:</p><blockquote><p>Before starting a long or expensive operation, run a smaller real-world test capable of invalidating the underlying assumption.</p></blockquote><p>This was particularly important because many operations in this project are expensive. Downloading the source model involves 1.42 TB of data. A complete conversion produces almost one terabyte of output. Changes to the expert format can require converting the model again.</p><p>Testing an assumption first is considerably less expensive than discovering a problem at the end of that process.</p><h2>Storage placement</h2><p>We initially planned to run the converted model from an external NVMe drive.</p><p>The nominal performance of the drive appeared sufficient, but conventional storage benchmarks did not represent the pattern used by WASTE. The engine reads expert-sized records from many different positions rather than processing one continuous file sequentially.</p><p>We therefore wrote a benchmark that reproduced the actual access pattern.</p><p>On the target machine, the external SSD reached approximately 0.94 GB/s. The internal SSD reached 12.78 GB/s.</p><p>The main limitation was the USB bridge in the external enclosure rather than the NVMe device itself.</p><p>As a result, the external drive is used for downloading and staging the source checkpoint, while the converted WASTE container is placed on the internal SSD.</p><p>This measurement changed an important deployment decision before the full model was downloaded and converted.</p><h2>Working with the released architecture</h2><p>Development started before the complete K3 checkpoint became publicly available. Early feasibility work was based on the published information and on measurements taken from Kimi-Linear, the closest available model from the same family.</p><p>When the K3 weights were released on July 27, 2026, we reviewed the actual configuration, tensor index and weight shapes.</p><p>The most important finding was that K3 uses a latent mixture-of-experts architecture.</p><p>The model has a hidden width of 7,168, but its routed experts operate on a projected width of 3,584. This reconciles the announced parameter count with the size of the released checkpoint and substantially reduces the amount of expert data that must be read for each token compared with a full-width implementation.</p><p>The released model also differed from our assumptions in several other ways, including its number of layers, attention heads, shared experts and attention implementation.</p><p>Those differences required changes to both the runtime and the memory model.</p><p>The initial projection based on the smaller model suggested that K3 might reach between 1 and 1.5 tokens per second on the target hardware. The final measured performance was closer to 0.3 tokens per second.</p><p>The earlier estimate did not sufficiently account for the size of the resident trunk, the resulting reduction in available cache memory, and the computational cost outside storage I/O.</p><p>The earlier projection remains in the repository together with an explanation of why it was inaccurate. Keeping that history makes the current limitations and the next optimization targets easier to understand.</p><h2>Understanding the cache</h2><p>Expert caching turned out to have a less intuitive behavior than expected.</p><p>K3 accesses approximately 17 GB of expert records for each token. If the expert cache is smaller than this working set, most entries are evicted before they can be reused by the next token.</p><p>Below this threshold, the measured cache hit rate is effectively zero.</p><p>Once the cache exceeds one token&#8217;s working set, reuse begins to provide a measurable benefit. However, increasing the cache indefinitely does not improve performance.</p><p>On the 64 GB test machine, larger memory budgets increased the reported cache hit rate while making inference substantially slower. The operating system began compressing and paging memory. An entry reported as present in the WASTE cache could therefore require a page fault before it was usable.</p><p>The engine remained within its configured allocation, but the machine as a whole was under memory pressure.</p><p>For this reason, WASTE does not attempt to consume all available memory. Its automatic planner allocates cache in multiples of the token working set and maintains a margin for the operating system and other processes.</p><p>On the test machine, a total budget of approximately 46 GB produced better results than larger allocations.</p><h2>Approaches that did not improve the system</h2><p>Several plausible approaches were tested and rejected.</p><h3>A three-bit model trunk</h3><p>Reducing the trunk from four bits to three freed more than six gigabytes of memory and increased the expert-cache hit rate.</p><p>It nevertheless reduced overall performance because the additional unpacking cost exceeded the I/O savings. More importantly, generation quality collapsed.</p><p>K3 was trained with quantization-aware training for its expert weights, but not for the rest of the model. The trunk therefore did not tolerate the same level of compression.</p><h3>Variable precision by expert</h3><p>The original format design allowed some experts to use three bits and others to use two, based on their relative importance.</p><p>Measurements showed that the quantization error was unusually uniform across experts and layers. An optimized allocation performed only marginally better than a random one.</p><p>Routing frequency does vary, but reducing the precision of infrequently used experts mainly saves disk capacity rather than the bytes read during normal inference. Disk capacity was not the main constraint.</p><p>The mechanism was therefore not implemented.</p><h3>GPU and Metal execution</h3><p>A Metal backend was implemented and verified for correctness. On the tested workload it was 22% slower than the CPU path.</p><p>WASTE performs many relatively small and dependent matrix-vector operations. This is not necessarily a favorable workload for GPU execution, particularly when the CPU implementation already operates near the system&#8217;s memory-bandwidth limits.</p><p>Metal remains available as an optional backend, but it is disabled by default.</p><h3>Index-layout changes</h3><p>A revised expert index layout produced a 1.44&#215; improvement in an isolated microbenchmark.</p><p>After converting a complete 19 GB model to the new layout, the end-to-end engine showed no meaningful improvement. The microbenchmark did not accurately represent the effects of multiple threads sharing the processor cache.</p><p>This was a useful reminder that a valid local measurement may still be irrelevant to the complete system.</p><h2>The optimizations that mattered</h2><p>The first correct version of the engine expanded quantized expert weights into floating-point values before using them.</p><p>Profiling showed that dequantization accounted for most of the expert processing time.</p><p>WASTE now performs the matrix-vector operation directly from the residual vector quantization representation. It builds partial dot-product tables for each codebook and uses those tables when processing the expert rows.</p><p>The full expert matrix is never materialized.</p><p>This approach was adapted from work previously done in <code>sqlite-vector</code>, where lookup tables are used to accelerate operations over quantized vectors.</p><p>Together with threading, SIMD kernels and the removal of repeated table construction, this reduced Kimi-Linear inference from 2.15 seconds per token in the first correct implementation to approximately 0.12 seconds per token.</p><p>After each optimization, the output was compared with a PyTorch reference implementation. The final logits remain within a small numerical tolerance, with the same most likely tokens and the same top-token ordering.</p><p>Correctness has remained a requirement throughout the optimization process.</p><h2>Why we built WASTE</h2><p>Cloud inference is appropriate for many applications. It provides access to specialized hardware, predictable deployment and models that may be impractical to operate independently.</p><p>At the same time, relying exclusively on remote inference introduces trade-offs.</p><p>Sensitive information may leave the local environment. Applications depend on network connectivity and external service availability. Usage costs are tied to token volume. Providers retain control over rate limits, model availability and version changes.</p><p>Local inference offers a different set of properties:</p><ul><li><p>prompts and results can remain on the device;</p></li><li><p>the application can continue operating without a network connection;</p></li><li><p>infrastructure costs can be based on owned hardware rather than token consumption;</p></li><li><p>model deployment and upgrades remain under the operator&#8217;s control.</p></li></ul><p>The name WASTE originally referred to the underused compute, memory and storage already available on local machines while inference is routinely moved to remote infrastructure.</p><p>The acronym&#8212;Weight-Aware Streaming Tensor Engine&#8212;was chosen later.</p><p>The intention is not to argue that all inference should move away from the cloud. It is to make local execution a practical option in cases where privacy, cost control, offline availability or infrastructure ownership are important.</p><h2>K3 as a reference point</h2><p>Kimi K3 is intentionally a demanding first target.</p><p>Its converted container occupies 982 GB. Each token requires access to approximately 17 GB of expert data. Its resident trunk consumes most of the minimum memory budget, and the internal SSD is already operating close to its measured throughput limit.</p><p>These constraints make it useful as a reference case.</p><p>Problems that are difficult to observe on smaller models become visible at this scale. Memory accounting errors become gigabytes. Ineffective caching strategies become immediately measurable. Storage placement becomes part of the inference architecture.</p><p>Smaller models are significantly less demanding.</p><p>Kimi-Linear 48B requires a 19 GB container, can open with less than 2 GB of RAM and reaches approximately 8.9 tokens per second with an 8 GB budget on the same hardware.</p><p>This is closer to the initial practical use case for WASTE: running capable local models inside applications, agents and private infrastructure without requiring a large inference stack.</p><p>K3 helps establish how far the same architecture can be extended.</p><h2>What comes next</h2><p>WASTE is the first release in a planned series of work focused on local model execution.</p><p>The current priorities include:</p><ul><li><p>improving the performance of expert retrieval and processing;</p></li><li><p>reducing the number of bytes read for each token;</p></li><li><p>supporting additional model architectures;</p></li><li><p>improving prefetching and cache persistence;</p></li><li><p>simplifying model conversion and distribution;</p></li><li><p>publishing pre-converted containers;</p></li><li><p>improving support for systems with larger unified-memory configurations.</p></li></ul><p>The immediate objective is not limited to making K3 faster. It is to develop a general runtime and model format that can support a range of local hardware, from relatively small systems to workstations with large amounts of memory and fast storage.</p><p>Some of the approaches we test will not produce useful results. As with this first release, we intend to document those findings rather than present only the successful experiments.</p><p>At its current speed, K3 running through WASTE is primarily a technical result rather than a general-purpose interactive deployment.</p><p>It nevertheless establishes that a model can be executed locally even when its total weights are many times larger than available memory.</p><p>Future work is now focused on improving efficiency, usability and model coverage.</p><p>WASTE is available under the Apache 2.0 license:</p><p><a href="https://github.com/sqliteai/waste">github.com/sqliteai/waste</a></p><p>This release represents the first step toward giving developers and organizations a practical way to run increasingly capable models on hardware they own, with direct control over data, operating costs and deployment.</p>]]></content:encoded></item><item><title><![CDATA[Rethinking Databases for Humans and AI Agents]]></title><description><![CDATA[Databases Were Built for Engines. It&#8217;s Time to Build Them for Humans.]]></description><link>https://marcobambini.substack.com/p/rethinking-databases-for-humans-and</link><guid isPermaLink="false">https://marcobambini.substack.com/p/rethinking-databases-for-humans-and</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Wed, 01 Jul 2026 13:02:49 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!V0IR!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb355754d-1ac4-4b81-935c-4662e9da991d_638x638.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Databases are one of those technologies that seem to survive every technological revolution while becoming even more important each time.</p><p>First, there was SQL. Declared dead countless times, yet still at the center of much of modern software. SQL made it possible to organize enormous amounts of information efficiently and reliably, but it always imposed a precise model: tables, columns, relationships, indexes, and queries that developers needed to understand in detail.</p><p>Then came the NoSQL wave, and databases like MongoDB changed the perspective. No more rigid schemas, but flexible JSON documents that better matched how developers represented data in modern applications.</p><p>And yet, one thing never really changed.</p><p>To store information in a database, you still need well-defined operations such as INSERT or UPDATE. To retrieve information, you still need to know columns, keys, structures, or implementation details of the underlying engine.</p><p>In other words, <strong>databases still speak the language of the engine, not the language of humans or AI agents.</strong></p><div><hr></div><p>Let&#8217;s think about a very simple example.</p><p>Imagine we want to store the following information:</p><blockquote><p>&#8220;Marco met Andrew in San Francisco to discuss a possible integration between SQLite AI and A51 Robotics.&#8221;</p></blockquote><p>In the SQL world, we would probably need to create something like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;7f5fe434-1e3f-4bb7-9ef5-79b8707996b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">CREATE TABLE meetings (
    id INTEGER PRIMARY KEY,
    person1 TEXT,
    person2 TEXT,
    city TEXT,
    topic TEXT
);
&#8203;
INSERT INTO meetings
VALUES (
    1,
    &#8216;Marco&#8217;,
    &#8216;Andrew&#8217;,
    &#8216;San Francisco&#8217;,
    &#8216;SQLite AI integration with A51 Robotics&#8217;
);</code></pre></div><p>And later, to retrieve that information, we would need to know the database structure and write queries such as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8f207128-2ea2-48f6-b055-1c39b8041b87&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT *
FROM meetings
WHERE person2 = &#8216;Andrew&#8217;;</code></pre></div><p>Or:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;8ad78990-1058-4146-831d-d5fc5ec7141c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT *
FROM meetings
WHERE topic LIKE &#8216;%A51%&#8217;;</code></pre></div><p>SQLite-Memory starts from a completely different idea.</p><p>The information can simply be stored by writing a Markdown file:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;3577ff1c-f274-41f7-bb2a-bc1e0b922f24&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown"># Meeting Notes
&#8203;
Marco met Andrew in San Francisco
to discuss a possible integration
between SQLite AI and A51 Robotics.</code></pre></div><p>No tables.<br>No columns.<br>No schema to design.</p><p>And most importantly, retrieval becomes natural.</p><p>You can simply ask:</p><blockquote><p>&#8220;What did we discuss with Andrew?&#8221;</p></blockquote><p>Or:</p><blockquote><p>&#8220;What discussions did we have regarding A51 Robotics?&#8221;</p></blockquote><p>Or even:</p><blockquote><p>&#8220;Do you remember the meeting in San Francisco?&#8221;</p></blockquote><p>Without knowing the name of a column, without knowing how the information was stored internally, and without writing SQL queries.</p><p><strong>Because humans do not think in tables.<br>And AI agents do not either.</strong></p><p>We believe the future of databases should be closer to how people actually think: natural language, context, and semantic memory.</p><p>SQLite-Memory was created precisely around this idea.</p><p>Instead of forcing users to design schemas and queries, SQLite-Memory lets them build persistent memory from simple Markdown files. The information is processed by a specialized parser, automatically structured, and made semantically searchable.</p><p>Search is no longer based solely on columns or keywords but on the meaning of the information itself.</p><p>Because we believe the future of databases is not just about storing data, but about making information understandable and accessible in the most natural way possible.</p><p><a href="https://www.sqlite.ai/sqlite-memory">SQLite-Memory</a> is available today as a native SQLite extension and is also built directly into the <a href="https://www.sqlite.ai/cloud">SQLite Cloud platform</a>. We are also working on a new PostgreSQL version to bring the same paradigm to existing PostgreSQL infrastructures and AI-native applications.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;8dd8e030-a97d-48d2-8cea-a9b77b8ec9fe&quot;,&quot;duration&quot;:null}"></div><p><strong>Memory becomes truly powerful when it can be shared.</strong></p><p>A personal memory is useful. A collective memory is transformative.</p><p>This is where SQLite-Sync comes into play.</p><p><a href="https://www.sqlite.ai/sqlite-sync">SQLite-Sync</a> allows semantic memories to be synchronized across devices, users, and AI agents while remaining fully offline-first. Knowledge created on a laptop can instantly become available on a phone, shared with a team, or incorporated into the working memory of multiple AI agents collaborating on the same tasks.</p><p>Traditional synchronization systems were designed around rows and records. Semantic memory introduces a different challenge: synchronizing knowledge, documents, and evolving context without losing meaning or creating conflicts.</p><p>To solve this problem, SQLite-Sync uses a new <a href="/__u/marcobambini.substack.com/p/block-level-lww-the-missing-piece">CRDT algorithm</a> specifically designed for Markdown-based semantic documents. Instead of treating content as opaque text blobs, it synchronizes knowledge at the block level, making collaborative editing, agent-to-agent memory sharing, and distributed semantic knowledge bases possible even in unreliable or completely offline environments.</p><p>We believe this combination of semantic memory and offline-first synchronization represents a foundational building block for the next generation of AI applications, where humans and agents continuously create, refine, and share knowledge across the edge and the cloud without ever having to think about where that information is physically stored.</p>]]></content:encoded></item><item><title><![CDATA[The Cloud vs Edge Debate Is Over]]></title><description><![CDATA[Why choose between Cloud and Edge when you can have both?]]></description><link>https://marcobambini.substack.com/p/the-cloud-vs-edge-debate-is-over</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-cloud-vs-edge-debate-is-over</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Tue, 26 May 2026 13:00:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fqj3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For years, software architecture has forced developers into a tradeoff.</p><p>You either centralized everything in the cloud or pushed logic and data closer to the edge.</p><p>The <strong>cloud</strong> gave us <strong>scalability</strong>, <strong>coordination</strong>, and <strong>shared intelligence</strong>.<br>The <strong>edge</strong> gave us <strong>responsiveness</strong>, <strong>offline capabilities</strong>, and <strong>low-latency interactions</strong>.</p><p>For a long time, that compromise was acceptable.</p><p>AI changes that completely.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!fqj3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!fqj3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg" width="1456" height="825" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:825,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:295115,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/199320321?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!fqj3!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1952a07c-7ce7-4570-a93f-ca576a8279d1_1920x1088.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The next generation of applications will not tolerate the latency introduced by continuous network roundtrips. AI agents, local copilots, robotics, industrial systems, real-time collaboration tools, and modern mobile applications increasingly need to reason, store memory, and execute directly on-device. But at the same time, they still need synchronization, coordination, shared context, centralized learning, and distributed state.</p><p>This is where the traditional distinction between &#8220;cloud database&#8221; and &#8220;edge database&#8221; starts to break down.</p><p style="text-align: center;"><strong>Developers should not have to choose one or the other.</strong></p><p style="text-align: center;"><strong>The future is both.</strong></p><div><hr></div><p>At <a href="https://sqlite.ai">SQLite AI</a>, we believe the cloud vs edge debate is largely an implementation detail.</p><p>Applications should simply operate on data:</p><ul><li><p>locally when possible,</p></li><li><p>globally when necessary,</p></li><li><p>continuously synchronized,</p></li><li><p>always available,</p></li><li><p>always low latency.</p></li></ul><p>The cloud should not be responsible for every interaction.<br>The edge should not become an isolated island.</p><blockquote><p><strong>What matters is creating an architecture where both sides work together naturally.</strong></p></blockquote><p>This is the direction we have been building toward over the past few months.</p><p>Not just hosted SQLite databases, but a complete infrastructure layer designed for local-first and AI-native applications.</p><div><hr></div><p>One of the biggest pieces of this evolution is <strong>SQLite-Sync</strong> (aka <strong>CloudSync</strong>).</p><p>SQLite-Sync introduces CRDT-based synchronization for SQLite databases, allowing applications and devices to continue operating locally while remaining continuously synchronized with the cloud.</p><p>What makes this particularly interesting is that synchronization is no longer limited to SQLite Cloud databases. SQLite-Sync can now synchronize directly with PostgreSQL and Supabase as well.</p><p>That means developers can finally combine:</p><ul><li><p>SQLite running locally on devices and at the edge,</p></li><li><p>with PostgreSQL acting as a centralized coordination layer in the cloud.</p></li></ul><p>Without giving up offline support.<br>Without forcing every operation through a remote server.<br>Without introducing unnecessary latency into AI workloads.</p><p>This hybrid architecture is becoming increasingly important as more applications move toward local AI execution.</p><div><hr></div><p>AI systems are fundamentally changing infrastructure requirements.</p><p>Traditional applications could tolerate waiting hundreds of milliseconds for requests to travel across the network.</p><p>AI-native applications cannot.</p><p>An AI agent continuously depending on cloud roundtrips for memory retrieval, vector search, or state synchronization quickly becomes inefficient and expensive. More importantly, it feels unnatural.</p><p>Modern AI systems increasingly need:</p><ul><li><p>local memory,</p></li><li><p>local persistence,</p></li><li><p>local vector search,</p></li><li><p>local reasoning,</p></li><li><p>local execution.</p></li></ul><p>At the same time, they still need:</p><ul><li><p>synchronization across devices,</p></li><li><p>shared organizational knowledge,</p></li><li><p>centralized policies,</p></li><li><p>distributed coordination.</p></li></ul><p>This creates a new category of infrastructure problems that traditional cloud architectures were never designed to solve.</p><div><hr></div><p>To support this shift, SQLite AI has evolved into a broader platform that spans both edge and cloud workloads.</p><p><strong>SQLite-Vector</strong> brings high-performance vector search directly into SQLite, enabling semantic search and embedding-based retrieval workloads to run locally with minimal memory usage.</p><p><strong>SQLite-Memory</strong> introduces persistent semantic memory for AI agents and applications, enabling the synchronization of markdown documents, structured knowledge, and long-term contextual memory across systems.</p><p><strong>SQLite-Columnar</strong> extends SQLite with column-oriented analytics capabilities optimized for large-scale scans and aggregations.</p><p>Individually, these extensions solve specific technical problems.</p><p>Together, they form the foundation for a different way of building software.</p><div><hr></div><p>We believe the next generation of applications will increasingly:</p><ul><li><p>execute locally,</p></li><li><p>synchronize globally,</p></li><li><p>reason on-device,</p></li><li><p>operate even without connectivity,</p></li><li><p>and continuously exchange state with the cloud.</p></li></ul><p style="text-align: center;"><strong>The network becomes a synchronization layer rather than a hard dependency for every interaction.</strong></p><p>This is especially important for AI.</p><p>As models become smaller, faster, and capable of running directly on consumer hardware, the bottleneck shifts away from inference itself and toward:</p><ul><li><p>synchronization,</p></li><li><p>distributed memory,</p></li><li><p>shared context,</p></li><li><p>local persistence,</p></li><li><p>and low-latency coordination.</p></li></ul><p>That is the infrastructure layer we are focused on building.</p><p>And we believe SQLite is uniquely positioned to become one of the foundational technologies behind it.</p><div><hr></div><h2>Explore SQLite AI</h2><ul><li><p><a href="https://sqlite.ai/?utm_source=marcobambini.substack.com">SQLite AI</a></p></li><li><p><a href="https://www.sqlite.ai/sqlite-sync?utm_source=marcobambini.substack.com">SQLite-Sync</a></p></li><li><p><a href="https://www.sqlite.ai/sqlite-vector?utm_source=marcobambini.substack.com">SQLite-Vector</a></p></li><li><p><a href="https://www.sqlite.ai/sqlite-memory?utm_source=marcobambini.substack.com">SQLite-Memory</a></p></li><li><p><a href="https://www.sqlite.ai/sqlite-columnar?utm_source=marcobambini.substack.com">SQLite-Columnar</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Block-Level LWW: The Missing Piece for Collaborative AI Agent Memory]]></title><description><![CDATA[How a deceptively simple CRDT algorithm unlocks something surprisingly powerful: AI agents that learn from each other without ever talking to each other.]]></description><link>https://marcobambini.substack.com/p/block-level-lww-the-missing-piece</link><guid isPermaLink="false">https://marcobambini.substack.com/p/block-level-lww-the-missing-piece</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Mon, 13 Apr 2026 12:02:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XUNd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!XUNd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!XUNd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg" width="1456" height="825" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:825,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:295115,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/194060416?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 424w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 848w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 1272w, /__u/substackcdn.com/image/fetch/$s_!XUNd!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142132bb-9f68-43dc-adb7-345f29e32fc0_1920x1088.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Every agent today accumulates knowledge: conversation history, user preferences, and research notes. That memory is what makes an agent useful.</p><p>But the moment you have more than one agent, memory becomes a distributed systems problem.</p><p>A fleet of agents, running on different devices, in different processes, often offline, will each learn independently.<br>Agent A analyzes climate data.<br>Agent B models infrastructure costs.<br>Agent C processes user feedback.</p><p>So whose memory is it, really?</p><p>The obvious answer is a shared database. But shared databases require coordination: locks, transactions, connectivity, and a central point of failure. The moment an agent goes offline, it either blocks or falls behind.</p><p>There is a better approach. It starts with a simple observation:</p><p><strong>Text is not atomic.</strong></p><div><hr></div><h2>Row-Level LWW: Almost but Not Quite Good Enough</h2><p>Last-Writer-Wins (LWW) is the simplest CRDT strategy. When two replicas disagree, the value with the newer timestamp wins. It is deterministic and coordination-free.</p><p>For atomic values, flags, counters, short strings, it works perfectly.</p><p>But memory is not atomic.</p><p>A memory entry might be hundreds of words. Agents do not overwrite it entirely. They edit parts of it. Add a line. Refine a paragraph. Update a detail.</p><p>If two agents edit different parts of the same entry while offline, row-level LWW throws away one of those edits entirely.</p><p>Both agents made valid updates. Only one survives.</p><p>That is the gap.</p><div><hr></div><h2>Block-Level LWW: Changing the Unit of Conflict</h2><p>The insight behind block-level LWW is simple:</p><p><strong>Do not treat text as a single value. Treat it as a sequence.</strong></p><p>Split the text into blocks, typically lines, though other granularities are possible, for example, delimited by <code>\n</code>.</p><p>Each block carries its own metadata:</p><ul><li><p><strong>Site ID</strong>, who wrote it</p></li><li><p><strong>Version clock</strong>, when</p></li><li><p><strong>Fractional index</strong>, where it sits in the sequence, allowing inserts between existing blocks without reordering everything</p></li></ul><p>Now the unit of conflict is not the entire document, but the individual line.</p><p>Concurrent edits to different blocks are preserved automatically.<br>Edits to the same block fall back to LWW.</p><p>Consider:</p><pre><code>Task A - Status: pending
Task B - Status: completed
Relevant context: team review scheduled for Friday</code></pre><p>Agent A edits line 1 offline:<br><code>Task A - Status: in-progress</code></p><p>Agent B edits line 3 offline:<br><code>Relevant context: team review moved to Monday</code></p><p>After sync:</p><pre><code>Task A - Status: in-progress
Task B - Status: completed
Relevant context: team review moved to Monday</code></pre><p>Both edits survive. No coordination. No conflicts to resolve.</p><p>This is not magic. It simply changes the unit of conflict. Instead of competing over an entire document, agents only compete over the specific lines they touch.</p><div><hr></div><h2>Why Markdown Is the Perfect Fit</h2><p>Not all text formats behave well under this model. Markdown does.</p><p>Markdown is inherently line-oriented. Headers, lists, code blocks, and most structural elements align naturally with line boundaries. Even paragraphs are just sequences of lines separated by blanks.</p><p>This means:</p><ul><li><p>Edits tend to map cleanly to individual lines</p></li><li><p>Structural boundaries match merge boundaries</p></li><li><p>Conflicts are naturally minimized</p></li></ul><p>Compare this with HTML or JSON, where structure and content are interleaved. A small edit can ripple across multiple lines and create artificial conflicts.</p><p>Markdown avoids this problem.</p><p>It also matches how agents behave. They mostly append new knowledge rather than rewrite everything. Block-level LWW handles insertions cleanly through fractional indexing.</p><p>The result is simple:</p><ul><li><p>fewer conflicts</p></li><li><p>automatic merges</p></li><li><p>human-readable output</p></li></ul><div><hr></div><h2>Markdown as the Source of Truth</h2><p>In <a href="https://github.com/sqliteai/sqlite-memory">sqlite-memory</a>, markdown is the primary representation of agent knowledge.</p><p>Agents ingest markdown documents. The system:</p><ul><li><p>stores raw text in a content table</p></li><li><p>computes embeddings locally</p></li><li><p>indexes them for hybrid search using vector similarity and FTS5</p></li></ul><p>The architecture is intentional:</p><p><strong>The text is the truth. Embeddings are a cache.</strong></p><p>This means:</p><ul><li><p>content can be synchronized independently</p></li><li><p>embeddings can be regenerated anywhere</p></li><li><p>no shared vector store is required</p></li></ul><p>Here is what that looks like from an agent&#8217;s perspective:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;2911d5e9-1218-4f34-8c20-56bcf06e4e8d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">-- Agent A
SELECT memory_enable_sync(&#8217;research&#8217;);
SELECT memory_add_text(&#8217;James Webb Space Telescope observations confirm...&#8217;, &#8216;research&#8217;);

-- Agent B
SELECT memory_enable_sync(&#8217;research&#8217;);
SELECT memory_add_text(&#8217;Great Barrier Reef bleaching events correlate with...&#8217;, &#8216;research&#8217;);</code></pre></div><p>After sync:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;b2b333cc-40cd-493c-97c7-3d0dbc88065a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT memory_reindex();</code></pre></div><p>Agent A can now answer questions about coral reefs.<br>Agent B can answer questions about space telescopes.</p><p>No direct communication. No shared embedding service. Only shared text.</p><div><hr></div><h2>What This Enables</h2><p>This architecture unlocks a fundamentally different model of agent systems.</p><h3>No single point of failure</h3><p>Every agent holds a complete, queryable copy of memory.</p><h3>Offline-first operation</h3><p>Agents can ingest, search, and reason without connectivity. Sync happens opportunistically.</p><h3>Selective sharing</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;078997b4-c4d2-45ab-8544-d3cb96a129c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT memory_enable_sync(&#8217;context-name&#8217;);</code></pre></div><p>Defines what is shared and what remains private.</p><h3>Parallel knowledge accumulation</h3><p>Agents can explore different domains independently and merge into a unified corpus without orchestration.</p><h3>Human-readable memory</h3><p>The source of truth is markdown. Anyone can inspect it. Debugging and auditing become straightforward.</p><div><hr></div><h2>The Mechanics of Sync</h2><p>Sync happens in two rounds and it is entirely based on <a href="https://github.com/sqliteai/sqlite-sync">sqlite-sync</a>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;48b235f1-d755-49b2-b378-2f7b00d90b17&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT cloudsync_network_sync(500, 3);
SELECT cloudsync_network_sync(500, 3);</code></pre></div><p>The first round pushes local changes outward.<br>The second round pulls in changes that others propagated in response.</p><p>After that, the database converges.</p><p>Then:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;sql&quot;,&quot;nodeId&quot;:&quot;68b86c2e-e35c-44e2-a3f3-51b08f5b4d3b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-sql">SELECT memory_reindex();</code></pre></div><p>Embeddings are generated locally for new content. Since they are keyed by content hash, this step is idempotent.</p><div><hr></div><h2>Why This Matters Now</h2><p>We are moving from single agents to agent fleets.</p><p>Current memory architectures do not scale well:</p><ul><li><p>centralized vector databases introduce coordination</p></li><li><p>API-based embeddings add latency and cost</p></li><li><p>shared mutable state requires complex conflict resolution</p></li></ul><p>This approach avoids those constraints.</p><p>It treats:</p><ul><li><p>text as a CRDT</p></li><li><p>agents as independent, occasionally connected nodes</p></li></ul><p>The result is a system that:</p><ul><li><p>degrades gracefully under network failure</p></li><li><p>scales without coordination</p></li><li><p>converges deterministically</p></li></ul><p>For AI systems operating at the edge of connectivity, this is not an optimization.</p><p><strong>It is the architecture.</strong></p><div><hr></div><h2>About sqlite-memory</h2><p><code>sqlite-memory</code> is part of the SQLite AI ecosystem, a set of extensions that bring semantic search, local inference, and distributed sync to SQLite: <a href="https://github.com/sqliteai/sqlite-memory">https://github.com/sqliteai/sqlite-memory</a></p>]]></content:encoded></item><item><title><![CDATA[That time is gone]]></title><description><![CDATA[I vividly remember that year, it was 2013, and I was trying to develop one of the most difficult things in my life: the Gravity programming language and VM. There are some picture that perfectly summarizes how I felt at that time:]]></description><link>https://marcobambini.substack.com/p/that-time-is-gone</link><guid isPermaLink="false">https://marcobambini.substack.com/p/that-time-is-gone</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Mon, 13 Apr 2026 11:31:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/19540ec0-1148-4841-b365-9cef1f51719a_720x540.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I vividly remember that year, it was 2013, and I was trying to develop one of the most difficult things in my life: the <a href="https://github.com/marcobambini/gravity">Gravity programming language and VM</a>. There are some picture that perfectly summarizes how I felt at that time:</p><div class="image-gallery-embed" data-attrs="{&quot;gallery&quot;:{&quot;images&quot;:[{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c0a9a933-e5b5-4c63-ab49-8f985b5283f0_3000x4000.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fc43733d-e11e-460e-9e34-cf3b55397e4b_3264x2448.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2f350403-521b-4142-8450-c96976149a32_4000x3000.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/51161ac1-3823-4257-bd39-e82fc46d52ed_3000x4000.jpeg&quot;}],&quot;caption&quot;:&quot;&quot;,&quot;alt&quot;:&quot;&quot;,&quot;staticGalleryImage&quot;:{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/115d51c7-29b4-4334-9cd3-0c462b30774a_1456x1456.png&quot;}},&quot;isEditorNode&quot;:true}"></div><p>Countless hours, days, and months studying, writing, testing. Most of the time in my head first, then with paper and pen and on a real computer at the end.</p><p>Just by looking at those pictures, I can again feel the pain, the intense sacrifice, the energy of the entire process, and, finally, the reward when everything worked as expected.</p><div class="image-gallery-embed" data-attrs="{&quot;gallery&quot;:{&quot;images&quot;:[{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef63bcd8-18e5-4475-99c8-957f40456d5b_1200x1200.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/660bac35-f3f2-4aaf-b374-c58edc11f936_1206x1202.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/44d2d7b5-e38d-4a0a-b7b6-e273e4c112b9_1204x1200.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ec4c5415-4015-4d6b-81fe-49f55c09688c_1200x1194.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e56b94dc-579d-4262-874c-3eb2ba347645_1204x1196.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ebdc15d1-10ad-41b8-ae5a-7cdd268f66dd_1206x1202.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2606d935-30fa-42ce-9131-edfb3f315af7_1198x1202.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c92e801b-a18a-4c95-99f1-b0bab93c95c9_1208x1202.png&quot;},{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/00c1e2f7-0c4f-48b8-8569-617ee86c5976_1198x1198.png&quot;}],&quot;caption&quot;:&quot;&quot;,&quot;alt&quot;:&quot;&quot;,&quot;staticGalleryImage&quot;:{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d0a9850-5ee9-4298-9f36-942c340a4105_1456x1454.png&quot;}},&quot;isEditorNode&quot;:true}"></div><p>More on my <a href="https://www.instagram.com/sqlabs/">Instagram</a>.</p><p>It was intense, just books, pen and paper, my dedication, and my skills.</p><p>The creative process gives you back much more than the final result. You&#8217;ll end up with new connections, new ideas, new ways to see the same problem, and when you can finally &#8220;touch&#8221; the product, you realize that the sum is immensely greater than the parts.</p><p>After <a href="https://creolabs.com">Creo</a> (Creo was running on top of Gravity), I founded another <a href="https://sqlite.ai">startup</a>, but I always felt that Gravity and what I learned during that time deserved more. I realize that I always try to find connections between what I built in those years and what I find in front of me today.</p><p>Today, you write in plain English, drink a cup of tea, and Claude or Codex or whatever you like more does all the effort for you. You are more like a director: you no longer feel the pain, you no longer manually craft every single detail, and most importantly, you no longer learn like every other human being has learned since there were humans on Earth.</p><p>Now I can write basically everything in a couple of days, which is honestly super cool, but I&#8217;ll end up learning nothing from the process. This scares me a lot.</p><p>I like to define this time, especially for the software industry, as &#8220;frighteningly exciting&#8220;.</p>]]></content:encoded></item><item><title><![CDATA[liteparser: a fast, embeddable SQLite parser]]></title><description><![CDATA[A complete SQLite parser you can embed anywhere.]]></description><link>https://marcobambini.substack.com/p/liteparser-a-fast-embeddable-sqlite</link><guid isPermaLink="false">https://marcobambini.substack.com/p/liteparser-a-fast-embeddable-sqlite</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Wed, 11 Mar 2026 09:18:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!V0IR!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb355754d-1ac4-4b81-935c-4662e9da991d_638x638.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While flying to San Francisco a few months ago, I decided to attempt something I had wanted for years: <strong>building a complete parser for SQLite</strong>:</p><div class="twitter-embed" data-attrs="{&quot;url&quot;:&quot;https://x.com/_marcobambini/status/1985004378017689774&quot;,&quot;full_text&quot;:&quot;A long trip ahead of me to go to San Francisco, my goal for the next 12 hours is to write an embeddable, self-contained, and high-performance complete parser for SQLite <span class=\&quot;tweet-fake-link\&quot;>#sqlite</span> <span class=\&quot;tweet-fake-link\&quot;>#OpenSource</span>&quot;,&quot;username&quot;:&quot;_marcobambini&quot;,&quot;name&quot;:&quot;Marco Bambini&quot;,&quot;profile_image_url&quot;:&quot;https://pbs.substack.com/profile_images/521619583879610368/m5vdV9Vv_normal.png&quot;,&quot;date&quot;:&quot;2025-11-02T15:21:37.000Z&quot;,&quot;photos&quot;:[],&quot;quoted_tweet&quot;:{},&quot;reply_count&quot;:2,&quot;retweet_count&quot;:0,&quot;like_count&quot;:5,&quot;impression_count&quot;:138,&quot;expanded_url&quot;:null,&quot;video_url&quot;:null,&quot;video_preview_media_key&quot;:null,&quot;belowTheFold&quot;:false}" data-component-name="Twitter2ToDOM"></div><p>The ability to fully understand a SQLite statement programmatically unlocks many interesting possibilities: query analysis, SQL transformations, tooling, and AI systems that interact with databases.</p><p>Unfortunately, building a SQLite parser is surprisingly difficult.</p><p>SQLite&#8217;s grammar has evolved for more than 20 years and includes many subtle rules around expression precedence, optional syntax, and compatibility quirks. The official parser is generated from a large <code>parse.y</code> file and is deeply integrated with the rest of the SQLite engine.</p><p>Over the years, I tried several approaches:</p><ul><li><p>writing a parser from scratch</p></li><li><p>manually parsing specific statements like <code>CREATE TABLE </code>(<a href="https://github.com/marcobambini/sqlite-createtable-parser">https://github.com/marcobambini/sqlite-createtable-parser</a>)</p></li><li><p>modifying SQLite&#8217;s <code>parse.y</code> grammar</p></li></ul><p>None of these attempts worked well. Most projects ended up either incomplete or too fragile to maintain.</p><p>Recently I decided to try again.</p><p>With the help of modern coding assistants, I was finally able to push through the complexity and build a <strong>standalone parser derived from SQLite&#8217;s official grammar</strong>.</p><p>The result is <strong>liteparser</strong>.</p><p>liteparser is a <strong>feature-complete, embeddable, high-performance parser for SQLite statements written in C</strong>.</p><p>SQLite statements are parsed into a structured AST that can be <strong>inspected, analyzed, or transformed programmatically</strong>, making it possible to build tools that understand and manipulate SQL queries safely without executing them.</p><p>This capability enables a wide range of tooling around SQLite, including query linters, SQL formatters, query analyzers, schema migration tools, database IDEs, and AI systems that need to reason about SQL.</p><p>Key characteristics:</p><ul><li><p>parses <strong>all SQLite statements</strong></p></li><li><p>generates a complete <strong>AST</strong></p></li><li><p>supports <strong>JSON representations of parsed queries</strong></p></li><li><p>statements can be <strong>inspected, analyzed, or transformed programmatically</strong></p></li><li><p>preserves <strong>SQLite operator precedence and grammar semantics</strong></p></li><li><p>uses <strong>arena allocation</strong> to minimize memory overhead</p></li><li><p>works on <strong>native platforms and WASM</strong></p></li><li><p>easily embeddable in other programming languages</p></li></ul><p>Because it derives from SQLite&#8217;s official <code>parse.y</code>, it behaves consistently with SQLite itself.</p><p>The library has been extensively tested using:</p><ul><li><p>the <strong>official SQLite test suite</strong></p></li><li><p><strong>millions of fuzz-testing iterations</strong></p></li></ul><p>As the founder of <strong><a href="https://sqlite.ai">SQLite AI</a></strong>, I decided to release <a href="https://github.com/sqliteai/liteparser">liteparser</a> as open source under the <strong>MIT license</strong>, so anyone can freely use it.</p><p>If you&#8217;re building tools that need to understand SQLite queries like linters, query analyzers, developer tooling, or AI systems, this library might be extremely useful.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;f6084518-8f65-4441-a41b-acb57f4cbdc9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">const char *sql = "SELECT name FROM users WHERE age &gt; 18;";
liteparser_ast *ast = liteparser_parse(sql);
liteparser_print_json(ast);</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;943c486b-061a-4246-ad6e-3a488a386133&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "type": "select",
  "columns": ["name"],
  "from": "users",
  "where": {
    "op": "&gt;",
    "left": "age",
    "right": 18
  }
}</code></pre></div><p>Project:</p><p><a href="https://github.com/sqliteai/liteparser">https://github.com/sqliteai/liteparser</a></p>]]></content:encoded></item><item><title><![CDATA[The Edge-First Manifesto]]></title><description><![CDATA[Cloud as a coordination layer. Edge as the execution environment.]]></description><link>https://marcobambini.substack.com/p/the-edge-first-manifesto</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-edge-first-manifesto</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Fri, 19 Dec 2025 10:03:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Smn-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Modern software is built on a fragile assumption: that the network is always available.</p><p>It isn&#8217;t.</p><p>Trains lose connectivity.<br>Factories operate behind firewalls.<br>Hospitals isolate systems.<br>Devices move, disconnect, and fail.</p><p>And yet we keep building applications that stop working the moment the cloud disappears.</p><p>This is not a limitation of hardware.<br>It is a limitation of mindset.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!Smn-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 424w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 848w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!Smn-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic" width="1456" height="825" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:825,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:106051,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/182069742?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 424w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 848w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!Smn-!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F07d46c28-85d2-43cb-9a65-e8a71a4f38bd_1920x1088.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>The cloud is not the computer</h2><p>For the last decade, we have treated the cloud as the default execution environment.</p><p>Computation, storage, identity, and application logic are centralized, always online, and always remote.</p><p>This model breaks under real-world conditions.</p><p>Latency is unpredictable.<br>Costs grow without bounds.<br>Privacy is compromised by design.<br>Failure becomes systemic instead of local.</p><p>The cloud is a powerful coordination layer.<br>It is a terrible place to depend on for basic application behavior.</p><div><hr></div><h2>Software must survive reality</h2><p>Applications should not stop working when connectivity degrades.<br>They should not lose data because a service is unavailable.<br>They should not require a round trip to a distant server to make progress.</p><p>An application that cannot function offline is not resilient.<br>It is incomplete.</p><p>Offline is not an edge case.<br><strong>Offline is the baseline.</strong></p><div><hr></div><h2>Edge-first is not cloud-last</h2><p>Edge-first does not mean rejecting the cloud.<br>It means redefining its role.</p><p>The edge is where data is created.<br>The edge is where decisions must be made.<br>The edge is where users interact with software.</p><p>The cloud exists to coordinate, not to control.</p><p>Synchronization should be explicit.<br>Conflicts should be deterministic.<br>Failure should be local, not global.</p><div><hr></div><h2>Data belongs to the application, not the platform</h2><p>Modern platforms encourage dependency.</p><p>Proprietary APIs.<br>Opaque execution.<br>Irreversible coupling.</p><p>This leads to lock-in, not reliability.</p><p>Applications should be portable.<br>State should be inspectable.<br>Logic should be reproducible.</p><p>A system you cannot debug locally is not under your control.</p><div><hr></div><h2>SQLite is the natural foundation of the Edge</h2><p>SQLite is everywhere.</p><p>It is embedded, deterministic, battle-tested, and reliable.</p><p>It does not require a server.<br>It does not assume connectivity.<br>It does not hide complexity behind a network boundary.</p><p>SQLite already powers the edge, silently.</p><p>We believe it should power the application runtime itself.</p><div><hr></div><h2>An application is a database with behavior</h2><p>Storage, computation, synchronization, security, and AI should not be separate services.<br>They should be capabilities of a single, local runtime.</p><p>Logic runs where the data lives.<br>State evolves deterministically.<br>Synchronization happens when possible, not when required.</p><p>This is not a return to the past.<br>It is a correction.</p><div><hr></div><h2>Determinism over magic</h2><p>Automatic systems that cannot be reasoned about eventually fail.</p><p>We choose explicitness over convenience.</p><p>Synchronization must be observable.<br>Conflicts must be explainable.<br>Behavior must be replayable.</p><p>If you cannot reproduce it, you cannot trust it.</p><div><hr></div><h2>AI belongs on the device</h2><p>Inference should not require a network round-trip.<br>Embeddings should not leak private data.<br>Learning should respect locality.</p><p>The edge is not too weak for AI.<br>The cloud is too far.</p><div><hr></div><h2>Build software that works without permission</h2><p>Software should continue to function without connectivity, without subscriptions, and without centralized approval.</p><p>The best software does not ask for permission to exist.</p><div><hr></div><h2>Our commitment</h2><p><strong>We are building an Edge Application Platform based on SQLite</strong>.</p><p>One that runs locally, synchronizes explicitly, executes logic on-device, integrates AI where data lives, and remains portable and open.</p><p>The cloud is optional.<br>The edge is mandatory.</p><div><hr></div><p>I am the founder of <strong><a href="https://sqlite.ai">SQLite AI</a></strong>, we&#8217;re building an edge-first application platform based on SQLite, where apps run locally, work offline by default, and use the cloud only for coordination. Our goal is to treat SQLite as the application runtime, not just a storage layer.<br><em>We&#8217;ll be sharing more technical details and announcements soon.</em></p>]]></content:encoded></item><item><title><![CDATA[The Secret Life of a Local-First Value]]></title><description><![CDATA[A deep dive into how CRDT-powered local-first apps track, merge, and sync every INSERT, UPDATE, and DELETE inside SQLite.]]></description><link>https://marcobambini.substack.com/p/the-secret-life-of-a-local-first</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-secret-life-of-a-local-first</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Mon, 29 Sep 2025 13:20:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!LXtf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my <a href="/__u/marcobambini.substack.com/p/why-local-first-apps-havent-become">previous article</a>, I provided a broad explanation of the algorithms that allow <strong>local-first applications</strong> to synchronize data correctly using <strong>CRDTs (<a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">Conflict-free Replicated Data Types</a>)</strong>.</p><p>When you see a local-first app that seamlessly syncs with other applications distributed worldwide, it feels like magic. Under the hood, a lot is happening, and it has to happen in the <strong>right order</strong> for the algorithm to preserve correctness..</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;ed7162e2-c765-47e9-b1cc-e014641bdcce&quot;,&quot;duration&quot;:null}"></div><blockquote><p>The source code of this simple iOS and Android <strong>ToDo</strong> app can be found at: <a href="https://github.com/sqliteai/sqlite-sync/tree/main/examples/to-do-app">https://github.com/sqliteai/sqlite-sync/tree/main/examples/to-do-app</a></p></blockquote><p>Let&#8217;s look more deeply at what really happens when a value in a local-first app (backed by SQLite) is <strong>INSERTed</strong>, <strong>UPDATEd</strong>, or <strong>DELETEd</strong>.</p><h3>Initial setup</h3><p>Suppose we have two users, Bob and Alice, each with a local SQLite database named <code>todo.sqlite</code> that contains the following table:</p><pre><code><code>CREATE TABLE todo (
         id TEXT PRIMARY KEY NOT NULL,
         title TEXT,
         status TEXT
);</code></code></pre><p>Initially, both databases are empty, and at some point, Alice adds a "Buy groceries" item to the <strong>todo</strong> table with status set to "in_progress". In SQL it can be translated to:</p><pre><code><code>INSERT INTO todo (id, title, status)
VALUES ('ID1', 'Buy groceries', 'in_progress');</code></code></pre><p>At the SQL level, this is just a row insert. But for the <strong>sync engine</strong>, this single <code>INSERT</code> is <strong>not enough information</strong> to replicate the change safely across peers. It needs to break the change apart and <strong>record causality</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!LXtf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!LXtf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:176858,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/174526948?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!LXtf!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1cdd80d7-058a-4349-97ab-dc24a1555fe1_1920x1080.heic 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3>How the sync engine intercepts the change</h3><p>Most local-first systems hook into SQLite by setting up <code>UPDATE</code><strong> / </strong><code>INSERT/DELETE</code><strong> triggers</strong> on the user table; this is usually the trick that enables the engine to know that Alice&#8217;s device just inserted a new row.</p><p>Once the engine is notified, it doesn&#8217;t just say &#8220;a row was added.&#8221; It <strong>splits the mutation into per-column events</strong>.<br>Why? Because CRDTs typically track <strong>each column independently</strong> so that two peers can edit different columns of the same row without overwriting each other.</p><p>For our <code>INSERT</code>, the engine conceptually turns it into three &#8220;column insertions&#8221;:</p><ul><li><p><code>(row=ID1, column=title, value=&#8217;Buy groceries&#8217;)</code></p></li><li><p><code>(row=ID1, column=status, value=&#8217;in_progress&#8217;)</code></p></li><li><p><code>(row=ID1, column=id, value=&#8217;ID1&#8217;)</code> (implicit for primary key)</p></li></ul><p>Each column is then annotated with <strong>causal metadata</strong>.</p><h3>Metadata captured per column</h3><p>For every column change, the engine typically stores:</p><ul><li><p><strong>site_id</strong>: Unique id of the device/database (e.g. Alice&#8217;s phone).</p></li><li><p><strong>column_name</strong>: Which column changed (<code>title</code>, <code>status</code>, etc.).</p></li><li><p><strong>row_key</strong>: Encoded primary key (points back to the row in <code>todo</code>).</p></li><li><p><strong>column_version</strong>: A <strong>Lamport clock</strong> or hybrid logical clock (HLC) that totally orders changes <em>from the same site</em>.</p></li><li><p><strong>db_version</strong>: The current <strong>database-wide Lamport clock</strong>, used to detect if a peer has already seen this change.</p></li><li><p><strong>op_type</strong>: Flag describing if this is an insert/update/delete at the column level (some systems call this a &#8220;tombstone&#8221; or &#8220;causal length&#8221;).</p></li><li><p><strong>seq</strong>: Order of mutations <em>within</em> the same db_version (to preserve local transaction order).</p></li></ul><p>In many implementations, this is stored in a hidden <strong>metadata_table</strong>, so Alice&#8217;s insert produces one entry per column with her <code>site_id</code> and incremented clocks.</p><h3>Why we need column-level clocks</h3><p>CRDT algorithms for documents, sets, and maps use <strong>per-field timestamps</strong> so that:</p><ul><li><p>If Bob changes <code>status</code> while Alice changes <code>title</code>, they can merge without conflict.</p></li><li><p>If both change the same column, the highest <strong>clock</strong> (or other CRDT rule) wins.</p></li></ul><p>This is what lets local-first apps avoid <strong>last-writer-wins bugs</strong> while still syncing automatically.</p><h3>Sync: how Bob receives Alice&#8217;s change</h3><p>When Alice later syncs with Bob (via any transport like HTTP, P2P, etc.):</p><ol><li><p><strong>Diff calculation:</strong></p><ul><li><p><strong>P2P mode:</strong></p><ul><li><p>Alice asks Bob: <em>&#8220;What </em><code>db_version</code><em> have you seen from me?&#8221;</em></p></li><li><p>Bob replies with the last <code>db_version</code> he knows for <strong>Alice&#8217;s </strong><code>site_id</code>.</p></li><li><p>Alice then ships only the ops with <code>db_version</code> &gt; Bob&#8217;s reply.</p></li></ul></li><li><p><strong>Server-orchestrated mode:</strong><br>Instead of talking directly, Alice contacts a <strong>sync service</strong>. The server keeps a <strong>version map</strong> (per-<code>site_id</code> last-seen <code>db_version</code>) for every client.</p><ul><li><p>Alice uploads her new ops; the server updates Alice&#8217;s entry.</p></li><li><p>When Bob syncs, the server tells him <em>which </em><code>db_version</code><em>s from each site he&#8217;s missing</em> and streams just those ops.</p><blockquote><p>This centralizes <strong>fan-out</strong>, simplifies <strong>conflict-free catch-up</strong>, and avoids NAT/P2P issues while preserving CRDT semantics.</p></blockquote></li></ul></li></ul></li><li><p><strong>Operation shipping:</strong><br>Alice sends all new rows in <strong>metadata_table</strong> beyond that version.</p></li><li><p><strong>Replay:</strong><br>Bob&#8217;s engine replays the ops in <strong>causal order</strong>:</p><ul><li><p>If the row doesn&#8217;t exist, it creates it.</p></li><li><p>It writes the columns in the same order Alice used (<code>seq</code>).</p></li><li><p>It updates Bob&#8217;s local <strong>clock table</strong> so he doesn&#8217;t reapply the same ops later.</p></li></ul></li></ol><p>After the replay, Bob&#8217;s user-facing <code>todo</code> table looks exactly like Alice&#8217;s.</p><h3>UPDATE under the hood</h3><p>If Alice later runs:</p><pre><code><code>UPDATE todo SET status = &#8216;done&#8217; WHERE id = &#8216;ID1&#8217;;
</code></code></pre><ul><li><p>SQLite writes to the <code>status</code> cell.</p></li><li><p>The engine captures the event (via trigger or hook).</p></li><li><p>It generates a new <code>column_version</code> for <code>status</code>.</p></li><li><p>Inserts a new op record into <strong>metadata_table</strong> with <code>op_type = UPDATE</code>.</p></li><li><p>The old value stays in <code>todo</code>; CRDT history doesn&#8217;t store the previous value, just the <em>new value + causal version</em>.</p></li></ul><p>When Bob syncs, his engine sees:</p><blockquote><p>&#8220;<code>status</code> column of <code>ID1</code> is now &#8216;done&#8217; with version <code>(Alice, 42)</code>&#8221;<br>If Bob&#8217;s local version for <code>status</code> is lower, it overwrites; if higher, it ignores (or merges, depending on CRDT type).</p></blockquote><h3>DELETE under the hood</h3><p>Deletes are usually implemented as a <strong>tombstone</strong>, not a physical row removal:</p><pre><code><code>DELETE FROM todo WHERE id = &#8216;ID1&#8217;;
</code></code></pre><ul><li><p>Instead of dropping the row immediately, the engine writes an op with <code>op_type = DELETE</code> (tombstone).</p></li><li><p>It may also store a <em>delete clock</em> per column or per row.</p></li></ul><p>When Bob syncs, his engine:</p><ul><li><p>Marks the row as deleted if Alice&#8217;s delete clock &gt; any of Bob&#8217;s column clocks.</p></li><li><p>Some systems keep the tombstone forever; others GC it once all peers have seen it.</p></li></ul><p>This ensures that if Bob was offline and later tries to update the deleted row, the system can detect and discard that update (since the delete happened causally later).</p><h3>Putting it together</h3><p>So a single user action like:</p><pre><code><code>INSERT INTO todo VALUES (&#8217;ID1&#8217;, &#8216;Buy groceries&#8217;, &#8216;in_progress1&#8217;);
</code></code></pre><p>Triggers:</p><ol><li><p>SQLite row write.</p></li><li><p>Engine hook intercept.</p></li><li><p>Mutation split into per-column ops.</p></li><li><p>Metadata: <code>(site_id, row_key, column_name, column_version, db_version, op_type, seq)</code>.</p></li><li><p>Ops appended to a local <strong>CRDT log</strong>.</p></li><li><p>Later: network sync ships the ops.</p></li><li><p>Remote site replays ops in causal order, resolving conflicts column by column.</p></li></ol><blockquote><p>This mechanism &#8212; <strong>column-wise causality + Lamport clocks + tombstones + metadata_table shipping</strong> is what lets local-first apps built on SQLite remain offline-first, merge safely, and stay reactive.</p></blockquote><div><hr></div><h3>Working Offline &#8212; and Why It Still Works</h3><p>One of the most powerful aspects of a <strong>local-first</strong> architecture is that <strong>Alice and Bob can be offline for as long as they want</strong> and everything will still sync correctly later:</p><ul><li><p>Alice might go for a long train ride, shut down her phone, and come back online hours later.</p></li><li><p>Bob could fly overseas with his phone in <strong>airplane mode</strong> for the entire trip.</p></li></ul><p>While they&#8217;re disconnected, both can freely <strong>INSERT</strong>, <strong>UPDATE</strong>, and <strong>DELETE</strong> items in their local SQLite database. Each change is recorded in the hidden <strong>metadata_table</strong> with its <code>site_id</code>, per-column version, and <code>db_version</code>.</p><p>When the network becomes available again, the <strong>sync layer</strong> simply asks:</p><blockquote><p>&#8220;What operations have I not yet seen from each site?&#8221;</p></blockquote><p>It then <strong>ships and merges</strong> all missing ops using the causal metadata.</p><p>Because the system is based on <strong>CRDTs (Conflict-free Replicated Data Types)</strong>, it provides what is called <strong>strong eventual consistency</strong>:</p><ul><li><p>Every replica eventually sees the same set of operations.</p></li><li><p>Conflicting edits to different columns merge automatically.</p></li><li><p>True conflicts (same column, same row) are resolved deterministically using the clocks.</p></li></ul><pre><code>The result: no matter how long someone stays offline or how messy the network conditions are, <strong>the databases will converge without human intervention or manual conflict resolution.</strong></code></pre><div><hr></div><p>I&#8217;m the founder of <strong><a href="https://www.sqlite.ai">SQLite AI</a></strong>, our mission is to make <strong>local-first sync with SQLite and AI on the Edge</strong> straightforward and reliable. If you&#8217;d like to experiment with a production-ready sync engine, the same kind of technology discussed in this article, you can explore it at: <a href="https://www.sqlite.ai/sqlite-sync">https://www.sqlite.ai/sqlite-sync</a></p><div><hr></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://marcobambini.substack.com/p/the-secret-life-of-a-local-first?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="/__u/marcobambini.substack.com/p/the-secret-life-of-a-local-first?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Why Local-First Apps Haven’t Become Popular?]]></title><description><![CDATA[Offline-first apps promise instant loading and privacy, but in practice, very few apps get offline support because getting sync right is surprisingly hard.]]></description><link>https://marcobambini.substack.com/p/why-local-first-apps-havent-become</link><guid isPermaLink="false">https://marcobambini.substack.com/p/why-local-first-apps-havent-become</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Mon, 22 Sep 2025 12:28:59 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HTL7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Offline-first apps <strong>sound like the future</strong>: instant loading, privacy by default, and no more spinning loaders on flaky connections.</p><p>But in practice, very few apps get offline support right. Most simply queue changes locally and push them when the network comes back (spoiler: this doesn&#8217;t really work). Eventually, users see a scary banner saying <em>&#8220;changes may not be saved.&#8221;</em></p><p>The reason is simple: <strong>syncing is hard.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!HTL7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!HTL7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:168692,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/174227731?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!HTL7!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7219314f-ace0-446a-b502-ff026c465236_1920x1080.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>When you build a local-first app, you&#8217;ve effectively created a <strong>distributed system</strong>. Multiple devices can mutate data independently, sometimes offline, and later must converge to the <strong>exact same state</strong> without losing data.</p><p>There are two main challenges to solve:</p><ol><li><p><strong>Unreliable ordering</strong></p></li><li><p><strong>Conflicts</strong></p></li></ol><p>Let&#8217;s go through them.</p><div><hr></div><h2>1. Unreliable Ordering</h2><p>In a distributed environment, events happen on multiple devices at different times. If you just apply them as they arrive, you get inconsistent results.</p><p>Example:</p><ul><li><p><strong>Device A</strong> sets <code>x = 3</code></p></li><li><p><strong>Device B</strong> sets <code>x = 5</code></p></li></ul><p>Both happen while offline.</p><p>When the devices sync, the final value of <code>x</code> depends on which update is applied first. That&#8217;s a problem.</p><p>Traditional backend databases solve this with <strong>strong consistency</strong>, but that requires global coordination&#8212;too slow and brittle for local-first systems.</p><p>Instead, we want <strong>eventual consistency</strong>: every device applies changes independently but eventually converges to the same result once all changes are known.</p><p>The challenge is figuring out the <strong>correct order of events</strong> without relying on a centralized clock (because the network might be down).</p><h3>The Solution: Hybrid Logical Clocks (HLCs)</h3><p>Surprisingly, there&#8217;s a simple solution to this seemingly hard problem: <strong>Hybrid Logical Clocks (HLCs)</strong>.</p><p>HLCs generate timestamps that are:</p><ul><li><p><strong>Comparable</strong> (can be sorted lexicographically)</p></li><li><p><strong>Causally consistent</strong> (encode the order in which events happened)</p></li></ul><p>HLCs combine two pieces of information:</p><ul><li><p><strong>Physical time</strong> (from the machine clock)</p></li><li><p><strong>Logical time</strong> (a counter that increments when clocks are out of sync or events happen too close together)</p></li></ul><p>In plain terms, HLCs let devices agree on &#8220;what happened first&#8221; without perfectly synchronized clocks.</p><h4>A Quick Example</h4><p>Imagine two machines, A and B:</p><ol><li><p><strong>Machine A</strong> records an event at <code>10:00:00.100</code>.<br>&#8594; Its HLC becomes <code>(10:00:00.100, 0)</code> (time + counter).</p></li><li><p>A sends a message to B with this HLC.</p></li><li><p><strong>Machine B</strong>&#8217;s clock shows <code>10:00:00.095</code> (slightly behind).<br>When B receives the message, it <strong>advances its HLC</strong> to at least match A&#8217;s timestamp.</p></li><li><p>B&#8217;s HLC becomes <code>(10:00:00.100, 1)</code> &#8212; the counter increments to indicate this happened <em>after</em> A&#8217;s event.</p></li></ol><p>Result:</p><ul><li><p>Event on A: <code>(10:00:00.100, 0)</code></p></li><li><p>Event on B: <code>(10:00:00.100, 1)</code></p></li></ul><p>Even though B&#8217;s physical clock was behind, we can now order events consistently across machines.</p><div><hr></div><h2>2. Conflicts</h2><p>Even with proper ordering, <strong>conflicts still happen</strong> when two devices modify the same data independently.</p><p>Example:</p><ul><li><p>Initial balance = <code>100</code></p></li><li><p><strong>Device A:</strong> <code>+20</code> &#8594; balance = <code>120</code></p></li><li><p><strong>Device B:</strong> <code>-20</code> &#8594; balance = <code>80</code></p></li></ul><p>When they sync, which value should &#8220;win&#8221;?<br>If you naively apply both updates, one overwrites the other&#8212;losing user data.</p><p>Most systems ask developers to write manual conflict resolution code, but that&#8217;s error-prone and hard to maintain.</p><h3>The Solution: CRDTs</h3><p>The right approach is <strong>CRDTs</strong> (Conflict-Free Replicated Data Types).</p><p>CRDTs guarantee two important properties:</p><ul><li><p><strong>Commutativity:</strong> Order of application doesn&#8217;t matter</p></li><li><p><strong>Idempotence:</strong> Applying the same change twice has no effect</p></li></ul><p>This means you can apply messages in any order, even multiple times, and every device will still converge to the same state.</p><p>One of the simplest CRDT strategies is <strong>Last-Write-Wins (LWW)</strong>:</p><ul><li><p>Each update gets a timestamp (physical or logical).</p></li><li><p>When two devices write to the same field, the update with the latest timestamp wins.</p></li></ul><p>Example:</p><ul><li><p><strong>Device A:</strong> balance = <code>120</code> at <code>10:00:00</code></p></li><li><p><strong>Device B:</strong> balance = <code>80</code> at <code>10:00:02</code></p></li></ul><p>When syncing, the system keeps <code>80</code> because it was written last.</p><div><hr></div><h2>Why SQLite Is Perfect for This</h2><p>When building a local-first app, you need a rock-solid local database. <strong>SQLite</strong> is the obvious choice: battle-tested, lightweight, and available everywhere.</p><p>That&#8217;s why we built our local-first framework as a <strong>SQLite extension</strong>.</p><p>Our approach (simplified):</p><ul><li><p>Every change is stored as a message in a <code>messages</code> table with:</p><ul><li><p><code>timestamp</code> (from HLC)</p></li><li><p><code>dataset</code> (table name)</p></li><li><p><code>row</code> (encoded primary keys)</p></li><li><p><code>column</code></p></li><li><p><code>value</code></p></li></ul></li></ul><p>Applying a message is as simple as:</p><ol><li><p>Look up the current value</p></li><li><p>If the incoming timestamp is newer &#8594; overwrite</p></li><li><p>If it&#8217;s older &#8594; ignore</p></li></ol><p>This guarantees convergence across devices, regardless of the sync order.</p><div><hr></div><h2>Why This Matters</h2><p>This architecture makes syncing <strong>simple and reliable</strong>:</p><ul><li><p><strong>Reliable:</strong> Survives weeks of offline use without data loss</p></li><li><p><strong>Deterministic:</strong> Final state always converges</p></li><li><p><strong>Minimal:</strong> Just a small SQLite extension, no heavy dependencies</p></li><li><p><strong>Cross-platform:</strong> The extension is available for iOS, Android, macOS, Windows, Linux, and WASM</p></li></ul><div><hr></div><h2>Takeaways for Developers</h2><ul><li><p>Stop faking offline support with request queues</p></li><li><p>Embrace <strong>eventual consistency</strong></p></li><li><p>Use proven distributed-systems techniques like <strong>HLCs</strong> and <strong>CRDTs</strong></p></li><li><p>Keep it small and dependency-free</p></li></ul><p>The result? Apps that are <strong>instant</strong>, <strong>offline-capable</strong>, and <strong>private by default</strong> &#8212; without the complexity of traditional client&#8211;server synchronization.</p><div><hr></div><p>I&#8217;m the founder of <strong><a href="https://www.sqlite.ai">SQLite AI</a></strong>, our mission is to make <strong>local-first sync with SQLite and AI on the Edge</strong> straightforward and reliable. If you&#8217;d like to experiment with a production-ready sync engine, the same kind of technology discussed in this article, you can explore it at: <a href="https://www.sqlite.ai/sqlite-sync">https://www.sqlite.ai/sqlite-sync</a></p>]]></content:encoded></item><item><title><![CDATA[The Future of Databases is Local-First]]></title><description><![CDATA[Databases Beyond the Cloud: The Next 25 Years of SQLite]]></description><link>https://marcobambini.substack.com/p/the-future-of-databases-is-local</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-future-of-databases-is-local</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Thu, 04 Sep 2025 14:06:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/371f6338-6887-4a98-bcc4-d37835197fc2_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m lucky enough to work on a subject I truly love, something so old in the IT world yet always surprising in how modern and relevant it still is: databases. At the end of the day, a database is nothing more than a way to read and write information efficiently. I feel like I&#8217;ve been dealing with databases forever, ever since I built my first commercial DBMS based on SQLite (<a href="https://en.wikipedia.org/wiki/REAL_Server">https://en.wikipedia.org/wiki/REAL_Server</a>), sometime around 2002 if I recall correctly. I still remember when a single instance managed to serve millions of monthly requests while using only a few dozen MB of memory, and it never crashed once.</p><p>Over the years, a clear trend has emerged, one that I still watch closely: for every new problem, a new database seems to be invented. The space is incredibly fragmented; there&#8217;s always a vendor claiming to have just the right database for each specific challenge. This fragmentation has only accelerated with the rise of AI, bringing new requirements like column-oriented processing and vector search. I have a strong opinion about this: I believe a single database could efficiently solve 90% of use cases. But maybe I&#8217;m a little too biased to say that out loud.</p><p>In any case, there&#8217;s one issue I think should already be behind us: choosing one database over another. Or rather, it would be behind us if we really wanted to push for a technological leap, a true database of the future. Today, the market is neatly split between cloud-based databases and embedded databases running on devices, on the Edge. It&#8217;s as if these were fundamentally different technologies, as if under the hood, they weren&#8217;t essentially the same thing. But they are.</p><p>Anything you can solve with a Cloud database, you can solve with an Edge database, except for one big difference: in the Cloud, there will always be one unavoidable inefficiency, no matter how advanced the technology is, the speed of light. If my request starts in Milan and my server is halfway across the world, light will still take a finite time to travel. That latency, dictated by physics, can&#8217;t be eliminated. It doesn&#8217;t matter how efficient the database is or how powerful the server is: light will always take about 30 ms to travel from Milan to London, or worse, 100&#8211;120 ms to reach New York.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;198b79ad-a16b-4d7e-92eb-ec0d143f646c&quot;,&quot;duration&quot;:null}"></div><p>Over the years, different strategies have been introduced to mitigate this problem. The most obvious one is replicating the same database in multiple regions, ensuring each request is served from the nearest node. But this raises other challenges: guaranteeing consistent reads across all nodes, and managing the complexity of keeping such a system running. My startup has solved many of these issues, but there&#8217;s still one challenge that no architecture like this can fully overcome.</p><p>What happens if there&#8217;s no network? Or if the connection is too slow? Or if my device, robot, or drone can&#8217;t afford to wait even 20 ms for an answer, because it needs to process something in real time? In those cases, the Cloud isn&#8217;t an option. There&#8217;s no way to send a request to some server and expect an instant response.</p><p>Soon, our homes will be full of smart devices and domestic robots. But what if the WiFi goes down? Do the robots fall down the stairs because the server didn&#8217;t respond? Or worse, does our smart alarm system fail because its camera couldn&#8217;t recognize that the face it saw belonged to a family member? That would be completely unacceptable.</p><p>The answer lies in using the technology we already have. Still, with a shift in mindset: <strong>local-first should be the default standard for building apps</strong>, apps that aren&#8217;t disrupted by network issues, airplane mode, or any other variable we can&#8217;t control. If we want to deliver truly great user experiences, the database must be local. Requests should have zero latency, and the database should support the new features modern applications demand: AI, vector search, federated learning, offline sync, and embeddable logic.</p><p>All of this can be done today with SQLite, enhanced by a set of extensions built to tackle these new challenges. It must work at zero latency, in microseconds, and at the same time synchronize intelligently with the Cloud, only when the network allows it, automatically resolving conflicts caused by concurrent transactions.</p><p>SQLite is the <a href="https://www.sqlite.org/mostdeployed.html">most widely deployed software in the world</a>, probably also the <a href="https://www.sqlite.org/testing.html">most rigorously tested</a>, and one of the very few projects with guaranteed <a href="https://www.sqlite.org/lts.html">long-term support for the next 25 years</a>. I&#8217;d even add that it&#8217;s also one of the most underrated pieces of software; too often dismissed as &#8220;just&#8221; an embedded database, when in reality it&#8217;s <a href="https://use.expensify.com/blog/scaling-sqlite-to-4m-qps-on-a-single-server">incredibly efficient as a server-side database too</a>.</p><p>I may sound biased, my startup is dedicated to these very problems and is all-in on SQLite, but I truly believe that, as of 2025, it makes no sense to keep choosing between the Cloud and the Edge. We don&#8217;t need two separate solutions for the same problem. What we need is a unified solution that combines Edge and Cloud, delivered by a single vendor, with a single set of extensions and smart features that can handle every use case. At the end of the day, you need an efficient way to handle data; everything else, like the database engine, the transport layer, or the sync algorithm, is just an implementation detail.</p><p>After 25 years (SQLite was first released in 2000), it&#8217;s time to say &#8220;welcome back&#8221; to SQLite, because the next 25 years will be remembered as the era that opened up <strong>a new market</strong>, one <strong>where the speed of light is no longer a limitation but simply a measure of how fast our new solutions can be</strong>.</p><div><hr></div><p>I&#8217;m the founder of <strong><a href="https://www.sqlite.ai">SQLite AI</a></strong>, our mission is to make <strong>local-first sync with SQLite and AI on the Edge</strong> straightforward and reliable. If you&#8217;d like to experiment with a production-ready sync engine, the same kind of technology discussed in this article, you can explore it at: <a href="https://www.sqlite.ai/sqlite-sync">https://www.sqlite.ai/sqlite-sync</a></p>]]></content:encoded></item><item><title><![CDATA[The State of Vector Search in SQLite]]></title><description><![CDATA[Making vector search fast, memory-efficient, and natural in SQLite.]]></description><link>https://marcobambini.substack.com/p/the-state-of-vector-search-in-sqlite</link><guid isPermaLink="false">https://marcobambini.substack.com/p/the-state-of-vector-search-in-sqlite</guid><dc:creator><![CDATA[Marco Bambini]]></dc:creator><pubDate>Mon, 01 Sep 2025 13:08:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/94f05423-ce3f-4101-8bac-34279d44a958_1920x1080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I usually don&#8217;t like to reinvent the wheel, but sometimes the available tools don&#8217;t quite fit. Recently, while working with vector data in SQLite, I noticed that the current ecosystem doesn&#8217;t fully align with what most SQLite users actually need.</p><p>The typical requirements are not &#8220;billion-scale&#8221; datasets or cloud-only setups. Most developers want something that is:</p><ul><li><p><strong>Memory-efficient</strong> (able to run comfortably on laptops, phones, and small servers).</p></li><li><p><strong>Fast enough</strong> to handle a few million multidimensional vectors.</p></li><li><p><strong>Simple to use</strong> within regular SQLite workflows.</p></li></ul><p>When you look at the current offerings, there&#8217;s a gap.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="/__u/substackcdn.com/image/fetch/$s_!a4p_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="/__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_webp, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 1456w" sizes="100vw"><img src="/__u/substackcdn.com/image/fetch/$s_!a4p_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:18926,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/heic&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://marcobambini.substack.com/i/172465902?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="/__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_424, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 424w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_848, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 848w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_1272, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 1272w, /__u/substackcdn.com/image/fetch/$s_!a4p_!, /__u/marcobambini.substack.com/w_1456, /__u/marcobambini.substack.com/c_limit, /__u/marcobambini.substack.com/f_auto, /__u/marcobambini.substack.com/q_auto:good, /__u/marcobambini.substack.com/fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcce20ded-059a-4a01-af3e-26c2881fa10b_1920x1080.heic 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Existing Options</h2><ul><li><p><strong>sqlite-vss</strong> (Alex Garcia)<br>Built on Meta&#8217;s Faiss C++ library, but plagued by integration issues that ultimately led Alex to abandon it. <a href="https://alexgarcia.xyz/blog/2024/building-new-vector-search-sqlite/index.html#wrong-sqlite-vss">He explains why here</a>.</p></li><li><p><strong>sqlite-vec</strong> (Alex Garcia)<br>A clean re-implementation in C, easier to maintain, exposing a brute-force search strategy. It works via virtual tables, which means vectors must live in separate tables and queries become more complex. (This is the one we first adopted and even sponsored.)</p></li><li><p><strong>libsql (Turso)</strong><br>Uses the <s>HNSW</s> DiskANN algorithm, a powerful choice, but the indexing phase can take hours. For many SQLite use cases, that indexing overhead is simply not practical. (It turns out that indexing is optional in libSQL, thanks https://substack.com/@penberg for the correction)</p></li></ul><div><hr></div><h2>Why Try Something Different?</h2><p>These projects are valuable experiments, but they didn&#8217;t solve the constraints we were facing: speed, memory limits, and natural SQL usage. Out of that frustration, we started prototyping an alternative, which eventually became <strong><a href="https://github.com/sqliteai/sqlite-vector">sqlite-vector</a></strong>.</p><p>The idea was simple:</p><ul><li><p>Use a brute-force-like approach, but <strong>highly optimized</strong>.</p></li><li><p>Implement <strong>hardware-specific distance functions</strong> (auto-selected at runtime).</p></li><li><p>Support quantization, preloading into memory, and multiple data types (FLOAT32, FLOAT16, BFLOAT16, INT8, UINT8).</p></li><li><p>Allow vectors to be stored in <strong>ordinary tables</strong>, with no special virtual tables required.</p></li></ul><div><hr></div><h2>Benchmark Exploration</h2><p>To understand the trade-offs, we ran tests with <strong>100,000 vectors of dimension 384 (FLOAT32)</strong> on an Apple M1 Pro MacBook Pro (2021, 16GB RAM).</p><h3>sqlite-vec (100,000 rows)</h3><pre><code>Insert-time:          1179.07 ms
Full-scan query time:   67.84 ms</code></pre><h3>sqlite-vector (100,000 rows)</h3><pre><code><code>Insert-time:          563.04 ms
Full-scan query time:  56.65 ms

8-bit quantization:   224.85 ms (one-time)
Quant-scan query:      17.44 ms
Quant-preload query:    3.97 ms
Quant-mem usage:       37.38 MB
Recall@20:             1.0000 (20/20) </code></code></pre><p><em>(We couldn&#8217;t complete the libsql test, as index creation ran for hours without finishing.)</em></p><p>All tests for sqlite-vector were run with a <strong>30 MB memory cap</strong>.</p><div><hr></div><h2>Observations</h2><ul><li><p>Insert time is ~<strong>50% faster</strong> than sqlite-vec.</p></li><li><p>Plain query time is ~<strong>16% faster</strong>.</p></li><li><p>With quantization, queries become <strong>3&#215; faster</strong>.</p></li><li><p>With quantization + preload, queries run in <strong>under 4 ms</strong>, roughly <strong>17&#215; faster</strong> than sqlite-vec, while still achieving perfect recall.</p></li><li><p>Memory usage stays low (37 MB for 100k vectors).</p></li></ul><p>Perhaps most importantly, the queries do not need complex JOIN statements to retrieve all information bound to embeddings:</p><pre><code><code>-- Create a regular table
CREATE TABLE images (
  id INTEGER PRIMARY KEY,
  embedding BLOB, -- store Float32/UInt8/etc.
  label TEXT
);

-- Run a nearest neighbor query on the quantized data
SELECT e.id, v.distance
FROM images AS e
JOIN vector_quantize_scan('images', 'embedding', '[12,22,11,243,...]', 20) AS v
  ON e.id = v.rowid;
</code></code></pre><div><hr></div><h2>Closing Thoughts</h2><p>Vector search is becoming an essential feature in many modern applications; yet, the SQLite ecosystem lacks an extension that meets the everyday needs of its community. Most developers don&#8217;t need billion-scale indexes or hours-long indexing jobs. What they need is something lightweight, memory-efficient, and fast enough to handle a few million vectors, all while feeling like &#8220;regular SQLite.&#8221;</p><p>The SQLite ecosystem already has a few approaches to vector search, but none yet feel fully aligned with the everyday needs of SQLite developers: reasonable scale, fast enough queries, efficient memory use, and seamless SQL integration.</p><p>We believe the community could benefit from <strong>sqlite-vector</strong>, which is why we&#8217;ve made it entirely free for open-source projects.</p><p>If you&#8217;re experimenting with vector search in SQLite, I&#8217;d love to hear your experiences, whether with sqlite-vector, sqlite-vec, libsql, or your own experiments.</p><div><hr></div><p>I&#8217;m the founder of <strong><a href="https://www.sqlite.ai">SQLite AI</a></strong>, our mission is to make <strong>local-first sync with SQLite and AI on the Edge</strong> straightforward and reliable. If you&#8217;d like to experiment with a production-ready vector engine, the same kind of technology discussed in this article, you can explore it at: <a href="https://www.sqlite.ai/sqlite-vector">https://www.sqlite.ai/sqlite-vector</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://marcobambini.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>