A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Some notes on Local-First Development (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Is that even respected? Retrospectively? What a shAItshow…
  28. https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
  29. <meta name="robots" content="noai, noimageai">
  30. <!-- Documented, feel free to shoot an email. -->
  31. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  32. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  36. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  37. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  38. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  39. <script>
  40. function toggleTheme(themeName) {
  41. document.documentElement.classList.toggle(
  42. 'forced-dark',
  43. themeName === 'dark'
  44. )
  45. document.documentElement.classList.toggle(
  46. 'forced-light',
  47. themeName === 'light'
  48. )
  49. }
  50. const selectedTheme = localStorage.getItem('theme')
  51. if (selectedTheme !== 'undefined') {
  52. toggleTheme(selectedTheme)
  53. }
  54. </script>
  55. <meta name="robots" content="noindex, nofollow">
  56. <meta content="origin-when-cross-origin" name="referrer">
  57. <!-- Canonical URL for SEO purposes -->
  58. <link rel="canonical" href="https://bricolage.io/some-notes-on-local-first-development/">
  59. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  60. <article>
  61. <header>
  62. <h1>Some notes on Local-First Development</h1>
  63. </header>
  64. <nav>
  65. <p class="center">
  66. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  67. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  68. </svg> Accueil</a> •
  69. <a href="https://bricolage.io/some-notes-on-local-first-development/" title="Lien vers le contenu original">Source originale</a>
  70. </p>
  71. </nav>
  72. <hr>
  73. <p>A few months ago in June, I attended <a href="https://www.youtube.com/results?search_query=Local-First+Meetup+Berlin+%231">a local-first meetup in Berlin</a> organized by Johannes Schickling, formerly the founder of Prisma. An intellectual crackle filled the air as we watched demos of new libraries and products. Many of us had been independently playing around with local-first development ideas for a while — in my case, over a decade — and the in-person meetup gave us the chance to trade notes late into the night.</p>
  74. <p><span class="gatsby-resp-image-wrapper">
  75. <a class="gatsby-resp-image-link" href="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg" target="_blank" rel="noopener">
  76. <span class="gatsby-resp-image-background-image"></span>
  77. <img class="gatsby-resp-image-image" alt="Picture of Berlin Local-First meetup" title="" src="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg" srcset="https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a80bd/berlin-meetup.jpg 148w,
  78. https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c91a/berlin-meetup.jpg 295w,
  79. https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/1c72d/berlin-meetup.jpg 590w,
  80. https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/a8a14/berlin-meetup.jpg 885w,
  81. https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/fbd2c/berlin-meetup.jpg 1180w,
  82. https://bricolage.io/static/0437f7e6c171f69089530459c0d8d109/49ee4/berlin-meetup.jpg 3630w" sizes="(max-width: 590px) 100vw, 590px" loading="lazy" decoding="async">
  83. </a>
  84. </span></p>
  85. <p>In the months since, I’ve continued to tinker with these technologies and collected some point-in-time notes on significant developments and what might happen in the years to come.</p>
  86. <h3 id="table-of-contents"><a href="#table-of-contents" aria-label="table of contents permalink" class="anchor before"></a>Table of Contents</h3>
  87. <h2 id="whats-happening"><a href="#whats-happening" aria-label="whats happening permalink" class="anchor before"></a>What’s Happening?</h2>
  88. <p>The web feels ready for a major upgrade. We had tightly coupled web frameworks in the Rails/Django years and lost them with the shift to API-powered SPAs. The developing database-grade sync technology will tightly recouple our application stacks allowing for a new era of framework innovation.</p>
  89. <p>I see “local-first” as shifting reads and writes to an embedded database in each client via“sync engines” that facilitate data exchange between clients and servers. Applications like Figma and Linear pioneered this approach, but it’s becoming increasingly easy to do. The benefits are multiple:</p>
  90. <ul>
  91. <li>Simplified state management for developers</li>
  92. <li>Built-in support for real-time sync, offline usage, and multiplayer collaborative features</li>
  93. <li>Faster (60 FPS) CRUD</li>
  94. <li>More robust applications for end-users</li>
  95. </ul>
  96. <p>(Some good reading material: this local-first case study on <a href="https://riffle.systems/essays/prelude/">building reactive, data-centric apps</a> and <a href="https://www.youtube.com/watch?v=jxuXGeMJsBU&amp;t=1s">Johannes’ talk in Berlin</a>; I will include some more links later.)</p>
  97. <p>Like the shift to componentized JavaScript UI over the last decade, I believe local-first will be the next large paradigm shift for rich client apps and work its way through the application world over the next decade</p>
  98. <h2 id="why-is-local-first-happening-now"><a href="#why-is-local-first-happening-now" aria-label="why is local first happening now permalink" class="anchor before"></a>Why is Local-First Happening Now?</h2>
  99. <p>I’ve read about and played with local-first type ideas for a decade or so but only now does it seem to be gaining steam with many young startups rushing to productize its ideas.</p>
  100. <p>Figma, Superhuman, and Linear are good examples of pioneering startups in the local-first paradigm and all rely on local-first ideas and bespoke sync engines to support their speed and multiplayer UX.</p>
  101. <p>Many builders now see local-first as a key way to differentiate their applications.</p>
  102. <p>Why is this happening now?</p>
  103. <p>My general model for change in technology is that a given community of practice (like application developers) can only adopt one large paradigm shift at a time. While local-first ideas have been floating around for decades, practitioners have so far been focused on more fundamental changes.</p>
  104. <p>What we’ve seen over the last decade is that application speed and collaboration features are powerful vectors to shake up an incumbent industry. Figma used local-first to displace Sketch and InVision; Linear is using local-first to displace Jira.</p>
  105. <p>We’ve shifted from Rails-type server-rendered apps, to single-page-apps powered by APIs. A core lesson from this transition is that while standard REST and GraphQL APIs are very easy to get started with for solving client/server sync, they require significant effort and skill to scale and refine and they struggle with use-cases like multiplayer and offline support.</p>
  106. <p>Sync engines are a robust database-grade syncing technology to ensure that data is consistent and up-to-date. It’s an ecosystem-wide refactor that many talented groups are exploring to attempt to simplify the application stack.</p>
  107. <p>Assuming they succeed, they’ll provide a solid substrate for new types of framework that can rely on local data and rock solid sync.</p>
  108. <h2 id="will-most-rich-client-apps-use-local-first"><a href="#will-most-rich-client-apps-use-local-first" aria-label="will most rich client apps use local first permalink" class="anchor before"></a>Will Most Rich Client Apps Use Local-First?</h2>
  109. <p>I’ve chatted with a number of developers who — frustrated at maintaining the homegrown bespoke sync systems they wrote — are replacing it with new local-first tooling. The tooling feels a lot better and having standard primitives make it easier to build great experiences.</p>
  110. <p>Local-first is developing into an ecosystem similar to authentication services but for handling data and features that need to be real-time, collaborative, or offline.</p>
  111. <p>The key question for us technologists is whether local-first will remain a niche technology or if it’ll gradually replace the current API-based approach.</p>
  112. <p>It’s still early but I’m confident that at a minimum, we’ll see multiple breakout startups along with a few healthy open-source ecosystems around the different approaches. And if local-first becomes the new default paradigm for handling data,, it will be much larger and reshape many parts of the cloud ecosystem.</p>
  113. <p>But: there are many issues to solve first! Let’s look in detail at one of the first issues that people encounter: handling CRUD operations.</p>
  114. <h3 id="crud-with-crdt-based-sync-engines"><a href="#crud-with-crdt-based-sync-engines" aria-label="crud with crdt based sync engines permalink" class="anchor before"></a>CRUD with CRDT-based Sync Engines</h3>
  115. <p>To fully replace client-server APIs, sync engines need robust support for fine-grained access control and complex write validation.</p>
  116. <p>The most basic use case for an API is <em>state transfer</em> from the server to the client. The client wants to show information about an object so reads the necessary data through the API.</p>
  117. <p>Local-first tools all handle this perfectly. They ensure the latest object data is synced correctly to the client db for querying from the UI.</p>
  118. <p>But while reads are generally easy, support for complex writes is still immature in local-first tooling. Clients tend to have unrestricted write access and updates are immediately synced to other clients. While this is generally fine for text collaboration or multiplayer drawing, this wouldn’t work for a typical ecommerce or SaaS application.</p>
  119. <p>Local-first tools need somewhere to put arbitrary business logic written in code, because real-world systems start out elegant and simple and, over time, accumulate lots of messy, important chunks of logic.</p>
  120. <p>A data property might normally be limited to 20 but Acme Corp paid $50k to get a limit of 100. Or write access needs to be restricted for sensitive information like account balances. Or writes need validation by third party APIs, like a calendar booking system that needs to ask an external calendar API if a time slot is open.</p>
  121. <p>Normally, these inevitable chunks of code and logic live on the server behind an API. And we still need somewhere to put them in a local-first world.</p>
  122. <p>Put differently: local-first CRDT-based sync engines can drive consistency within a system but real-world systems also need an authoritative server which can enforce consistency within external constraints and systems.</p>
  123. <p>Or as Aaron Boodman, a <a href="https://replicache.dev/">Replicache</a> co-founder, put it: “CRDTs converge, but to where?”</p>
  124. <h3 id="using-a-distributed-state-machine-to-handle-complex-writes"><a href="#using-a-distributed-state-machine-to-handle-complex-writes" aria-label="using a distributed state machine to handle complex writes permalink" class="anchor before"></a>Using a Distributed State Machine to Handle Complex Writes</h3>
  125. <p>I asked Anselm Eickhoff and James Arthur (founders of <a href="https://jazz.tools/">Jazz</a> and <a href="https://electric-sql.com/">ElectricSQL</a> respectively, both CRDT-based tools) about how they suggest handling writes that need an authoritative server.</p>
  126. <p>They both suggested emulating API request/response patterns through a distributed state machine running on a replicated object.</p>
  127. <p>So let’s say a client wants to update the user name. It creates the following object:</p>
  128. <div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  129. <span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
  130. <span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"requestedUpdate"</span><span class="token punctuation">,</span>
  131. <span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
  132. <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
  133. <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
  134. <span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
  135. <span class="token punctuation">}</span><span class="token punctuation">,</span>
  136. <span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
  137. <span class="token punctuation">}</span>
  138. <span class="token punctuation">{</span>
  139. <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Bob"</span><span class="token punctuation">,</span>
  140. <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
  141. <span class="token punctuation">}</span></code></pre></div>
  142. <p>The server listens for writes and upon receiving this one, validates the request and updates the state machine object along with the name on the user object (which again are synced back to clients as the “server” is just another client):</p>
  143. <div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
  144. <span class="token property">"machine"</span><span class="token operator">:</span> <span class="token string">"updateUserName"</span><span class="token punctuation">,</span>
  145. <span class="token property">"state"</span><span class="token operator">:</span> <span class="token string">"finished"</span><span class="token punctuation">,</span>
  146. <span class="token property">"request"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
  147. <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
  148. <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span><span class="token punctuation">,</span>
  149. <span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122496</span>
  150. <span class="token punctuation">}</span>
  151. <span class="token property">"response"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
  152. <span class="token property">"error"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
  153. <span class="token property">"timestamp"</span><span class="token operator">:</span> <span class="token number">1694122996</span>
  154. <span class="token punctuation">}</span>
  155. <span class="token punctuation">}</span>
  156. <span class="token punctuation">{</span>
  157. <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Fred"</span><span class="token punctuation">,</span>
  158. <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">123</span>
  159. <span class="token punctuation">}</span></code></pre></div>
  160. <p>This has the interesting and useful implication that in-flight requests are synced to other clients and it’s trivial for the server to emit progress updates. E.g. an app that supports a team of users uploading images for some nifty AI enhancements. The client can emit progress updates on the upload and the server as it works through enhancements.</p>
  161. <p>In other words, requests/responses have the same multiplayer, offline, real-time sync properties as the rest of the app.</p>
  162. <p>This pattern can be wrapped up in a standard mutation function e.g. <code class="language-text">const {res, error} = await updateName({ name: "fred" })</code></p>
  163. <p>Jazz also lets you restrict writes to certain object fields to a specific role. Sensitive information would be marked read-only for clients who would need to request the server to update them.</p>
  164. <h3 id="local-first-dx-is-still-being-explored"><a href="#local-first-dx-is-still-being-explored" aria-label="local first dx is still being explored permalink" class="anchor before"></a>Local-First DX is Still Being Explored</h3>
  165. <p>Beyond the question of <em>can</em> you build any application with local-first tools, there’s still the question of whether devs will <em>want</em> to.</p>
  166. <ul>
  167. <li>Do advantages make it worth learning a new stack and migrating applications?</li>
  168. <li>There’s a lot of missing components still — what will a full production DX for a local-first toolchain look like?</li>
  169. <li>How complex will it feel to build a simple app?</li>
  170. <li>Is there enough demand to fund all the new libraries and products that’ll need to be built?</li>
  171. </ul>
  172. <p>The success of Figma, Superhuman, and Linear suggest these issues will get solved in time.</p>
  173. <h2 id="what-approaches-are-people-exploring-now"><a href="#what-approaches-are-people-exploring-now" aria-label="what approaches are people exploring now permalink" class="anchor before"></a>What Approaches are People Exploring Now?</h2>
  174. <p>I’m grouping approaches I see into three broad categories.</p>
  175. <h3 id="1-replicated-data-structures"><a href="#1-replicated-data-structures" aria-label="1 replicated data structures permalink" class="anchor before"></a>1. Replicated Data Structures</h3>
  176. <p>These projects provide support for replicated data structures. They are convenient building blocks for any sort of real-time or multiplayer project. They typically give you APIs similar to native Javascript maps and arrays but which guarantee state updates are replicated to other clients and to the server.</p>
  177. <p>It feels like magic when you can build a simple application and and see changes instantly replicate between devices with no additional work.</p>
  178. <p>Most replicated data structures rely on CRDT algorithms to merge concurrent and offline edits from multiple clients.</p>
  179. <p>There’s a number of open source and hosted projects offering replicated data structures, including the granddaddy in this space, Firebase, plus many newer ones.</p>
  180. <p>These services are great for making parts of an app real-time / multiplayer. E.g. a drawing surface, a chat room, a notification system, presence, etc. They’re very simple to get started with and the shared data structures approach offers a much better DX than manually passing events through with websockets or push messaging services.</p>
  181. <p>Open source projects:</p>
  182. <p>Hosted services:</p>
  183. <h3 id="2-replicated-database-tables"><a href="#2-replicated-database-tables" aria-label="2 replicated database tables permalink" class="anchor before"></a>2. Replicated Database Tables</h3>
  184. <p>An approach several projects are taking is to sync from Postgres to a client db (generally SQLite). You pick tables (or materialized views) to sync to the client and then they get loaded along with real-time updates as writes land in Postgres.</p>
  185. <p>SQLite in the browser is one big advantage of this approach as it gives you the rich, familiar querying power of SQL in the client.</p>
  186. <p>Given Postgres’ widespread usage and central position in most application architectures, this is a great way to start with local-first. Instead of syncing data in and out of replicated data structures, you can read and write directly to Postgres as normal, confident that clients will be in sync.</p>
  187. <p>I’ve built a number of job queues and notification systems over the years and they’ve all struggled with their version of the Byzantine Generals problem. Clients would miss an update (usually due to being offline), and then users would complain about zombie jobs that never finished. In contrast, replicated database tables mean the background process can simply write updates to Postgres, confident all connected clients will get the update.</p>
  188. <h4 id="postgres-to-sqlite"><a href="#postgres-to-sqlite" aria-label="postgres to sqlite permalink" class="anchor before"></a>Postgres to SQLite:</h4>
  189. <p>ElectricSQL and Powersync support syncing client writes back to Postgres and other clients.</p>
  190. <ul>
  191. <li>
  192. <p>SQLite to SQLite</p>
  193. </li>
  194. <li>
  195. <p>MongoDB to Client DB</p>
  196. </li>
  197. </ul>
  198. <h3 id="3-replication-as-a-protocol"><a href="#3-replication-as-a-protocol" aria-label="3 replication as a protocol permalink" class="anchor before"></a>3. Replication as a Protocol</h3>
  199. <p>The startup <a href="https://replicache.dev/">Replicache</a> has a unique “replication as a protocol” approach. Replicache is a client JS library along with a replication protocol — which lets you integrate arbitrary backends, provided you follow the spec. It’s more upfront work, as the sync engine is “some assembly required”, but as Replicache is mostly your own code, it gives the most flexibility and power of any local-first tool I’ve seen to date. The startup behind Replicache is also building Reflect, a hosted backend for Replicache.</p>
  200. <h2 id="soshould-you-go-local-first"><a href="#soshould-you-go-local-first" aria-label="soshould you go local first permalink" class="anchor before"></a>So…Should You Go Local-First?</h2>
  201. <p>I think it depends on your use case and risk tolerance.</p>
  202. <p>For the right people and teams, it’s an exciting time to jump in. Realtime, multiplayer, and offline features will significantly improve much of our day-to-day software.</p>
  203. <p>For almost any <strong>real-time use case</strong>, I’d choose <em>replicated data structures</em> over raw web sockets as they give you a much simpler DX and robust guarantees that clients will get updates.</p>
  204. <p>For <strong>multiplayer and offline</strong>, again you’ll almost certainly want to pick an open source <em>replicated data structure</em> or hosted service. There’s a lot of difficult problems that they help solve.</p>
  205. <p>The <em>Replicated Database</em> approach also works for the real-time, multiplayer, offline use cases. It should be especially useful for data-heavy applications and ones with many background processing writing into Postgres.</p>
  206. <p>But in general, I’d still be wary of using local-first outside real-time / multiplayer / offline use cases. Local-first is definitely still bleeding-edge. You will hit unexpected problems. A good community has rapidly developed, but there’ll still be some stretches on the road where you’ll have to solve novel problems.</p>
  207. <p>So: if you need local-first, see if it makes sense to isolate the local-first parts and architect the rest of the app (for now) in a more conventional fashion.</p>
  208. <p>I’ve found that most of the major tools have plenty of examples and demos to play with and active Discord channels. There’s also a general local-first community over at <a href="https://localfirstweb.dev/">https://localfirstweb.dev/</a> and they hold regular meetups.</p>
  209. <p>Following along, I’m getting a lot of the same vibes that I got in the then-nascent React community circa 2014 or 2015.</p>
  210. <p>There are also a lot of interesting implications of local-first development in the realm of privacy, decentralization, data control and so on, but I’ll leave it to others more well-versed in these topics to flesh them out.</p>
  211. <p>And some more links below. Happy building!</p>
  212. <p>Discuss this post on <a href="">X.com née Twitter</a>, <a href="https://warpcast.com/kam/0x0c1632">Farcaster</a>, or <a href="https://bsky.app/profile/kam.bsky.social/post/3k77qihvoip2h">Bluesky</a></p>
  213. <p><em>Thanks to Sam Bhagwat, Shannon Soper, Johannes Schickling, Andreas Klinger, Pekka Enberg, Anselm Eickhoff, and James Arthur for reading drafts of this post</em></p>
  214. </article>
  215. <hr>
  216. <footer>
  217. <p>
  218. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  219. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  220. </svg> Accueil</a> •
  221. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  222. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  223. </svg> Suivre</a> •
  224. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  225. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  226. </svg> Pro</a> •
  227. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  228. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  229. </svg> Email</a> •
  230. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  231. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  232. </svg> Légal</abbr>
  233. </p>
  234. <template id="theme-selector">
  235. <form>
  236. <fieldset>
  237. <legend><svg class="icon icon-brightness-contrast">
  238. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  239. </svg> Thème</legend>
  240. <label>
  241. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  242. </label>
  243. <label>
  244. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  245. </label>
  246. <label>
  247. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  248. </label>
  249. </fieldset>
  250. </form>
  251. </template>
  252. </footer>
  253. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  254. <script>
  255. function loadThemeForm(templateName) {
  256. const themeSelectorTemplate = document.querySelector(templateName)
  257. const form = themeSelectorTemplate.content.firstElementChild
  258. themeSelectorTemplate.replaceWith(form)
  259. form.addEventListener('change', (e) => {
  260. const chosenColorScheme = e.target.value
  261. localStorage.setItem('theme', chosenColorScheme)
  262. toggleTheme(chosenColorScheme)
  263. })
  264. const selectedTheme = localStorage.getItem('theme')
  265. if (selectedTheme && selectedTheme !== 'undefined') {
  266. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  267. }
  268. }
  269. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  270. window.addEventListener('load', () => {
  271. let hasDarkRules = false
  272. for (const styleSheet of Array.from(document.styleSheets)) {
  273. let mediaRules = []
  274. for (const cssRule of styleSheet.cssRules) {
  275. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  276. continue
  277. }
  278. // WARNING: Safari does not have/supports `conditionText`.
  279. if (cssRule.conditionText) {
  280. if (cssRule.conditionText !== prefersColorSchemeDark) {
  281. continue
  282. }
  283. } else {
  284. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  285. continue
  286. }
  287. }
  288. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  289. }
  290. // WARNING: do not try to insert a Rule to a styleSheet you are
  291. // currently iterating on, otherwise the browser will be stuck
  292. // in a infinite loop…
  293. for (const mediaRule of mediaRules) {
  294. styleSheet.insertRule(mediaRule.cssText)
  295. hasDarkRules = true
  296. }
  297. }
  298. if (hasDarkRules) {
  299. loadThemeForm('#theme-selector')
  300. }
  301. })
  302. </script>
  303. </body>
  304. </html>