<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alfredo Rivera</title>
    <description>The latest articles on Forem by Alfredo Rivera (@alfierivera).</description>
    <link>https://forem.com/alfierivera</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887808%2Faf3647e9-5c7a-43f4-8b2d-d04bde2fd04a.jpg</url>
      <title>Forem: Alfredo Rivera</title>
      <link>https://forem.com/alfierivera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alfierivera"/>
    <language>en</language>
    <item>
      <title>Escape the cloud</title>
      <dc:creator>Alfredo Rivera</dc:creator>
      <pubDate>Thu, 23 Apr 2026 17:18:56 +0000</pubDate>
      <link>https://forem.com/alfierivera/escape-the-cloud-4j1l</link>
      <guid>https://forem.com/alfierivera/escape-the-cloud-4j1l</guid>
      <description>&lt;p&gt;A local-first platform where users own their data cryptographically — built on encrypted SQLite, CRDTs, and libp2p.&lt;/p&gt;

&lt;p&gt;Here is the simplest thing you can do with the platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPlatform&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM notes WHERE scope_id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user:alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks like any other database query. The difference is where it runs.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;exec&lt;/code&gt; call crosses into a Web Worker running an encrypted SQLite database stored on the user's device. The encryption key is derived from the user's 12-word mnemonic and never transmitted anywhere. No server was involved.&lt;/p&gt;

&lt;p&gt;Add a second device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Same mnemonic → same keys → same identity&lt;/span&gt;
&lt;span class="c1"&gt;// Devices find each other on the LAN via mDNS&lt;/span&gt;
&lt;span class="c1"&gt;// Changes sync peer-to-peer — no server, no internet required&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createPlatform&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mnemonic&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a collaborator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Alice grants Bob editor access to a document&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bobPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;doc:abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EDITOR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Enforced inside the WASM binary via a BEFORE INSERT trigger&lt;/span&gt;
&lt;span class="c1"&gt;// Bob's writes to other scopes are rejected before they touch the database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a live query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;doc-blocks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM blocks WHERE scope_id = ? ORDER BY pos ASC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;doc:abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;diffs&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;applyDiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;diffs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff arrives when a remote peer's change lands locally. Column-level — only what changed. The developer does not manage WebSocket connections, conflict resolution, or presence channels.&lt;/p&gt;




&lt;h2&gt;
  
  
  The schema is the configuration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;scope_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;  &lt;span class="n"&gt;REALTIME_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- syncs at 5ms, live queries fire on change&lt;/span&gt;
  &lt;span class="k"&gt;cursor&lt;/span&gt;   &lt;span class="n"&gt;EPHEMERAL_INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- syncs fast, never backed up&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;    &lt;span class="n"&gt;LWW_TEXT&lt;/span&gt;         &lt;span class="c1"&gt;-- last-write-wins, batched at 50ms&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;crsql_as_crr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blocks'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;crsql_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blocks'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'OWNER:RW, EDITOR:RW, VIEWER:R'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column type affinities configure sync strategy, CRDT merge behaviour, and access policy. One &lt;code&gt;CREATE TABLE&lt;/code&gt;. The runtime handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  When you need a server
&lt;/h2&gt;

&lt;p&gt;Some problems need centralization — aggregation, ranking, feed algorithms. The platform handles this through server peers: specialist participants on the same mesh that receive capability-gated data from users, do the work, and write results back through the same sync mechanism.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Write a request row — server peer picks it up via live query&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  INSERT INTO peer_requests VALUES (?, 'srv:ai:alice', 'ai.assistant', ?, 'pending', NULL, ?, ?)
`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ulid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarise my week&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;myPubKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;

&lt;span class="c1"&gt;// Response arrives through CRDT sync — no separate API layer&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai-result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT status, result FROM peer_requests WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server peer only sees data from users who have issued it a capability grant. Aggregation happens server-side. The underlying data stays with users.&lt;/p&gt;




&lt;h2&gt;
  
  
  What exists today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;slf-db&lt;/strong&gt; — the WASM binary. Encrypted SQLite with CRDT replication, live queries, sync outbox, and capability enforcement baked in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slf-worker&lt;/strong&gt; — the Web Worker. Owns libp2p networking, identity, sync protocol, pairing, inbox, and leader election.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slf-sdk&lt;/strong&gt; — the main thread SDK. Comlink bridge to the worker, Drizzle adapter, and proxies for peers, files, identity, and publishing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slf-proxy&lt;/strong&gt; — the Go binary. Gives native apps TCP connections and LAN discovery via mDNS. Same source compiles to an Electron child process and a Capacitor gomobile framework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slf-auth&lt;/strong&gt; — the auth authority. Registers identities, issues attestations, manages @handles, and federates across instances via the same CRDT sync everything else uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The apps that will run on this — notes, health, finance, passwords — are next.&lt;/p&gt;

&lt;p&gt;The hard open problem is recovery. Losing a mnemonic with no backup is permanent. WebAuthn sealing mitigates the daily-use risk — the mnemonic lives in the secure enclave, daily opens are biometric — but it does not solve the lost-all-devices scenario. That is an active design problem, not a solved one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Codebase is private while the core stabilises. Questions welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>p2p</category>
      <category>webassembly</category>
      <category>database</category>
    </item>
    <item>
      <title>One-trick ponies are easy to replace</title>
      <dc:creator>Alfredo Rivera</dc:creator>
      <pubDate>Sun, 19 Apr 2026 20:12:28 +0000</pubDate>
      <link>https://forem.com/alfierivera/one-trick-ponies-are-easy-to-replace-c5o</link>
      <guid>https://forem.com/alfierivera/one-trick-ponies-are-easy-to-replace-c5o</guid>
      <description>&lt;p&gt;There's a structural weakness in almost every app built in the last decade and we don't talk about it much because we're all doing it.&lt;/p&gt;

&lt;p&gt;The app requires our infrastructure to function. The moment we stop paying the server bill, the moment the company gets acquired and wound down, the moment we make a business decision that doesn't work out — the app stops working. Everything the user put into it becomes inaccessible, or gone.&lt;/p&gt;

&lt;p&gt;We built a generation of software where the app and the company are the same thing. One-trick ponies. Easy to replace, but only after the damage is done.&lt;/p&gt;

&lt;p&gt;The user bears the cost of that fragility. Not us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ownership gap
&lt;/h2&gt;

&lt;p&gt;Most apps have a line in their terms of service that says something like "you own your data." It's technically true in a legal sense. In practice it means almost nothing, because the data lives on infrastructure we control. We can read it. We can lose it. We can hand it over. We can take it with us when we shut down.&lt;/p&gt;

&lt;p&gt;"Your data" and "data we're storing on your behalf" are very different things. The gap between them is where most of the broken trust in modern software lives — data breaches, forced migrations, shutdown notices, subpoenas, acquisitions. Every one of those is a moment when users find out what "your data" actually meant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We built a world where users have to trust us not to misuse what we can never stop ourselves from seeing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This isn't a policy problem. Strong privacy policies don't close the gap — they just describe it more politely. The gap is architectural. The only way to close it is to change where the data lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the data just lived on the device
&lt;/h2&gt;

&lt;p&gt;The idea I've been exploring: each user has their own SQLite database, on their own device, encrypted at rest with a key derived from something only they know. The app reads and writes to it directly. There is no server-side copy. Not "we anonymize your data." Not "we encrypt it at rest." It simply isn't there.&lt;/p&gt;

&lt;p&gt;This isn't a new idea. Local-first software has been a research topic for years. What's changed is that the primitives to build it properly in a browser finally exist. OPFS landed in all major browsers in 2023 — real persistent file system access with the synchronous I/O that SQLite needs. Before that you were stuck with in-memory databases that evaporated on tab close. cr-sqlite brings CRDT semantics to SQLite tables, which means two local databases can sync and merge correctly without a central authority to resolve conflicts. libp2p handles peer discovery and transport so devices can find each other directly — on a local network, over WebRTC, through a mesh that includes cloud nodes as peers rather than authorities.&lt;/p&gt;

&lt;p&gt;The server component doesn't disappear entirely. But it changes its role. Instead of holding data, it helps devices find each other. Instead of being the source of truth, it's a well-connected peer with better uptime. If it goes away, the app keeps working. The data is still there, on the device, where it always was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for how you write the app
&lt;/h2&gt;

&lt;p&gt;The direction I've been exploring is encoding behavior directly in the schema. SQLite lets you declare any string as a column type — it stores it verbatim and applies the closest standard affinity, but ignores what it doesn't recognize. A sync runtime can read those type names at open time and self-configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt;      &lt;span class="n"&gt;REALTIME_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;is_pinned&lt;/span&gt; &lt;span class="n"&gt;LWW_INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;cursor&lt;/span&gt;    &lt;span class="n"&gt;EPHEMERAL_INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;REALTIME_TEXT&lt;/code&gt; streams changes to connected peers as they happen. &lt;code&gt;LWW_INTEGER&lt;/code&gt; gets last-write-wins semantics from cr-sqlite's CRDT merge. &lt;code&gt;EPHEMERAL_INTEGER&lt;/code&gt; broadcasts for presence and expires — never persisted, never synced to offline peers.&lt;/p&gt;

&lt;p&gt;The same &lt;code&gt;CREATE TABLE&lt;/code&gt; statement runs against a vanilla SQLite database. The schema is the configuration. No separate sync layer to set up, no registration calls, no framework to learn on top of the framework you're already using.&lt;/p&gt;

&lt;p&gt;I find this direction interesting but I'm genuinely uncertain whether it covers real developer needs or just the ones I've imagined. This is one of the things I most want to stress-test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The identity question nobody wants to answer
&lt;/h2&gt;

&lt;p&gt;If the data lives on the device and moves between devices as encrypted payloads, identity has to work differently too. There's no server handing out session tokens. The approach I've been working through is a keypair derived from a mnemonic — twelve words that generate the same Ed25519 keypair deterministically on any device. Your keypair is your identity. Your data is addressable by your public key. Messages for you are encrypted to it.&lt;/p&gt;

&lt;p&gt;Lose the mnemonic, lose the identity. No password reset. No support ticket. That's technically clean and I'll be honest — practically uncomfortable for most consumer use cases. Whether it's an unsolvable friction point or an abstraction problem that hasn't been cracked well enough yet, I'm not sure. Hardware keys, biometrics, social recovery schemes all exist as partial answers. None of them feel complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm actually asking
&lt;/h2&gt;

&lt;p&gt;I've been building toward this for a while and I'm at the point where the architecture makes sense to me but I'm aware that's not the same as it making sense to anyone else.&lt;/p&gt;

&lt;p&gt;The one-trick pony problem is real. Apps die and take user data with them regularly enough that we've stopped being surprised by it. The ownership gap is real. "Your data" as a marketing claim rather than a technical property is a kind of slow dishonesty that erodes trust in software broadly.&lt;/p&gt;

&lt;p&gt;Whether this specific approach to fixing those problems is the right one, or whether there are practical walls I haven't hit yet — that's what I want to find out.&lt;/p&gt;




&lt;p&gt;If you've thought about this problem and decided the tradeoffs weren't worth it, I want to hear why. If you've built something in this space and hit walls I haven't mentioned, same. The parts I'm least certain about: whether schema-encoded sync is actually usable or just elegant on paper, and whether keypair identity is a dealbreaker or a solvable UX problem. Both feel important to get right before spending a year building.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>database</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
