<?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: Arnaud Dagnelies</title>
    <description>The latest articles on Forem by Arnaud Dagnelies (@dagnelies).</description>
    <link>https://forem.com/dagnelies</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%2F233722%2F1082a100-a181-4cf6-9727-58ef383ee676.jpeg</url>
      <title>Forem: Arnaud Dagnelies</title>
      <link>https://forem.com/dagnelies</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dagnelies"/>
    <language>en</language>
    <item>
      <title>Cloudflare Workers performance: an experiment with Astro and worldwide latencies</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Mon, 19 Jan 2026 23:31:23 +0000</pubDate>
      <link>https://forem.com/dagnelies/cloudflare-workers-performance-an-experiment-with-astro-and-worldwide-latencies-12ik</link>
      <guid>https://forem.com/dagnelies/cloudflare-workers-performance-an-experiment-with-astro-and-worldwide-latencies-12ik</guid>
      <description>&lt;h2&gt;
  
  
  Why use Cloudflare Workers?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://workers.cloudflare.com/" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt; let you host pages and run code without managing servers. Unlike traditional servers placed in a single or a few locations, the deployed static assets and code are mirrored around the globe in the data centers shown as blue dots below. Naturally, this offers better latencies, scalability and robustness.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7du3wwru3l7tzlqcm7x6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7du3wwru3l7tzlqcm7x6.png" alt="Map of data centers" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Their developer platform also extends beyond “Workers” (the compute part) and include storage, databases, queues, AI and lots of other developer tooling. The whole with a generous free tier and reasonable pricing beyond that.&lt;/p&gt;

&lt;p&gt;Why am I writing this? I find it fairly good, had a good experience with it, and that’s why I will present it here. This article is not sponsored in any way. I just think it’s somehow a responsibility of developers to communicate about the tools they use in order to keep their ecosystem lively. I’ve seen too much good stuff getting abandoned because there was no “buzz”.&lt;/p&gt;

&lt;p&gt;The benefits of using Cloudflare Workers is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Great latencies worldwide&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unlimited scalability&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No servers to take care of&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Further tooling for data, files, AI, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitHub pull requests preview URLs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Free tier good enough for most hobby projects&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;Like every tool, it has use cases for which it shines and others it is not suited for. This is important to grasp and understanding the &lt;a href="https://developers.cloudflare.com/workers/reference/how-workers-works/" rel="noopener noreferrer"&gt;underlying technology&lt;/a&gt; helps tremendously. Basically, in loads your whole app bundled as a script and evaluates it on the fly. It’s fast and works wonderfully if your API and used frameworks are slim and minimalistic. However, it would be ill-advised in following use cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Large complex apps&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The cost of evaluating your API / SSR script will grow as your app grows. The larger it becomes, the more inefficient its invocation as a whole will become. There are also some &lt;a href="https://developers.cloudflare.com/workers/platform/limits/#worker-size" rel="noopener noreferrer"&gt;limits&lt;/a&gt; how large your “script” can be. Although it has been raised multiple times in the past, the fact that this is extremely inefficient will always remain. Thus, be careful when picking dependencies/frameworks since they can quickly bloat your codebase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Heavy resource consumption&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Due to its nature, it is not suited to compute stuff requiring large amounts of CPU/RAM/time like statistic models or scientific computation. Large caches are problematic too. Waiting for long-running async server-side requests is OK though, the execution is suspended in-between and do not count towards execution time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Long-lived connections&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s also problematic. You should rather use polling than keeping connections open.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;In other words: “The slimmer, the better!”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s kind of difficult to say what’s small enough and when it becomes too large. This is rather suited for small self-contained microservices of modest size. Even debugging using breakpoint might turn out challenging. For such larger applications, traditional server deployments would be more suited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What will we build?
&lt;/h2&gt;

&lt;p&gt;A “&lt;a href="https://quoted.day/" rel="noopener noreferrer"&gt;Quote of the Day&lt;/a&gt;” Web application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzslh5herwtqn4k4hceyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzslh5herwtqn4k4hceyy.png" alt="Screenshot" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The purpose is not to build something big, but rather a simple proof-of-concept. The quotes will be stored in a KV store and fetched Client-side. That way, we can measure how fast the whole works and if it lives up to the expectations.&lt;/p&gt;

&lt;p&gt;The default version of &lt;a href="https://quoted.day" rel="noopener noreferrer"&gt;https://quoted.day&lt;/a&gt; is available in two flavours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://quoted.day/spa" rel="noopener noreferrer"&gt;https://quoted.day/spa&lt;/a&gt;: a static page, fetching the quote text/author asynchronously&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://quoted.day/ssr" rel="noopener noreferrer"&gt;https://quoted.day/ssr&lt;/a&gt;: Server-Side-Rendering, rendering the page with the quote on the server&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I swapped which one is the default from time to time to perform experiments. Performance (latency) may vary depending where you are located and whether what you fetch is “hot” or “cold”. Before we delve into details on how to build such an app, let’s take a look at the performance we can expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarking latencies worldwide
&lt;/h2&gt;

&lt;p&gt;Unlike the &lt;em&gt;internal&lt;/em&gt; Cloudflare latency measures, measured “inside” the worker and therefore quite optimistic, we will look at the “real” &lt;em&gt;external&lt;/em&gt; latency thanks to the great tool &lt;a href="https://www.openstatus.dev/play/checker" rel="noopener noreferrer"&gt;https://www.openstatus.dev/play/checker&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;Thanks to that, we can obtain a pretty good idea of the overall latencies that can be observed all over the world. Note however that Australia, Asia and Africa may have rather erratic latencies that “jump” sometimes.&lt;/p&gt;

&lt;p&gt;We will also benchmark multiple things separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Static assets&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stateless functions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hot KV read&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cold KV read&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;KV writes&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, every case will get “two passes”, to hopefully fill caches on the way, and only record the second one.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Static assets&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This was obtained by fetch the main page at &lt;a href="https://quoted.day/spa" rel="noopener noreferrer"&gt;https://quoted.day/spa&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 cdg Paris, France&lt;/td&gt;
&lt;td&gt;33ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;33ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;32ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;54ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;32ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;36ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;35ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;50ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;36ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;26ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;41ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;49ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;29ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;53ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;95ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;30ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;31ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;294ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;436ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 gru Sao Paulo, Brazil&lt;/td&gt;
&lt;td&gt;252ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;559ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;28ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Stateless function&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;his is obtained by fetching the endpoint &lt;a href="https://quoted.day/api/time" rel="noopener noreferrer"&gt;https://quoted.day/api/time&lt;/a&gt; which simply returns the current time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;38ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;32ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;36ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 cdg Paris, France&lt;/td&gt;
&lt;td&gt;75ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;76ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;88ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;73ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;97ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;36ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;62ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;95ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;39ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;92ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;90ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;22ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;108ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;99ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;45ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;27ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;99ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 gru Sao Paulo, Brazil&lt;/td&gt;
&lt;td&gt;89ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;26ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;220ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;26ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;540ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;354ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;71ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Hot KV read&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This is obtained by fetching a fixed quote from the KV store using the endpoint &lt;a href="https://quoted.day/api/quote/123" rel="noopener noreferrer"&gt;https://quoted.day/api/quote/123&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;34ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 cdg Paris, France&lt;/td&gt;
&lt;td&gt;39ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;35ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;37ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;34ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;77ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;103ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;25ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;33ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;55ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;168ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;106ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;52ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;122ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;33ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;123ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;43ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;99ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;88ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;69ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 gru Sao Paulo, Brazil&lt;/td&gt;
&lt;td&gt;99ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;64ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;91ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;345ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;126ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;65ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;856ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;Cold KV read&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This is obtained by fetching a random quote from the KV store using the endpoint &lt;a href="https://quoted.day/api/quote" rel="noopener noreferrer"&gt;https://quoted.day/api/quote&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that each call will cache the result for a day at the edge location, resulting in possibly turning cold reads into hot reads as traffic increases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;131ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;105ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;110ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;130ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 cdg Paris, France&lt;/td&gt;
&lt;td&gt;145ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;134ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;127ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;133ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;197ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;201ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;220ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;243ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;229ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;287ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;270ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;288ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;245ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;502ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;322ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;323ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;247ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;217ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 gru Sao Paulo, Brazil&lt;/td&gt;
&lt;td&gt;601ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;822ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;574ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;335ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;964ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  KV writes
&lt;/h2&gt;

&lt;p&gt;This is obtained by fetching &lt;a href="http://quoted.day/api/bump-counter" rel="noopener noreferrer"&gt;quoted.day/api/bump-counter&lt;/a&gt; which creates a temporary KV pair with an expiration time of 10 minutes. It kind of emulates the concept of initiating a “session”.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;a href="https://quoted.day/api/bump-counter" rel="noopener noreferrer"&gt;🇫🇷 cdg Paris, France&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;128ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;151ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;147ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;194ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;145ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;240ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;176ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;212ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;238ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;305ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;295ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;408ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;423ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;337ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;359ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;409ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;335ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;347ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;438ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;247ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;508ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;684ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;713ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;734ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;1,259ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;1,139ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;2,266ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;SSR Page with KV cold reads&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Lastly, in this test, we combine the reading a random quote (that usually results in a cold KV read) and renders it server-side in a page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Latency&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 koyeb_par Paris, France&lt;/td&gt;
&lt;td&gt;111ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇬🇧 lhr London, United Kingdom&lt;/td&gt;
&lt;td&gt;108ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 railway_europe-west4-drams3a Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;125ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇫🇷 cdg Paris, France&lt;/td&gt;
&lt;td&gt;133ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 koyeb_fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;139ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇩🇪 fra Frankfurt, Germany&lt;/td&gt;
&lt;td&gt;146ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇪 arn Stockholm, Sweden&lt;/td&gt;
&lt;td&gt;142ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇳🇱 ams Amsterdam, Netherlands&lt;/td&gt;
&lt;td&gt;70ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-east4-eqdc4a Virginia, USA&lt;/td&gt;
&lt;td&gt;151ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_was Washington, USA&lt;/td&gt;
&lt;td&gt;159ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ewr Secaucus, New Jersey, USA&lt;/td&gt;
&lt;td&gt;201ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 iad Ashburn, Virginia, USA&lt;/td&gt;
&lt;td&gt;209ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 ord Chicago, Illinois, USA&lt;/td&gt;
&lt;td&gt;217ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 dfw Dallas, Texas, USA&lt;/td&gt;
&lt;td&gt;220ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 sjc San Jose, California, USA&lt;/td&gt;
&lt;td&gt;191ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 railway_us-west2 California, USA&lt;/td&gt;
&lt;td&gt;201ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇨🇦 yyz Toronto, Canada&lt;/td&gt;
&lt;td&gt;255ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 lax Los Angeles, California, USA&lt;/td&gt;
&lt;td&gt;257ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇺🇸 koyeb_sfo San Francisco, USA&lt;/td&gt;
&lt;td&gt;268ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇮🇳 bom Mumbai, India&lt;/td&gt;
&lt;td&gt;422ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 nrt Tokyo, Japan&lt;/td&gt;
&lt;td&gt;332ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;284ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇧🇷 gru Sao Paulo, Brazil&lt;/td&gt;
&lt;td&gt;327ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 railway_asia-southeast1-eqsg3a Singapore, Singapore&lt;/td&gt;
&lt;td&gt;632ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇸🇬 koyeb_sin Singapore, Singapore&lt;/td&gt;
&lt;td&gt;677ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇿🇦 jnb Johannesburg, South Africa&lt;/td&gt;
&lt;td&gt;673ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇦🇺 syd Sydney, Australia&lt;/td&gt;
&lt;td&gt;385ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🇯🇵 koyeb_tyo Tokyo, Japan&lt;/td&gt;
&lt;td&gt;350ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Observations
&lt;/h2&gt;

&lt;p&gt;In is interesting to see how you can infer how the KV works just by watching the numbers. It appears the KV store is not actively replicated, but rather KV pairs are copied “on-demand” at remote locations. When cached (by default 1 minute), subsequent reads are fast. The latencies of such “hot” KV pairs are pretty good overall. No complains here. How long the pair remains cached there can also be configured using the &lt;code&gt;cacheTtl&lt;/code&gt; parameter during the KV &lt;code&gt;get&lt;/code&gt; request. However, the downside of increasing that value is that this cached copy do not reflect changes / updates triggered from other locations during that time.&lt;/p&gt;

&lt;p&gt;Unsurprisingly, cold reads have worse latencies. The other thing you can infer from the numbers is that there seem to be an “origin location”, and cold reads latencies increase proportionally according to the distance to this location. Therefore, pay attention “where” you create the KV store, as it impacts all future latencies around the globe. Note that workers KV might change in the future, this is merely an observation of its state right now.&lt;/p&gt;

&lt;p&gt;While read operations are OK, the write operations are rather disappointing right now. I expected it to have great latencies too, writing to the “edge” and letting the propagation take place asynchronously, but it is the opposite. Writes appear to communicates with the “origin” storage. The time it takes to set a value gets higher the further away you are from where you created the KV store. This is kind of bad news, because setting/updating values is a pretty common operation, for example to authenticate users. Dear Cloudflare team, I hope you improve that part in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  A word of caution
&lt;/h2&gt;

&lt;p&gt;If you develop your webapp, publish it and take a look at it, you will probably not even notice the bad latencies. You will face the optimal latencies with the origin KV store being near you. However, someone at the other end of the planet will have an uglier experience. If that person has a handful of cache misses or writes, the response time might quickly climb into a few seconds before the response arrives. That is &lt;em&gt;not&lt;/em&gt; how I would expect a “distributed” KV store to behave. Let us be clear, right now this behaves more like a centralized KV store with on-demand cached copies at the edge.&lt;/p&gt;

&lt;p&gt;Quite ironically, it basically feels more like a traditional single-location database right now (+caches). While latencies of a single cache miss or a single write is not dramatic, it can quickly pile up with multiple calls and especially write-heavy webapps risk facing increased “sluggishness” depending on their location. Here as well, being “minimalistic” regarding KV calls should be taken to heart during the conception of the webapp using workers.&lt;/p&gt;

&lt;p&gt;Lastly, there was one more setting available in the Worker: “Default Placement” vs “Smart Placement”. I tried both but I did not see noticeable changes within the latencies. I think it’s due to the fact that there is a single KV store call and that it takes time and traffic to gather telemetry and adjust the placement of workers. It might be great, but for this experiment, it had no effect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Single-Page-Applications vs Server-Side-Rendering
&lt;/h2&gt;

&lt;p&gt;Here as well, one is not universally better or worse than the other and the answer which one to use is “it depends”.&lt;/p&gt;

&lt;p&gt;Besides strong differences regarding frameworks and overall architecture, it also has practical fundamental differences for the end user. It’s also fascinating to see history repeating itself, where the internet first started with server rendered pages, than single-page-application with data fetching took over and a resurgence of SSR, just like in the past, just with new tech stacks.&lt;/p&gt;

&lt;p&gt;SSR is actually the easiest one to explain: you fetch all the required data server side, put everything in a template and return the resulting page to the end user. It takes a bit of time and processing power server-side, is not cachable, but the client gets a “finished” page.&lt;/p&gt;

&lt;p&gt;The SPA does the opposite. Although the HTML/CSS/JS is static and cached (hence quickly fetched), the resources are typically much larger due to all the client-side javascript libs needed. Then starts the heavy lifting, where data is fetched and the page rendered, typically while showing a loading spinner. As a result, the total time to render the page is longer.&lt;/p&gt;

&lt;p&gt;However, interacting with the SPA is typically smoother &lt;em&gt;afterwards&lt;/em&gt;, because interactions just exchange data with the server and make local changes to the page. In contrast, SSR means navigating and loading a new page. Hence, the choice whether SPA or SSR is more suited depends on how “interactive” the page/app should be.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a rule of thumb, if it’s more like a static “web page”, go for SSR, if it’s more like an interactive “web app”, go for SPA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lastly, the nice thing about Astro, picked here as illustrative example, is that the whole spectrum is possible: static pages, SPA and SSR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;The source code of this experiment is here: &lt;a href="https://github.com/dagnelies/quoted-day" rel="noopener noreferrer"&gt;https://github.com/dagnelies/quoted-day&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have a Github and a Cloudflare Account, you can also fork &amp;amp; deploy by clicking here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://deploy.workers.cloudflare.com/?url=https://github.com/dagnelies/quoted-day" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdeploy.workers.cloudflare.com%2Fbutton" alt="Deploy to Cloudflare" width="184" height="39"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the button doesn’t work, here it is as link instead: &lt;a href="https://deploy.workers.cloudflare.com/?url=https://github.com/dagnelies/quoted-day" rel="noopener noreferrer"&gt;https://deploy.workers.cloudflare.com/?url=https://github.com/dagnelies/quoted-day&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It will fork the GitHub repository and deploy it on an internal URL so that you can preview it. Afterwards, you can edit the code and it will auto-deploy it, etc.&lt;/p&gt;

&lt;p&gt;Note that the example references a KV store that is mine. So you will have to create your own KV store named and swap the QUOTES KV id in the &lt;code&gt;wrangler.json&lt;/code&gt; file with yours. You will also have to initially fill it with quotes if you want to reproduce the example. Luckily, there are scripts in the &lt;code&gt;package.json&lt;/code&gt; to do just that.&lt;/p&gt;

&lt;p&gt;Everything beyond this point would deserve a tutorial on its own. This was merely the result of an experiment, how the latencies hold up and some insights on the platform. Enjoy!&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>astro</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Passkeys / WebAuthn Library v2.0 is there! 🎉</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Tue, 13 Aug 2024 15:00:07 +0000</pubDate>
      <link>https://forem.com/dagnelies/passkeys-webauthn-library-v20-is-there-6c9</link>
      <guid>https://forem.com/dagnelies/passkeys-webauthn-library-v20-is-there-6c9</guid>
      <description>&lt;p&gt;Hello folks,&lt;/p&gt;

&lt;p&gt;I'm pleased to announce the release of the v2.0 of my &lt;a href="https://webauthn.passwordless.id" rel="noopener noreferrer"&gt;WebAuthn library&lt;/a&gt;!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This library greatly simplifies the usage of &lt;strong&gt;passkeys&lt;/strong&gt; by invoking the &lt;a href="https://w3c.github.io/webauthn/" rel="noopener noreferrer"&gt;WebAuthn protocol&lt;/a&gt; more conveniently. It is &lt;a href="https://github.com/passwordless-id/webauthn" rel="noopener noreferrer"&gt;open source&lt;/a&gt;, opinionated, dependency-free and minimalistic (9kb only).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  👀 Demos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://webauthn.passwordless.id/demos/basic.html" rel="noopener noreferrer"&gt;Basic Demo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webauthn.passwordless.id/demos/conditional-ui.html" rel="noopener noreferrer"&gt;Autocomplete with conditional mediation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webauthn.passwordless.id/demos/authenticators.html" rel="noopener noreferrer"&gt;Authenticators list&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://webauthn.passwordless.id/demos/playground.html" rel="noopener noreferrer"&gt;Testing Playground&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These demos are plain HTML/JS, not minimized. Just open the sources in your browser if you are curious.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 Installation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modules (recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @passwordless-id/webauthn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base package contains both client and server side modules. You can import the &lt;code&gt;client&lt;/code&gt; submodule or the &lt;code&gt;server&lt;/code&gt; depending on your needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@passwordless-id/webauthn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@passwordless-id/webauthn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: the brackets in the import are important!&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternatives
&lt;/h3&gt;

&lt;p&gt;For &lt;strong&gt;browsers&lt;/strong&gt;, it can be imported using a CDN link in the page, or even inside the script itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/@passwordless-id/webauthn@2.0.0/dist/webauthn.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, a &lt;strong&gt;CommonJS&lt;/strong&gt; variant is also available for old Node stacks, to be imported using &lt;code&gt;require('@passwordless-id/webauthn')&lt;/code&gt;. It's usage is discouraged though, in favor of the default ES modules.&lt;/p&gt;

&lt;p&gt;Note that at least NodeJS &lt;strong&gt;19+&lt;/strong&gt; is necessary. (The reason is that previous Node versions had no &lt;code&gt;WebCrypto&lt;/code&gt; being globally available, making it impossible to have a "universal build")&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Getting started
&lt;/h2&gt;

&lt;p&gt;There are multiple ways to use and invoke the WebAuthn protocol.&lt;br&gt;
What follows is just an example of the most straightforward use case. &lt;/p&gt;

&lt;h3&gt;
  
  
  Registration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {client} from '@passwordless-id/webauthn'
await client.register({
  challenge: 'a random string generated by the server',
  user: 'John Doe'
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, this registers a passkey on any authenticator (local or roaming) with &lt;code&gt;preferred&lt;/code&gt; user verification. For further options, see &lt;a href="https://webauthn.passwordless.id/registration/" rel="noopener noreferrer"&gt;→ Registration docs&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {client} from '@passwordless-id/webauthn'
await client.authenticate({
  challenge: 'a random string generated by the server'
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, this triggers the native passkey selection dialog, for any authenticator (local or roaming) and with  &lt;code&gt;preferred&lt;/code&gt; user verification. For further options, see &lt;a href="https://webauthn.passwordless.id/authentication/" rel="noopener noreferrer"&gt;→ Authentication docs&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Verification
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {server} from '@passwordless-id/webauthn'
await server.verifyRegistration(registration, expected)
await server.verifyAuthentication(registration, expected)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the docs for &lt;a href="https://webauthn.passwordless.id/registration/" rel="noopener noreferrer"&gt;registration&lt;/a&gt; and &lt;a href="https://webauthn.passwordless.id/authentication/" rel="noopener noreferrer"&gt;authentication&lt;/a&gt; for the corresponding verification examples.&lt;br&gt;
Or simply interact with real-life examples in the &lt;a href="https://webauthn.passwordless.id/demos/playground.html" rel="noopener noreferrer"&gt;Testing Playground&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⁉️ Why a "Version 2"?
&lt;/h2&gt;

&lt;p&gt;Nobody likes breaking changes, so what's the reason for it? The "Version 2" is not only a complete overhaul of the first version, it also differs from the previous mainly regarding its default behavior and "intermediate payloads". The said, it remains similar, striving for simplicity and ease-of-use.&lt;/p&gt;

&lt;h3&gt;
  
  
  A bit of ❤️ for security keys
&lt;/h3&gt;

&lt;p&gt;Previously, this lib defaulted to using the platform as authenticator if possible. The user experience was improved that way, going straight to user verification instead of intermediate popup(s) to select the authenticator. It was a smooth experience.&lt;/p&gt;

&lt;p&gt;This behavior was born from a time where credentials were always device-bound. The terms "synced credentials" and "passkeys" did not even exist when the initial version of this lib was made. Nowadays, most platforms / authenticators now sync credentials in the cloud. While this is certainly convenient, the security and privacy guarantees are not as strong as with device-bound credentials. That is why security keys now deserve some love, since they are the only ones providing such strong guarantees. &lt;/p&gt;

&lt;p&gt;The new behavior is to let the user choose the authenticator by default. It's also the native protocol's default. We want to give the choice to use security &lt;em&gt;by default&lt;/em&gt;, since they &lt;em&gt;are&lt;/em&gt; more secure.&lt;/p&gt;

&lt;p&gt;In order to better support for security keys, some defaults also changed, like the user verification now being &lt;code&gt;preferred&lt;/code&gt; and being discoverable also being &lt;code&gt;preferred&lt;/code&gt;. Both should allow a broader set of security keys to be usable by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reflects latest protocol changes
&lt;/h3&gt;

&lt;p&gt;The protocol evolved with time, and still is. For example, autocomplete with "conditional mediation" was added. It's an addition I'm not fan of, but certainly has its merits. It basically let's you trigger authentication using the autocomplete feature of a username input field. This is especially useful because there is no way to know if a credential has already been registered for the user or not. However, be wary that this feature is not universally supported for all platforms / browsers / authenticators.&lt;/p&gt;

&lt;p&gt;Another recent change is the usage of &lt;code&gt;hints&lt;/code&gt; ("security-key", "client-device", "hybrid"), which should replace the &lt;code&gt;authenticatorAttachment&lt;/code&gt; and &lt;code&gt;transports&lt;/code&gt; properties in the long term. They were kind of wacky anyway. That said, only Chrome supports hints for now, but it's handled in a backwards-compatible way by this library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payloads compatible with other server-side libs
&lt;/h3&gt;

&lt;p&gt;This is another crucial large change. The response format has been changed completely to be compatible with the output as the &lt;code&gt;PublicKeyCredential.toJson()&lt;/code&gt; method. An official part of the spec that only FireFox implements. &lt;/p&gt;

&lt;p&gt;That way, it is possible to use the browser-side of this library and use almost any other server-side library for your favorite platform. Using this intermediate format increases compatibility cross-libraries in the long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  🙏 Last words 
&lt;/h2&gt;

&lt;p&gt;The original project &lt;a href="https://passwordless.id" rel="noopener noreferrer"&gt;https://passwordless.id&lt;/a&gt; is currently in limbo between proof-of-concept and production-ready. It would simply take more time than I can afford. However, I wanted to wrap up the changes in this library, since the scope is way smaller, and it seems more used by a wider range of people. It also follows the same goal: getting rid of annoying passwords while providing more security. Although I wanted to achieve this goal on a grander scale, this library should at least help others to use passkeys more easily.&lt;/p&gt;

&lt;p&gt;...but it's kind of crazy that I do this. Actually, I'm currently on holiday and I should rather play with my kids outside rather than writing this article. Well, I'm still kind of an old nerd too. I just wanted to wrap it up. Consider this lib stable, use it to your heart's content and if you want to help the open-source ecosystem keep its balance, consider sponsoring this project too.&lt;/p&gt;

&lt;p&gt;Have a nice week,&lt;br&gt;
Arnaud&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>passkeys</category>
    </item>
    <item>
      <title>What do you think of the look and feel?</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Fri, 05 Jul 2024 08:09:46 +0000</pubDate>
      <link>https://forem.com/dagnelies/what-do-you-think-of-the-look-and-feel-2fek</link>
      <guid>https://forem.com/dagnelies/what-do-you-think-of-the-look-and-feel-2fek</guid>
      <description>&lt;p&gt;Hi there,&lt;/p&gt;

&lt;p&gt;I'm about to release the "version 2" of a passkeys lib and I'm currently in the middle updating the docs too.&lt;/p&gt;

&lt;p&gt;What do you think of the current look and feel?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://webauthn-ciy.pages.dev/"&gt;https://webauthn-ciy.pages.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demos are not yet updated and the "content" is not yet finished, but early feedback is great too. I wonder too if I should put the demos in a separate repository.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>showdev</category>
      <category>ui</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Open-Source Exploitation</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Mon, 10 Jun 2024 17:57:45 +0000</pubDate>
      <link>https://forem.com/dagnelies/open-source-exploitation-2eh4</link>
      <guid>https://forem.com/dagnelies/open-source-exploitation-2eh4</guid>
      <description>&lt;p&gt;Hi folks,&lt;/p&gt;

&lt;p&gt;This is not my title, but the title of a presentation I saw few days ago. I wanted to share with you as I think it is important for the open source community as a whole.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/9YQgNDLFYq8"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;For my part, I agree completely to what he said. What about you?&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>opensource</category>
      <category>techtalks</category>
    </item>
    <item>
      <title>Passkeys F.A.Q.</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Sun, 26 May 2024 19:34:03 +0000</pubDate>
      <link>https://forem.com/dagnelies/passkeys-faq-8jo</link>
      <guid>https://forem.com/dagnelies/passkeys-faq-8jo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The WebAuthn protocol is more than 200 pages long, it's complex and gets constantly tweaked. Moreover, the reality of browsers and authenticators have their own quirks and deviate from the official RFC. As such, all information on the web should be taken with a grain of salt.&lt;/p&gt;

&lt;p&gt;Also, there is some confusion regarding where passkeys are stored because the protocol evolved quite a bit in the past few years. In the beginning, "public key credentials" were hardware-bound. Then, major vendors pushed their agenda with "passkeys" synced with the user account in the cloud. Then, even password managers joined in with synced accounts shared with the whole family for example.&lt;/p&gt;

&lt;p&gt;How the protocol works, and its security implications, became fuzzier and more nuanced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What &lt;em&gt;is&lt;/em&gt; a passkey?
&lt;/h2&gt;

&lt;p&gt;Depending on who you ask, the answer may vary. According to the W3C specifications, it's a &lt;strong&gt;discoverable&lt;/strong&gt; public key credential.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you ask me, that's a pretty dumb definition. Calling &lt;strong&gt;any&lt;/strong&gt; public key credential a passkey would have been more straightforward.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is an authenticator?
&lt;/h2&gt;

&lt;p&gt;The authenticator is the hardware or software that issues public key credentials and signs the authentication payload.&lt;/p&gt;

&lt;p&gt;Hardware authenticators are typically security keys or the device itself using a dedicated chip. Software authenticators are password managers, either built-in in the platform or as dedicated app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is the passkey hardware-bound or synced in the cloud?
&lt;/h2&gt;

&lt;p&gt;It depends. It can be either and it's up to the &lt;em&gt;authenticator&lt;/em&gt; to decide.&lt;/p&gt;

&lt;p&gt;In the past, where security keys pioneered the field, hardware-bound keys were the norm. However, now that the big three (Apple, Google, Microsoft) built it in directly in their platform, software-bound keys, synced with the platform's user account in the cloud became the norm. These are sometimes also dubbed "multi-device" credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I decide if the created credential should be hardware-bound or synced?
&lt;/h2&gt;

&lt;p&gt;Sadly, that is something only the authenticator can decide. You cannot influence whether the passkey should be synced or not, nor can you filter the authenticators that can be used.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Concerns have been raised many times in the RFC, see &lt;a href="https://github.com/w3c/webauthn/issues/1714"&gt;issue #1714&lt;/a&gt;, &lt;a href="https://github.com/w3c/webauthn/issues/1739"&gt;issue #1739&lt;/a&gt; and &lt;a href="https://github.com/w3c/webauthn/issues/1688"&gt;issue #1688&lt;/a&gt; among others (and voice your opinion!).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Are passkeys a form of 2FA?
&lt;/h2&gt;

&lt;p&gt;Not by default. Passkeys are a single step 2FA only if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The credential is hardware-bound, not &lt;code&gt;synced&lt;/code&gt;. Then this first factor is "something you possess".&lt;/li&gt;
&lt;li&gt;The flag &lt;code&gt;userVerification&lt;/code&gt; is &lt;code&gt;required&lt;/code&gt;. Then this second factor is "something you are" (biometrics) or "something you know" (PIN code).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;While requiring user verification would be ideal, this also restrict the hardware authenticators that can be used. Not all USB security keys have fingerprint sensor or PIN.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Are hardware-bound credentials more secure than synced ones?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Yes&lt;/em&gt;. When the credential is hardware-bound, the security guarantees are straightforward. You must possess the device. Extremely simple and effective.&lt;/p&gt;

&lt;p&gt;When using synced "multi-device" passkeys, the "cloud" has the key, your devices have the key, and the key is in-transit over the wire. While vendors go to great length to secure every aspect, it is still exposed to more risk. All security guarantees are hereby delegated to the software authenticator, whether it's built-in in the platform or a password manager. At best, these passkeys are as safe as the main account itself. If the account is hacked, whether it's by a stolen password, temporary access to your device or a lax recovery procedure, all the passkeys would come along with the hacked account. While it offers convenience, the security guarantees are not as strong as with hardware bound authenticators.&lt;/p&gt;

&lt;p&gt;The privacy concerns are similar. It is a matter of thrust with the vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to deal with recovery when using hardware-bound credentials?
&lt;/h2&gt;

&lt;p&gt;A device can be lost, broken, or stolen. You must deal with it. The most straightforward way is to offer the user a way to register multiple passkeys, so that losing one device does not imply locking oneself out.&lt;/p&gt;

&lt;p&gt;Another alternative is to provide a recovery procedure per SMS, TOTP or some other thrusted means. Relying on solely a password as recovery is discouraged, since the recovery per password then becomes the "weakest link" of the authentication system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discoverable vs non-discoverable?
&lt;/h2&gt;

&lt;p&gt;There are two ways to trigger authentication. By providing a list of allowed credential ids for the user or not.&lt;/p&gt;

&lt;p&gt;If no list is provided, the default, an OS native popup will appear to let the user pick a passkey. One of the &lt;em&gt;discoverable&lt;/em&gt; credential registered for the website. However, if the credential is &lt;em&gt;not discoverable&lt;/em&gt;, it will not be listed.&lt;/p&gt;

&lt;p&gt;Another way is to first prompt the user for its username, then the list of allowed credential IDs for this user from the server. Then, calling the authentication with &lt;code&gt;allowCredentials: [...]&lt;/code&gt;. This usually avoids a native popup and goes straight to user verification.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There is also another indirect consequence for "security keys" (USB sticks like a Yubikey). Discoverable credentials need the ability to be listed, and as such require some storage on the security key, also named a "slot", which are typically fairly limited. On the other hand, non-discoverable credential do not need such storage, so unlimited non-discoverable keys can be used. There is an interesting article about it &lt;a href="https://fy.blackhats.net.au/blog/2023-02-02-how-hype-will-turn-your-security-key-into-junk/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Can I know if a passkey is already registered?
&lt;/h2&gt;

&lt;p&gt;No, the underlying WebAuthn protocol does not support it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A request to add an &lt;code&gt;exists()&lt;/code&gt; method to guide user experience has been brought up by me, but was ignored so far. See &lt;a href="https://github.com/w3c/webauthn/issues/1749"&gt;issue #1749&lt;/a&gt; (and voice your opinion!).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As an alternative to the problem of not being able to detect the existence of passkeys, major vendors pushed for an alternative called "conditional UI" which in turn pushes discoverable synced credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is conditional UI and mediation?
&lt;/h2&gt;

&lt;p&gt;This mechanism leverages the browser's input field autocomplete feature to provide public key credentials in the list. Instead of invoking the WebAuthn authentication on a button click directly, it will be called when loading the page with "conditional mediation". That way, the credential selection and user verification will be triggered when the user selects an entry in the input field autocomplete.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that the input field &lt;em&gt;must&lt;/em&gt; have &lt;code&gt;autocomplete="username webauthn"&lt;/code&gt; to work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is attestation?
&lt;/h2&gt;

&lt;p&gt;The attestation is a proof of the authenticator model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that several platforms and password managers do not provide this information. Moreover, some browsers allow replacing it with a generic attestation to increase privacy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Do I need attestation?
&lt;/h2&gt;

&lt;p&gt;Unless you have stringent security requirements where you want only specific hardware devices to be allowed, you won't need it. Furthermore, the UX is deteriorated because the user first creates the credential client-side, which is then rejected server-side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There was a feature request sent to the RFC to allow/exlude authenticators in the registration call, but it never landed in the specs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Usernameless authentication?
&lt;/h2&gt;

&lt;p&gt;While it is in theory possible, it faces a very practical issue: how do you identify the credential ID to be used? Browsers do not allow having a unique identifier for the device, it would be a privacy issue. Also, things like local storage or cookies could be cleared at any moment. But &lt;em&gt;if&lt;/em&gt; you have a way to identify the user, in a way or another, then you can also deduct the credential ID and trigger the authentication flow directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the security aspects?
&lt;/h2&gt;

&lt;p&gt;The security aspects are vastly different depending on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Synced or hardware-bound&lt;/li&gt;
&lt;li&gt;User verification or not&lt;/li&gt;
&lt;li&gt;Discoverable or not&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A hardware-bound key is a "factor", since you have to possess the device. The other factor would be "user verification", since it is something that you know (device PIN or password) or are (biometrics like fingerprint).&lt;/p&gt;

&lt;p&gt;Many implementations favor &lt;em&gt;synced credentials with optional user verification&lt;/em&gt; though, for the sake of &lt;em&gt;convinience&lt;/em&gt;, combined with discoverable credentials. This is even the default in the WebAuthn protocol and what many guides recommend.&lt;/p&gt;

&lt;p&gt;In that case, the security guarantee becomes: &lt;em&gt;"the user has access to the software authenticator account"&lt;/em&gt;. It's a delegated guarantee. It is obvious that having the software authenticator compromised (platform account or password manager), would leak all passkeys since they are synced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about privacy aspects?
&lt;/h2&gt;

&lt;p&gt;Well, if the passkeys are synced, it's like handing over the keys to your buddy, the software authenticator, in good faith. That's all. If the software authenticator has bad intents, gets hacked or the NSA/police knocks on their door, your keys may be given over.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that if a password manager has an "account recovery" or "sharing" feature, it also means it is able to decrypt your (hopefully encrypted) keys / passwords. On the opposite, password managers without recovery feature usually encrypt your data with your main password. This is the more secure/private option, since that way, even they cannot decrypt your data.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>passkeys</category>
      <category>webauthn</category>
      <category>authentication</category>
      <category>passwordless</category>
    </item>
    <item>
      <title>My Passkeys lib, now with authenticator icons</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Mon, 26 Feb 2024 21:14:38 +0000</pubDate>
      <link>https://forem.com/dagnelies/passkeys-library-now-with-authenticator-icons-3kgn</link>
      <guid>https://forem.com/dagnelies/passkeys-library-now-with-authenticator-icons-3kgn</guid>
      <description>&lt;p&gt;Hello folks!&lt;/p&gt;

&lt;p&gt;Since I have little time to cater about &lt;a href="https://passwordless.id/" rel="noopener noreferrer"&gt;Passwordless.ID&lt;/a&gt; lately, I wanted to at publish some little update. So here we go, the &lt;a href="https://webauthn.passwordless.id/" rel="noopener noreferrer"&gt;WebAuthn library&lt;/a&gt; (to enable passwordless login using passkeys) now also delivers more information about the "authenticator". In particular the icon and whether it is a multi-device or device-bound credential. (The latter one is actually just interpreting the existing flags)&lt;/p&gt;

&lt;p&gt;👇 Check out the &lt;a href="https://webauthn.passwordless.id/demos/basic.html" rel="noopener noreferrer"&gt;simple demo&lt;/a&gt; if you want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965557829%2F0ae8c2f3-84ee-454c-a3ab-9b0321c5f599.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965557829%2F0ae8c2f3-84ee-454c-a3ab-9b0321c5f599.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👇 Or the list of &lt;a href="https://webauthn.passwordless.id/demos/authenticators.html" rel="noopener noreferrer"&gt;authenticators&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965618988%2Faa3ff3ec-5770-46d6-aef8-c81d5fb7e6a4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965618988%2Faa3ff3ec-5770-46d6-aef8-c81d5fb7e6a4.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👇 Or the &lt;a href="https://webauthn.passwordless.id/demos/playground.html" rel="noopener noreferrer"&gt;playground&lt;/a&gt; to experiment with the various options.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965673392%2Fa056b457-be2e-4e02-8eff-3fc30b348230.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708965673392%2Fa056b457-be2e-4e02-8eff-3fc30b348230.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ As a side note, please do not forget that the challenge should be randomly generated server side! I have found repositories on GitHub using hardcoded challenges, which is of course a red flag regarding security since it opens the way to replay attacks!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read &lt;a href="https://passwordless.id/protocols/webauthn/1_introduction" rel="noopener noreferrer"&gt;here&lt;/a&gt; or &lt;a href="https://webauthn.passwordless.id/" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want a brief introduction of how the WebAuthn protocol behind Passkeys works.&lt;/p&gt;




&lt;p&gt;Starting from version 1.4.0+, the parsed registration payload now looks as follows, with extra properties &lt;code&gt;synced&lt;/code&gt;, &lt;code&gt;icon_light&lt;/code&gt; and &lt;code&gt;icon_dark&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arnaud"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"credential"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authenticator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rpIdHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T7IIVvJKaufa_CeBCQrIR3rm4r0HJmAjbMYUxvt8LqA="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"counter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"synced"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aaguid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"08987058-cadc-4b81-b6e1-30de50dcbe96"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Windows Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon_light"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-light.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon_dark"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://webauthn.passwordless.id/authenticators/08987058-cadc-4b81-b6e1-30de50dcbe96-dark.png"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attestation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQB25KbRrQPjtlx0qZ2Tsvh2YHaPTPTUJznShhr5XnP3zBmVv..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The icons are deliberately made as links to keep the library compact. Otherwise, it would be bloated way beyond the megabyte. Instead, all icons are available as links.&lt;/p&gt;

&lt;p&gt;These authenticator icons were made thanks to this &lt;a href="https://github.com/passkeydeveloper/passkey-authenticator-aaguids" rel="noopener noreferrer"&gt;repository&lt;/a&gt; which contains the icons as data urls, sometimes in PNG, sometimes in SVG, sometimes square, sometimes rectangle, some tiny, some really large, etc. So some extra scripting was done to homogenize them as 64x64 PNGs and host them so that each "aaguid" can be shown through a direct link.&lt;/p&gt;




&lt;p&gt;Thanks for reading. If you like it, leaving a comment or a &lt;a href="https://github.com/passwordless-id/webauthn" rel="noopener noreferrer"&gt;star&lt;/a&gt; is appreciated. Getting some feedback is always nice.&lt;/p&gt;

&lt;p&gt;Have a nice day!&lt;/p&gt;

</description>
      <category>webauthn</category>
      <category>passkeys</category>
      <category>authentication</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I prefer Maven over Gradle</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Tue, 20 Feb 2024 07:58:21 +0000</pubDate>
      <link>https://forem.com/dagnelies/why-i-prefer-maven-over-gradle-5ga5</link>
      <guid>https://forem.com/dagnelies/why-i-prefer-maven-over-gradle-5ga5</guid>
      <description>&lt;p&gt;In the Java world, one of the first question developers encounter is &lt;em&gt;"should I use Grade or Maven as build tool?"&lt;/em&gt;. It's a fundamental decision which will stick to you with time. And when googling it, Gradle's biased comparison even pops up as the top search result (at least for me).&lt;/p&gt;

&lt;p&gt;At first sight, Gradle looks cool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Their website looks way nicer and polished than Maven's one&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The syntax is much more compact than Maven's verbose XML&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gradle is "newer" while Maven is "older"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gradle is much faster (according to them)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No wonder people pick it up when faced with uncertainty and just wanna get started. So now, let me tell what's wrong with Gradle IMHO and why Maven is still the better option, even so many years later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration vs Scripting
&lt;/h2&gt;

&lt;p&gt;Basically, the &lt;code&gt;pom.xml&lt;/code&gt; that you define in &lt;strong&gt;Maven is a "configuration"&lt;/strong&gt;. You define the name, the version, the list of dependencies, etc. Since it follows a specific schema, with a set of properties to define, you can also look at it visually through a UI for example. It's a declarative definition of your library/app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707747413246%2Fa1906d1f-4e9e-4b89-9f26-5171e1dbe9a6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707747413246%2Fa1906d1f-4e9e-4b89-9f26-5171e1dbe9a6.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the opposite, a &lt;strong&gt;Gradle build script&lt;/strong&gt; is exactly that: a script. It's using the &lt;a href="https://www.groovy-lang.org/" rel="noopener noreferrer"&gt;Groovy&lt;/a&gt; language, or recently also Kotlin, to let you write anything you want. Let it sink in, you use a programming language to define what the build should do. You can import other scripts, send a HTTP request to check the current weather and insert a funny UI generated picture in your build artifact.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707748150235%2F7ef9b475-e867-4d0f-84af-2d4ac62230a9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707748150235%2F7ef9b475-e867-4d0f-84af-2d4ac62230a9.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While they have many aspects in common, they nature ("configuration" vs "script") is what differentiates them fundamentally.&lt;/p&gt;

&lt;p&gt;You may think: "Isn't it great if I can do anything with that build script?! It's ultimate freedom!". That is right, but this boon is also a curse.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When I see a Maven project, with a pom.xml, I know what it does and where to find what. It's always the same. Directories, commands to run, changing the version, whatever, it's the same for all maven projects.&lt;/p&gt;

&lt;p&gt;When I see a Gradle project, I have no idea what the build script does. If you don't have a clear documentation ready, you'll have to dive into the build script to actually discover and try to understand what it exactly does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The price of freedom
&lt;/h2&gt;

&lt;p&gt;It's not rare that you need something specific in your build. In both Maven and Gradle, it's possible to do so, but here also their approach is opposite.&lt;/p&gt;

&lt;p&gt;In Gradle, it's straightforward. Since it's scripting, just write whatever you want, you can do anything very easily. Your own build stages, calling functions, using variables, importing some other scripts, whatever. It's easy.&lt;/p&gt;

&lt;p&gt;In Maven, it's the opposite. Adding something custom is more difficult. You will have to use a plugin to enable the specific functionality, or even write a plugin yourself if really necessary. While writing a plugin is definitely more work, this kind of also enforces reusability though.&lt;/p&gt;

&lt;p&gt;The takeaway here is the same as before. While Maven builds tend to always follow the same build stages and conventions, Gradle builds tend to become more and more complex and customized over time, because it's so easy to "just add a few lines" to the build script. Look at it after a few years and the Maven &lt;code&gt;pom.xml&lt;/code&gt; is likely almost as readable as the first days while the Gradle &lt;code&gt;build.gradle&lt;/code&gt; script became rocket science.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As an exercise, I picked a random &lt;code&gt;gradle.build&lt;/code&gt; file from another team at work to look at it. It had over thousand lines and the few dependencies it had were externalized in another file and combined in a fancy way.&lt;/p&gt;

&lt;p&gt;On the opposite, pick any Maven project, and the list of dependencies will always be in the same place, in the &amp;lt;dependencies&amp;gt;...&amp;lt;/dependencies&amp;gt; tag.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;History repeating itself.&lt;/em&gt; As a side note, it's interesting to see that in the very early days of Java, before maven was born, build scripts were the norm. In the beginning they were plain shell scripts invoking &lt;code&gt;javac ...&lt;/code&gt; to compile the source code, packaging it, etc. Then came "ant" to do the same, in a bit more structured way but still tended to become customized and complex over time. Then one day came the idea to use a more declarative approach, by defining a project and its dependencies while letting the tool take care of how it is build. Maven was born. Then, some day, Gradle was born, because "I want to customize stuff".&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradle lies in your face
&lt;/h2&gt;

&lt;p&gt;Now, this is a little grudge of mine against Gradle's marketing habits. When going on their website, they will feature a "Gradle vs Maven" comparison claiming that Gradle is "oh so much faster" than Maven and the following picture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707751546753%2F9c1332b6-cd25-4655-a5e1-c65dfb19af29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1707751546753%2F9c1332b6-cd25-4655-a5e1-c65dfb19af29.png" alt="pseudo-benchmark"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's take a closer look...&lt;/p&gt;

&lt;p&gt;First, what's shocking is that a "Clean build with tests" is so much faster than the original build! It's almost instant! Including tests! ...let me get this straight: &lt;em&gt;this is not "clean" at all. It's just doing nothing&lt;/em&gt;. I find Maven much more sensible in that case, it actually rebuilds everything from scratch. To go a little bit further, a "build" in Maven will just check for changes and compile changed files, which would result in a similar figure, while a "clean build" will remove the whole directory and re-build everything. I find this should be the expected behavior unlike Gradle's "clean build" not cleaning anything. After all, the aim of a clean build is usually to fix issues due to some undesirable thing lying around in the build directory, for whatever reason.&lt;/p&gt;

&lt;p&gt;Then, let's look at the normal case: is Gradle really twice as fast? Well, here is another question for you: who compiles the source code? ...got an idea? Well, it's the &lt;code&gt;javac&lt;/code&gt; compiler from the JDK, it's not the build tool! So why would Gradle be twice as fast?! Here is the trick: &lt;em&gt;Gradle runs the tests in parallel while Maven do it sequentially&lt;/em&gt;. That is the reason! Gradle ain't faster or anything, it just runs the tests in parallel. I dislike this default. It's just a question of time until you get tests having side-effects and race conditions. Then you'll obtain "Heisenberg tests" succeeding sometimes and failing sometimes, depending on how their executions overlap. You'll wonder why and waste lots of time investigating the issue. Moreover, it usually runs in a background jobs after commits anyway.&lt;/p&gt;

&lt;p&gt;Now, while I dislike Gradle's defaults, what I'm really annoyed about is how they distort the truth. They should say "we run tests in parallel by default and our 'clean' does nothing instead!". That should have been the correct way to put it instead of using their misleading statements insinuating that they compile faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradle is not simple
&lt;/h2&gt;

&lt;p&gt;For Maven, the scope of dependencies is relatively straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Compile (the "usual" dependency)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test (for tests)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provided (provided at runtime by JDK or a container)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runtime (quite rare. For drivers or alike available at runtime but not for compiling)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's enough and I never needed anything else.&lt;/p&gt;

&lt;p&gt;Gradle on the other hand has &lt;a href="https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_configurations_graph" rel="noopener noreferrer"&gt;&lt;em&gt;lots&lt;/em&gt; of scopes&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;api&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;implementation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;compileOnly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;compileOnlyApi&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;runtimeOnly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;testImplementation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;testCompileOnly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;testRuntimeOnly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;...a few more deprecated scopes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;...a few more classpath scopes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;...you can also extend and combine scopes&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Well, you basically get it. Gradle is "super-customizable", so much that you often wonder what it exactly does or that you make a mistake without realizing. Gradle sells it as "Maven has few, built-in dependency scopes, which forces awkward module architectures" but IMHO it's Gradle which is confusing and overcomplexified here while Maven has exactly what's actually required.&lt;/p&gt;

&lt;p&gt;That is just the tip of the iceberg. But basically Gradle is super-customizable while maven favors conventions. No wonder Gradle is also a company that thrives with support and training. If it was simple, such things would not sell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradle needs maven, but not the other way round
&lt;/h2&gt;

&lt;p&gt;Every single library in the Maven Central Repository must have &lt;code&gt;pom.xml&lt;/code&gt;. It's the declarative definition of the library containing name, version, license, etc, and most importantly the list of its dependencies. Without a Maven &lt;code&gt;pom.xml&lt;/code&gt; there would be no Central Repository nor dependency management possible.&lt;/p&gt;

&lt;p&gt;Whether you use Grade or Maven, both read the &lt;code&gt;pom.xml&lt;/code&gt; Maven definition to build the dependency tree. It's at the core of the dependency dependencies system to pull all transitive dependencies and resolve version conflicts.&lt;/p&gt;

&lt;p&gt;In other words, Maven can live without Gradle, but Gradle still needs Maven to exist. Maven just applies a standardized build based on the &lt;code&gt;pom.xml&lt;/code&gt; while Gradle builds in in some way and generates a &lt;code&gt;pom.xml&lt;/code&gt; as a build step if you want to actually publish your library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maven isn't perfect either
&lt;/h2&gt;

&lt;p&gt;Now, I bashed a lot about Gradle, but Maven isn't perfect either. It has issues too. Their website sucks IMHO, it could welcome YAML as more compact alternative format, some plugins should be built-in and the format itself could be tweaked here and there. But overall I find it OK considering it's a format that lived more than 20 years.&lt;/p&gt;

&lt;p&gt;The other drawback is a lack of flexibility. It's indeed rigid in how it expects your project to be and may become problematic if you need for example to mix multiple different techs. For example a building a node project, running a python script, etc. as part of the build procedure to place some extra stuff inside the produced artifact. But for that IMHO, it's better to use CI scripts, running as GitHub actions or GitLab pipelines to build a "mixed bundle". Let each tech stack build its own artifacts and combine them later through scripting. I favor that approach over pushing the build scripts customizations too far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it with a grain of salt
&lt;/h2&gt;

&lt;p&gt;While I bashed at Gradle and praised Maven, it should be taken with a grain of salt. At the end of the day, they are just tool and either can be used wisely or like a fool.&lt;/p&gt;

&lt;p&gt;With maven too, you can also produce "monster &lt;code&gt;pom.xml&lt;/code&gt; files" by using tons of plugins and super-complex configurations overriding all defaults. Likewise, Gradle is not necessarily a monster. Use it wisely, keep your build script clean, refrain from adding custom build steps and you will do just fine. It's not bad per-se.&lt;/p&gt;

&lt;p&gt;It's just that by default, in the hands of average developers, Maven's &lt;code&gt;pom.xml&lt;/code&gt; will tend to remain understandable (because it takes effort to escape out of the conventions) while Gradle's &lt;code&gt;build.gradle&lt;/code&gt; will tend to become more complex and customized over time (because it's so easy to do so). All the small shortcuts now and little extra steps that stray away from the build conventions tend to become liabilities in the long term.&lt;/p&gt;

&lt;p&gt;As said previously, Gradle's great flexibility and customizability of the build is both a boon and a curse. Although I prefer to build "generic" projects where I can, because it's by far simpler to maintain in the long run, using Gradle definitely has its place when you need more specific stuff that requires customization.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: as a rule of thumb, Maven's &lt;code&gt;pom.xm&lt;/code&gt;tends to remain fairly generic with time, while Gradle's &lt;code&gt;build.gardle&lt;/code&gt; leans towards being highly customized and therefore complex. This is due to their "nature", while Maven is based on a rigid project "definition", Gradle is a free form build "script".&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>java</category>
      <category>maven</category>
      <category>gradle</category>
    </item>
    <item>
      <title>How security and privacy impacts the database design</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Tue, 05 Dec 2023 08:07:06 +0000</pubDate>
      <link>https://forem.com/dagnelies/how-security-and-privacy-impacts-the-database-design-51ed</link>
      <guid>https://forem.com/dagnelies/how-security-and-privacy-impacts-the-database-design-51ed</guid>
      <description>&lt;p&gt;What I like about the banner picture above is that it is a very useful analogy to our problem at hand. A database is nothing else than a glorified electronic version of it. Lots of small storage boxes containing some data, and identified by a label or number, usually called "primary key".&lt;/p&gt;

&lt;p&gt;In security, the main concern is protecting against breaches altogether. However, it is also a good idea to think about mitigating impact in case of breaches. Assuming we deal with some kind of sensitive user data, let's have some thoughts on what should be on the labels and the content of these wooden boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should be the user "identifier"?
&lt;/h2&gt;

&lt;p&gt;Should it be the e-mail? Or a username? ...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: No, it should be an anonymous ID.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How to &lt;em&gt;identify&lt;/em&gt; users is something to be decided at the very beginning. It's crucial information that spreads everywhere: in the database, in links, in software logic... Soon, everything will be tied to it and too late to change. Doing so will be extremely difficult in the best case, or impossible in the worst case.&lt;/p&gt;

&lt;p&gt;But why should it be an anonymous ID? ...It's not only about data breaches.&lt;/p&gt;

&lt;p&gt;That ID is probably going to show up everywhere. In URLs, in logs, in databases, sent to third-party services... So, in the grand scheme of things, privacy-wise, it's better to be anonymized.&lt;/p&gt;

&lt;p&gt;Security-wise, using a username/e-mail as the primary ID is not a vulnerability by itself. However, it slightly increases the "attack surface". For example, imagine you have a REST API with some endpoints accidentally not properly controlling access. If they accept a username/e-mail as parameter, it's trivial to invoke these endpoints with other user's username/e-mail as input, since these are fairly easy to find out. On the other hand, using anonymized IDs would already make exploiting such a vulnerability more difficult, since you don't know other's ID in the first place. This not only applies for REST API, but in all kinds of hacking, whether it's digging in a stolen file or eavesdropping traffic. It does not make your system invulnerable, but it's an additional layer of safety.&lt;/p&gt;

&lt;p&gt;Imagine again the wooden boxes above, just don't place names on it for everyone to see but use some other identifier instead. For example, use the SHA256 of the username/e-mail with a salt. That's simple, deterministic and anonymous.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the stored data?
&lt;/h2&gt;

&lt;p&gt;Most of the time, plain data is read and written directly to the database. The connection is typically protected by credentials to restrict access. This is all good and fine but the main access credentials become critical. If the access credentials are leaked or stolen, all the data can be extracted. Plain and simple.&lt;/p&gt;

&lt;p&gt;Depending on the sensitivity of the data, like for example personal or financial data, another layer of protection is required: encrypting the data. Instead of placing the original text sheet in the wooden box, you encrypt it beforehand. Reading and writing encrypted data in the database makes it unreadable to others. Even database administrators would not be able to pry into the stored data, nor any other software not in possession of the encryption key. Likewise, this also increases privacy.&lt;/p&gt;

&lt;p&gt;Note that although most databases also feature some encryption mechanisms, it typically refers to the raw data persisted on disk. To make the database storage files unreadable when stolen. These should not be mixed up and ideally, both should be used.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: always encrypt all personal, financial or otherwise sensitive information when storing it in a database&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Hash what you can!
&lt;/h2&gt;

&lt;p&gt;Not only passwords but also recovery codes and other nonces.&lt;/p&gt;

&lt;p&gt;As an anecdote, it reminds me of a story about a hacker who found an SQL injection vulnerability, enabling him to pry into the database. Of course, the user passwords were hashed. Even the sensitive data was encrypted. On the other hand, the recovery codes were stored "as is". The hacker now just had to start a recovery procedure for an arbitrary user account, and then look up the corresponding plain text recovery code. Bam! The hacker could now simply complete the recovery procedure and reset the password for any user account. That escalated quickly.&lt;/p&gt;

&lt;p&gt;So if you don't really &lt;em&gt;need&lt;/em&gt; the plain text data, don't even store it, just a hash of it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: hash not only passwords but also recovery codes, nonces, challenges and such.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pairwise identifiers
&lt;/h2&gt;

&lt;p&gt;User accounts are usually identified by some sort of ID. It might be a hash, a UUID, a username, an e-mail, whatever. When some third-party interacts with your service, it'll likely use this ID to identify the user or resource.&lt;/p&gt;

&lt;p&gt;This also means that this ID is "universal" and that everyone out there will use this ID as an identifier. A user can be tracked that way, and attack attempts can be carried out that way using the known ID from another party.&lt;/p&gt;

&lt;p&gt;A concept to push privacy and security further is called "pairwise identifiers". Each "consumer" of your service will be provided with different user identifiers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Third-party service XYZ: can you please send me data related to user ABC123 ?&lt;/p&gt;

&lt;p&gt;Provider: sure ...let me check ...for you XYZ, the account ABC123 is mapped to "Bob" ...here you go.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course, "Bob" is not the username, but should be the internal primary id.&lt;/p&gt;

&lt;p&gt;It's about providing a mapping so that each third-party sees different IDs, so that they cannot correlate users among themselves. Also, it prevents leaked IDs from being used by others. As usual, these benefits both privacy and security.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: it's best to provide third-party services anonymized IDs, unique third-party for each third-party. Both for privacy and security.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Good ol' cookies
&lt;/h2&gt;

&lt;p&gt;This last advice is not directly related to the database but rather to how to maintain browser sessions. There are two camps: "use a good ol' session ID cookie" vs "use a JSON Web Token" (In an Authorization header or as cookie). The latter sounds more modern and trendy but it has drawbacks regarding security.&lt;/p&gt;

&lt;p&gt;Let's review the good old way of handling sessions first. It's fairly straightforward: set a "Http-Only" cookie with some random session identifier when the user is authenticated. Voila, done. The browser will automatically send the cookie on each subsequent request and it cannot be read nor written by scripts. Server-side, you can retrieve the session data based on that id. Then, when the user signs out you can remove the cookie and clear your session data. Same if the user is inactive for too long. Simple, effective and there is not much that can go wrong.&lt;/p&gt;

&lt;p&gt;Regarding JWT usage, there are two dangers with it. First, if the token which is typically stored browser side is stolen, the attacker can impersonate the user, even long after the user signed out... Except if you keep a database of revoked tokens, which not only loses the main benefit of JWT being "stateless" but also adds undesired complexity.&lt;/p&gt;

&lt;p&gt;The second danger is the signing key being stolen, whether it's because of an accident, a vulnerability exploit or a malicious insider. Although this is unlikely because the key should be well protected, the potential consequences of a breach are catastrophic. Basically, your service would be doomed overnight because attackers would be able to impersonate any user at will, bypassing authentication altogether. This is a worst-case scenario that would not be possible for sessions identified by random IDs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: prefer random identifiers in a Http-Only cookie over using JWT tokens for user sessions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Doing it "later" is no good
&lt;/h2&gt;

&lt;p&gt;It might be tempting to delay it, since it introduces complexity to the codebase. However, it's no good. The more you delay it, the more effort and time will be consumed in a later stage refactoring anyway. But even more importantly, while some changes &lt;em&gt;can&lt;/em&gt; be delayed, others would break the API and data compatibility for all "consuming" software!&lt;/p&gt;

&lt;p&gt;In particular, everything regarding hashed user IDs and pairwise identifiers will be breaking all software and integrations relying on these IDs. Changing these is a "hard cut" that should be done ASAP early on, ideally during the conception phase.&lt;/p&gt;

&lt;p&gt;Other changes have a less critical impact. For example, hashing of short-lived recovery codes, nonces, challenges, etc. can be done in a version update. This will invalidate existing codes, nonces, and challenges and cause a small disturbance, but everything will work fine again afterwards.&lt;/p&gt;

&lt;p&gt;The only change which is delayable with less concern is the data encryption. This is an internal database change, opaque to the API which remains unchanged and the consumers as well. It can be done in one fell swoop by encrypting all the data in the database at once using a background process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: don't delay it, do it ASAP since it's breaking changes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Side benefits
&lt;/h2&gt;

&lt;p&gt;Following these recommendations will increase the security and privacy of your system, but that's not all. Doing this, although it sounds complex, has its benefits too regarding code structure. &lt;em&gt;It forces the software to access user data through a streamlined access point instead of fetching it directly from the database&lt;/em&gt;. When all calls for user data go through this access point, it's also easier to monitor, control access and properly handle the data in this one place.&lt;/p&gt;

&lt;p&gt;At least, that's what we experienced after our "&lt;a href="https://blog.passwordless.id/endless-refactoring"&gt;endless refactoring&lt;/a&gt;" at &lt;a href="https://passwordless.id"&gt;Passwordless.ID&lt;/a&gt;. The refactoring is large, and the "version 2" breaks compatibility, requiring us to deploy a separate version and clear all user accounts. It is a &lt;em&gt;very&lt;/em&gt; hard cut. However, we are pleased with the result. The system is now more secure, with better privacy, and better structured than ever before. Something I am proud of!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR: it will even make your codebase structure cleaner!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks for reading! If you liked it, leave a comment!&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Endless refactoring ...when things keep piling up</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Sat, 18 Nov 2023 09:26:22 +0000</pubDate>
      <link>https://forem.com/dagnelies/endless-refactoring-when-things-keep-piling-up-1272</link>
      <guid>https://forem.com/dagnelies/endless-refactoring-when-things-keep-piling-up-1272</guid>
      <description>&lt;p&gt;Refactoring is a necessary thing. The reasons are numerous, but most of them have one thing in common: they are underestimated. As the old adage says: "the devil lies in the details". And typically these "details" surface during refactoring itself, leading to even more stuff to refactor, sometimes creeping up to dangerous levels.&lt;/p&gt;

&lt;p&gt;This is a tale of such refactoring for &lt;a href="https://passwordless.id"&gt;Passwordless.ID&lt;/a&gt;, or rather how a large refactoring was aborded in the middle in favour of an intermediate solution.&lt;/p&gt;

&lt;p&gt;Well, it turns out the refactoring was closer to a whole rewrite. But before delving into the details, let's check the "why" it was done, since the goals are what drive such refactoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The big refactoring started to take place, mainly driven by a follow-up of &lt;a href="https://blog.passwordless.id/ui-api-db-pushing-three-tier-architecture-too-far"&gt;UI, API, DB: pushing "three-tier architecture" too far?&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before, front-end and back-end were in two separate repositories, with their own lifecycle. However, from a workflow perspective, &lt;strong&gt;having both the API and UI in the same repository is more convenient&lt;/strong&gt;. Changes always go hand-in-hand, you have a single URL for previews, no &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"&gt;CORS&lt;/a&gt; necessary, a single pipeline and deployment, etc. The code is almost the same, it's just more convenient to work with a single repository.&lt;/p&gt;

&lt;p&gt;The second goal was &lt;strong&gt;improving user experience, and specifically reducing "time-to-rendering" as much as possible&lt;/strong&gt;. Currently, this was slowed down by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;130kb assets loading due to frontend framework (not that much, but way larger than it could be)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Waterfall loading" (load initial assets, run script, fetch dynamic content, load other stuff, render it all ...duh, feels sluggish)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cross-origin requests (adds one more "&lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request"&gt;preflight&lt;/a&gt;" HTTP round-trip)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combination of these things resulted in rendering times above a second when not cached, sometimes even two if the CDN caches also missed. In other words, it was slow despite being a super simple page.&lt;/p&gt;

&lt;p&gt;Lastly, some &lt;strong&gt;structural changes to improve security and privacy&lt;/strong&gt; would require changes on the data level. This includes changing the primary ID from the username to a hash and another round of encryption for the stored data. This would break the compatibility with existing data. Please note though that the privacy and security of the existing version is by no means compromised. This is about being paranoid and security/privacy in-depth by adding one more protection layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  How?
&lt;/h2&gt;

&lt;p&gt;There were three main areas related to the refactoring.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Combining both codebases with same domain previews&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Making the UI more lightweight&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Adapting the data layer&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are just a few sentences but represent lots of work. In particular, the UI refactoring. Making this more lightweight is easier said than done. Investigating alternatives and shifting away from the larger Vue framework turned out to be quite the burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake made
&lt;/h2&gt;

&lt;p&gt;We proceeded as follows&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Merge both codebases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactor the backend code while we are at it 🙄&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Investigate frontend framework alternatives 😅&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Try out various frontend techs😬&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start porting UI🥵&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What started as a refactoring was turning more and more in the direction of a full rewrite.&lt;/p&gt;

&lt;p&gt;The refactoring of the backend code was a bit time-consuming but worth it IMHO. It's slightly more lightweight and forcing a proper file structure is a good thing. It makes the code more organized and neater. You can directly pinpoint API endpoints to source code files and the related middleware without even looking at the code.&lt;/p&gt;

&lt;p&gt;On the other hand, exploring the vast space of technologies available to replace the front-end was a bottomless pit. Even the first steps of refactoring the UI became a huge time sink. It's not only comparing the few major frameworks, it's even broader, related to the methodology: ranging from SPA (single-page-applications), to SSR (server-side rendering), to SSG (server-side generation), to bundling tools for plain HTML/TS/JS/CSS. Each of these methodologies has its own ecosystem and tools, with many frameworks to compare. Last but not least, many frameworks are often hybrids and can be "configured" differently to span a range of SPA/SRR/SSG.&lt;/p&gt;

&lt;p&gt;In retrospect, it was a big mistake. It slowly but surely evolved into a complete rewrite. The UI refactoring is a huge endeavour that should have been left untouched and made separately. &lt;em&gt;Not because it should not be done, but because it should not have been done&lt;/em&gt; &lt;strong&gt;&lt;em&gt;now&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it should have proceeded
&lt;/h2&gt;

&lt;p&gt;The whole order in which we started the "big refactoring" was wrong. Of course, it wasn't originally expected to be that time-consuming either, it was just "let's refactor that". In hindsight, we should have paid much more attention to the priorities and plan the refactoring accordingly. It should have been done in smaller steps, one after another, and in an order that makes sense according to the priorities.&lt;/p&gt;

&lt;p&gt;In particular, the UI-related one should have been done last. Of course, from a marketing and business point of view, one might disagree. The holy UI/UX would take priority, with the slogan "make it nice first, with a slicker UI which loads super fast". Something that "looks awesome" ...but that would hide and delay upcoming breaking changes, which would just frustrate early adopters later on. I'm against these kinds of short-term wins at the price of long-term liabilities. The sooner the breaking change is done, the better. That is also why any kind of promotion is on hold until this time-consuming refactoring is completed.&lt;/p&gt;

&lt;p&gt;What should have been prioritized is merging both codebases into one for single domain deployment, data schema refactoring to use hashes as IDs and one more encryption layer. The reason to handle them first is because it's a breaking change. These two things make it incompatible with the original first version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Merge both codebases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure single domain dev previews work fine&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apply schema changes for hashed IDs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add extra encryption round&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once this is done, the new version can be published. And afterwards, the UI and back-end related "improvements" can take place. Those which take more time but keep compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  To abord or to persevere?
&lt;/h2&gt;

&lt;p&gt;We're now stuck "in the middle" of the rewrite including the new UI, so it's kind of annoying to drop the work in progress halfway. On the other hand, it looks like the road ahead is still quite long. So, despite it's annoying to let the work in progress halfway done, it's better for Passwordless.ID as a whole. The sooner the "v2" is released, the better.&lt;/p&gt;

&lt;p&gt;Also, what we did not investigate was how to make the existing UI lighter. We aren't talking about excessive amounts here, it's ~130kb gzipped JS/CSS over the wire. But for a simple sign in/up page, it's still unnecessarily large. After some experimenting, it turns out that ~100kb can be spared, just by dropping the &lt;a href="https://github.com/bootstrap-vue-next/bootstrap-vue-next"&gt;bootstrap-vue-next&lt;/a&gt; lib in favour of using plain bootstrap classes directly and a few workarounds for special components. That the lib came in with that much "baggage" and could not be properly "tree-shaken" came unsuspected. Dropping it, the page goes from ~130kb =&amp;gt; ~30kb, already much nicer.&lt;/p&gt;

&lt;p&gt;The full UI rewrite will still take place later on, to be even more efficient and internationalized. But for now, we will make the bare minimum UI changes and focus on the breaking change first, which will hopefully be completed sooner.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>UI, API, DB: pushing "three-tier architecture" too far? 🤔</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Sun, 17 Sep 2023 08:11:51 +0000</pubDate>
      <link>https://forem.com/dagnelies/ui-api-db-pushing-three-tier-architecture-too-far-47oa</link>
      <guid>https://forem.com/dagnelies/ui-api-db-pushing-three-tier-architecture-too-far-47oa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;What may look ideal in theory, may turn out cumbersome in practice.&lt;/p&gt;

&lt;p&gt;-- Myself&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;During inception, the &lt;a href="https://passwordless.id"&gt;Passwordless.ID&lt;/a&gt; "app" was built in the purest form of a three-tier architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The UI - A vue app "compiled" into a single-page-application&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The API - An API built with Cloudflare Workers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DB - A distributed DB as a service&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In particular, each "tier" was completely independent, built with its own tech stack and deployed on a dedicated subdomain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The UI: &lt;a href="https://ui.passwordless.id"&gt;https://ui.passwordless.id&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The API: &lt;a href="https://api.passwordless.id"&gt;https://api.passwordless.id&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DB: internal network&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This complete separation of "tiers" may look ideal. It seems to be full of advantages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;There is a clear separation of concerns&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each tier can be updated independently&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One could make changes to the UI without affecting the API and vice-versa.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They can scale independently&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The UI is fully browser cached&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each "tier" (UI, API, DB) could theoretically be swapped out with another tech in the long term...&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This might seem like an exemplary separation of concerns, something ideal to strive for. It's also what we strived for during inception. Sounds great right? Well, it turns out it's not that great ...it's actually pretty bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupling back-end and front-end sucks
&lt;/h2&gt;

&lt;p&gt;Being able to update and make changes to UI and API independently sounds great, but in practice, it turns out it's very rarely needed.&lt;/p&gt;

&lt;p&gt;The vast majority of the time, when you work on something, whether it is a new feature, a change or a bug, you typically update mostly the UI and API hand in hand. You change files in both, test both together, and deploy both.&lt;/p&gt;

&lt;p&gt;In the daily routine, having two repositories, toolchains and domains to deploy to turns out to be counter-productive. It'll lead to two commits, two builds, a "joint deploy", etc. It's not that big of a deal either, it's just annoying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-origin requests suck
&lt;/h2&gt;

&lt;p&gt;Due to the UI and API being on two distinct subdomains, requests between the UI and the API are now "cross-origin requests". It applies to different subdomains too. Configuring the API to allow such "cross-origin requests" is no big deal when you know how it works, but some developers may find it cumbersome.&lt;/p&gt;

&lt;p&gt;The subtle disadvantage is that it introduces one more round-trip: the &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request"&gt;Preflight requests&lt;/a&gt;. These requests are automatically sent by the browser to check if the requests from UI to API are allowed, before sending the actual requests. While not dramatic, it makes the UI slightly less responsive since it doubles the first request's latency.&lt;/p&gt;

&lt;p&gt;Lastly, it also has an impact on session handling and security aspects due to its cross-origin nature. However, that's a whole other topic itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency sucks
&lt;/h2&gt;

&lt;p&gt;You probably know it, when you develop locally, everything is snappy. The page loads instantly and you are happy ...and once it's deployed, you notice that the experience on your phone in "real life" is not that great, especially before all the browser caching kicks in.&lt;/p&gt;

&lt;p&gt;The slowness comes from various things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the loading of assets from the SPA&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the UI pre-flight requests to the API&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the UI actual requests to the API&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the API calls to the distributed DB&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the "rendering" of the page content&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these things makes the UI sluggish and is a consequence of the distinct tiers being fully separated. To be more precise, it is because network calls are involved between all parts, each one increasing the latency and sluggishness by a notch.&lt;/p&gt;

&lt;p&gt;Moreover, you typically don't notice this "sneaky" behaviour during the initial phase of development. When testing locally, everything appears lightning fast since everything occurs locally without network latency. Often, the sluggishness introduced by the network calls is only discovered when the first prototype is deployed and going "live".&lt;/p&gt;

&lt;p&gt;That is why an application which combines everything locally, or in a same subnet, is usually much more responsive than having distinct "tiers" like UI / API / DB each separated by a network, which is common in a SaaS world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinct subdomains suck
&lt;/h2&gt;

&lt;p&gt;Whether you want to show your users a feature preview, provide a developer sandbox, make A/B testing or reproduce some bug in real-life conditions, "staging" environments are always useful.&lt;/p&gt;

&lt;p&gt;If both the UI and API are packaged in the same app, deploying it at a single domain like &lt;em&gt;&lt;a href="https://prod.passwordless.id"&gt;https://prod.passwordless.id&lt;/a&gt;&lt;/em&gt; would be straightforward. Then, you could also work on a feature branch and deploy it to &lt;em&gt;&lt;a href="https://new-feature.passwordless.id"&gt;https://new-feature.passwordless.id&lt;/a&gt;&lt;/em&gt; to test it out in a live environment.&lt;/p&gt;

&lt;p&gt;However, this becomes much more complex if you have it split. It would become something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;a href="https://ui.new-feature.passwordless.id"&gt;https://ui.new-feature.passwordless.id&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;a href="https://api.new-feature.passwordless.id"&gt;https://api.new-feature.passwordless.id&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also requires some plumbing, so that the new feature UI talks with the corresponding API URLs, including adjusting CORS properly. This is extra work and is error-prone.&lt;/p&gt;

&lt;p&gt;If the UI and API were bundled together in the same (sub)domain, that would not be a problem since relative URLs could simply be used and CORS are not involved either.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPAs (sometimes) suck
&lt;/h2&gt;

&lt;p&gt;SPAs like Vue, React or Angular are not bad. You have plenty of libraries with all kinds of widgets and fancy stuff. You can just "magically" quickly generate whole apps with some initializer. But it has a cost too: the learning curve, the complex toolchain, the clunky dependencies ...and the initial page load time due to larger size and rendering delays.&lt;/p&gt;

&lt;p&gt;It's a tradeoff. While SPAs typically have a longer initial loading time, they offer complex widgets and increased interactivity in return. It offers ways to structure complex web applications in a modular way to keep their complexity under control. All these things are great ...if you need them. Otherwise, when you just need a few basic pages, it would likely turn into useless overhead.&lt;/p&gt;

&lt;p&gt;In the end, whether SPAs "make sense" totally depends on the app. The more complex and user interaction-heavy the app is, the better suited it will be for SPAs. However, in the case for Passwordless.ID, which has a relatively simple UI, it was counter-productive.&lt;/p&gt;

&lt;p&gt;Doing it as a Vue SPA was great to get started quickly, but in the meantime, it hinders me more than benefits me. The UI library used was bug-ridden, the various toolchains between UI, API and deployment platform do not always play well together, the resulting bundle is 400kb big and it'd cost time and effort to reduce it and the bad resulting latency is the nail in the coffin. Good ol' HTML ain't that bad after all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the basics
&lt;/h2&gt;

&lt;p&gt;Lately, there is a renaissance of good ol' server-side templating. The basics are making a comeback under two umbrella names: SSR (Server-side rendering) and SSG (Server-side generation). SSG means templating at build time, like "generating" pages in various languages, while SSR means templating with dynamic data, like showing the result of a database query. Both have their roles and are complementary.&lt;/p&gt;

&lt;p&gt;It's just going back to the forgotten roots of the web, noticing that after all, it's quite handy to produce HTML with the right data inside directly. It's simple, fast and "slim". This is a contrast to the SPAs which typically inflate, requiring larger JS assets, fetch the data in a second step and add rendering delays.&lt;/p&gt;

&lt;p&gt;Sadly, the ecosystem is very fragmented in this area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Porting software sucks
&lt;/h2&gt;

&lt;p&gt;It would be foolish to leave the "architecture" decision purely to theoretical arguments. Moreover, it usually involves switching or at least adapting the technology stack. As such, its ecosystem plays a crucial role. If you go against the "intended usage" of your tech stack, you may fight an uphill battle not worth it.&lt;/p&gt;

&lt;p&gt;In particular, at the time of Passwordless.ID's inception in mid-2022, Cloudflare pages &lt;em&gt;functions&lt;/em&gt; simply did not exist yet. As such, the option to package both (with Cloudflare) was not even possible at that point. Pages &lt;em&gt;functions&lt;/em&gt; appeared later, by the end of 2022. It certainly would have been able to use a more traditional technology stack at that time. However, the deployment and scalability comfort from a Cloudflare Pages/Workers combo was what pulled us over. It was a pragmatic choice rather than the ideal tech stack.&lt;/p&gt;

&lt;p&gt;The point is that it would &lt;strong&gt;&lt;em&gt;now&lt;/em&gt;&lt;/strong&gt; be possible to combine the back-end and front-end in a single codebase. Is it worth porting the existing codebase? Tough question. I'd say "probably" but it's a substantial effort. The main issue is that in our case, the whole ecosystem around Cloudflare pages &lt;em&gt;functions&lt;/em&gt; is very young. It lacks tooling, libraries, Q&amp;amp;A, documentation and so on. It is a "bleeding edge" right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's make it suck less
&lt;/h2&gt;

&lt;p&gt;Do you know what also sucks? Authentication. It sucks for users (because they create oh so many accounts), it sucks for developers (because it's so complex) and it sucks for security (because passwords are vulnerable).&lt;/p&gt;

&lt;p&gt;So at least, let's try to make authentication suck less and use &lt;a href="https://passwordless.id"&gt;Passwordless.ID&lt;/a&gt;. Think of it as a free universal account, a free public service, that makes your developer's life easy, is comfortable for the users and is more secure.&lt;/p&gt;

&lt;p&gt;In the meantime, we'll start porting the "tiered" app to a "bundled" app, making it even better, swifter and more lightweight. Thanks for reading and stay tuned!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>devops</category>
    </item>
    <item>
      <title>Randomly Generated Avatars</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Wed, 19 Jul 2023 08:10:24 +0000</pubDate>
      <link>https://forem.com/dagnelies/randomly-generated-avatars-18n5</link>
      <guid>https://forem.com/dagnelies/randomly-generated-avatars-18n5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;As a follow-up from an earlier &lt;a href="https://blog.passwordless.id/replacing-avatar-portraits"&gt;article&lt;/a&gt; regarding the update to randomly generated default avatars for &lt;a href="http://Passwordless.ID"&gt;Passwordless.ID&lt;/a&gt;, I wanted to post a "how to". This is a beginner tutorial since making such avatars is actually really simple.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR; Here is the full demo.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/dagnelies/embed/rNQdZvM?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  The image format
&lt;/h2&gt;

&lt;p&gt;The first thing you should think about is the image format, usually, one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Jpeg&lt;/strong&gt;: great for real user photos due to the high compression ratio. However, this compression also produces some "blur" on lines and sharp edges. As such it is not ideal for the avatars we are going to make.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PNG&lt;/strong&gt;: theses have lossless compression. In other words, every pixel remains exactly the same as it was originally drawn. Edges and lines remain "sharp".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SVG&lt;/strong&gt;: these are scalable vector graphics. Unlike a "raster of pixels", it is a declarative format describing shapes and paths.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Of course, you could also save it as a "100% quality" Jpeg to avoid any quality loss, but then it is larger than PNGs. Jpeg compression is amazing though for common photos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our case, we picked SVG for the upcoming avatar pictures. In the past, SVG was kind of avoided because support was not always well supported for all software platforms. This is however largely in the past.&lt;/p&gt;

&lt;p&gt;SVG offers several benefits: the first is being scalable. Due to its vector nature, it is perfectly sharp at any scale, even if you zoom in on a 4K display. Other "raster" formats like Jpeg or PNG become "pixelated" when zooming in. The other is being more compact. While the byte size of Jpeg/PNG grows with picture size, SVG grows proportional to the shape's complexity. For relatively simple stuff like the avatars here, they are super compact.&lt;/p&gt;
&lt;h2&gt;
  
  
  The SVG "template"
&lt;/h2&gt;

&lt;p&gt;SVG is an XML format that describes the shapes. As such, what will be generated is a big XML string. To be more exact, we will fill the template below with the appropriate values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"gradient"&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"${startX}"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"${startY}"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"${endX}"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"${endY}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"10%"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"hsl(${startHue}, 100%, 50%)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;offset=&lt;/span&gt;&lt;span class="s"&gt;"90%"&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"hsl(${endHue}, 100%, 50%)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#gradient)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"55"&lt;/span&gt; &lt;span class="na"&gt;text-anchor=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt; &lt;span class="na"&gt;dominant-baseline=&lt;/span&gt;&lt;span class="s"&gt;"middle"&lt;/span&gt; &lt;span class="na"&gt;font-size=&lt;/span&gt;&lt;span class="s"&gt;"75"&lt;/span&gt; &lt;span class="na"&gt;font-family=&lt;/span&gt;&lt;span class="s"&gt;"Times New Roman"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#ffffff"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;${char}&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this template is filled with meaningful values, you will obtain an avatar SVG image that can be stored as a plain normal ".svg" file.&lt;/p&gt;

&lt;p&gt;Alternatively, you also deliver it as "data URL" since it is quite compact. This simply means encoding the resource directly instead of a "plain URL" fetching it. It is composed of two parts: the mime-type (&lt;code&gt;image/svg+xml&lt;/code&gt; in this case) and the (base64 encoded) data.&lt;/p&gt;

&lt;p&gt;This can be used like any other URL in the &lt;code&gt;src&lt;/code&gt; tag of an image as follows: &lt;code&gt;&amp;lt;img src=&lt;/code&gt; &lt;code&gt;"data: image/svg+xml;base64,{{the-base64-encoded-svg}}"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Voilà, you got your image!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting some random values
&lt;/h2&gt;

&lt;p&gt;The missing step is now filling this SVG template with some random values. Alternatively, if you want something more deterministic, you could also use the hash value of the name for example.&lt;/p&gt;

&lt;p&gt;As you saw in the SVG template, instead of using RGB colors, &lt;a href="https://www.w3schools.com/colors/colors_hsl.asp"&gt;HSL colors&lt;/a&gt; were used. This stands for Hue-Saturation-Lightness. This makes it easy to generate bright colors from all rainbow colors, with maximal color saturation and average "lightness".&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;// Gradient colors&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startHue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&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;endHue&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Gradient direction&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;

  &lt;span class="c1"&gt;// Calculate the start and end points of the gradient&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startX&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;endY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// The character to appear on the avatar&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charAt&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;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the gradient direction, it's a bit more tricky since an angle cannot be provided directly. There are some "transforms" available, but to ensure the widest compatibility with SVG renderers, sticking to the basics seems a safe bet. As such, the angle is converted to starting and ending coordinates for the gradient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you
&lt;/h2&gt;

&lt;p&gt;The resulting full source code can be seen in the example provided at the beginning. :)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://codepen.io/dagnelies/pen/rNQdZvM"&gt;A Pen by Arnaud Dagnelies (&lt;/a&gt;&lt;a href="http://codepen.io"&gt;codepen.io&lt;/a&gt;&lt;a href="https://codepen.io/dagnelies/pen/rNQdZvM"&gt;)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>svg</category>
    </item>
    <item>
      <title>Extracting addresses from OpenStreetMaps</title>
      <dc:creator>Arnaud Dagnelies</dc:creator>
      <pubDate>Fri, 19 May 2023 08:19:52 +0000</pubDate>
      <link>https://forem.com/dagnelies/extracting-addresses-from-openstreetmaps-5g2i</link>
      <guid>https://forem.com/dagnelies/extracting-addresses-from-openstreetmaps-5g2i</guid>
      <description>&lt;h1&gt;
  
  
  Why?
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Because there is no worldwide quality source for addresses!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Really, that's no joke. There are many commercial providers for "industrialized countries" of variable quality/pricing but worldwide coverage is lacking, the data formats are diverse and the license terms provider specific.&lt;/p&gt;

&lt;p&gt;There are also some open source projects related to addresses though, each with their gochas. Two of these projects are mentioned in the last section "Honorable Mentions" at the end of this article, along with their drawbacks.&lt;/p&gt;

&lt;p&gt;This project is born in order to provide quality addresses with worldwide coverage under an open license, by directly extracting addresses from the raw data dumps of &lt;a href="https://openstreetmap.org"&gt;OpenStreetMap&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Birth of &lt;a href="https://openstreetdata.org"&gt;OpenStreetData.org&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;How does the it look like? Here is a screenshot, but if you prefer, check out &lt;a href="https://openstreetdata.org"&gt;the website&lt;/a&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DYtEZuNi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tb78xn8teds3r89i3oh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DYtEZuNi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tb78xn8teds3r89i3oh.png" alt="Image description" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is divided into two parts: extracts and addresses. Another "points of interest" was planned, but not further developed due to lack of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Country dumps
&lt;/h2&gt;

&lt;p&gt;Country extracts are provided in two formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://wiki.openstreetmap.org/wiki/PBF_Format"&gt;PBF&lt;/a&gt;, the native OpenStreetMap binary format. This format is very compact and many tools can handle it efficiently. Nevertheless, it is not always very practical to handle due to its low-level nature. It's basically a huge list of points with IDs, lines that reference these IDs and relations that reference the lines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/GeoJSON"&gt;GeoJson&lt;/a&gt; sequences. It's a text file where each line is a "feature": a JSON object with arbitrary properties and a geometry with coordinates. Although the file size is typically larger and the processing sometimes slower, it offers other benefits. The JSON format is universal, the line-based sequence makes it straightforward to filter it with grep-like tools and the geometry can be parsed directly without requiring to go through the whole file.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note though that both formats are not 100% equivalent. During the conversion process, some choices were required to be made. In particular, in the original PBF a "closed line" (where the last point is the same as the first) could be interpreted as a line or as an area either way. There is no clear-cut indication whether it's a "line" happening to turn in a circle, like a roundabout, or a polygonal "area", like a building outline. This led to "closed lines" being interpreted as lines or polygons based on a lot of hand-picked feature properties. For example, if &lt;code&gt;building=...&lt;/code&gt; was part of the properties, it was considered a polygon, except if an &lt;code&gt;area=false&lt;/code&gt; tag was also present, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative areas
&lt;/h2&gt;

&lt;p&gt;Despite not being shown on the site, extracting precise boundaries of a country's provinces, regions, counties, cities, suburbs and so on was the first crucial step. How a country is subdivided into smaller areas varies greatly from country to country and is abstracted under the name "administrative areas" of various levels.&lt;/p&gt;

&lt;p&gt;This step is crucial because of the way addresses are extracted. Streets and houses were extracted using "spatial joins" with the administrative areas. Their coordinates were used to determine which administrative areas (city, county, province...) they belong to, as well as the postal code ...if postal code areas are defined in the country.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Currently, the reason of missing (or wrong) addresses for some countries are improper &lt;a href="https://github.com/dagnelies/open-street-data/blob/main/admin_level_mapping.tsv"&gt;mapping of the administrative areas&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Streets
&lt;/h3&gt;

&lt;p&gt;Here is an example of the "streets" for Austria:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;suburb&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;country&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;state&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;province&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;city&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;postal_code&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;street_name&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Oberösterreich&lt;/td&gt;
&lt;td&gt;Bezirk Vöcklabruck&lt;/td&gt;
&lt;td&gt;Attersee am Attersee&lt;/td&gt;
&lt;td&gt;4864&lt;/td&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Oberösterreich&lt;/td&gt;
&lt;td&gt;Bezirk Vöcklabruck&lt;/td&gt;
&lt;td&gt;Attersee am Attersee&lt;/td&gt;
&lt;td&gt;4864&lt;/td&gt;
&lt;td&gt;Altenberg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Oberösterreich&lt;/td&gt;
&lt;td&gt;Bezirk Vöcklabruck&lt;/td&gt;
&lt;td&gt;Attersee am Attersee&lt;/td&gt;
&lt;td&gt;4864&lt;/td&gt;
&lt;td&gt;Attergauer Landesstraße&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Oberösterreich&lt;/td&gt;
&lt;td&gt;Bezirk Vöcklabruck&lt;/td&gt;
&lt;td&gt;Attersee am Attersee&lt;/td&gt;
&lt;td&gt;4864&lt;/td&gt;
&lt;td&gt;Attersee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Abtsdorf&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Oberösterreich&lt;/td&gt;
&lt;td&gt;Bezirk Vöcklabruck&lt;/td&gt;
&lt;td&gt;Attersee am Attersee&lt;/td&gt;
&lt;td&gt;4864&lt;/td&gt;
&lt;td&gt;Atterseestraße&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Vorarlberg&lt;/td&gt;
&lt;td&gt;Bezirk Feldkirch&lt;/td&gt;
&lt;td&gt;Marktgemeinde Rankweil&lt;/td&gt;
&lt;td&gt;6830&lt;/td&gt;
&lt;td&gt;Wüstenrotgasse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Vorarlberg&lt;/td&gt;
&lt;td&gt;Bezirk Feldkirch&lt;/td&gt;
&lt;td&gt;Marktgemeinde Rankweil&lt;/td&gt;
&lt;td&gt;6830&lt;/td&gt;
&lt;td&gt;Zehentstraße&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Vorarlberg&lt;/td&gt;
&lt;td&gt;Bezirk Feldkirch&lt;/td&gt;
&lt;td&gt;Marktgemeinde Rankweil&lt;/td&gt;
&lt;td&gt;6830&lt;/td&gt;
&lt;td&gt;Zieglerweg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Vorarlberg&lt;/td&gt;
&lt;td&gt;Bezirk Feldkirch&lt;/td&gt;
&lt;td&gt;Marktgemeinde Rankweil&lt;/td&gt;
&lt;td&gt;6830&lt;/td&gt;
&lt;td&gt;Zunftgasse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;AT&lt;/td&gt;
&lt;td&gt;Vorarlberg&lt;/td&gt;
&lt;td&gt;Bezirk Feldkirch&lt;/td&gt;
&lt;td&gt;Marktgemeinde Rankweil&lt;/td&gt;
&lt;td&gt;6830&lt;/td&gt;
&lt;td&gt;Übersaxner Straße&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;168769 rows × 7 columns&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It extracted all streets having a name from the raw data and determined the administrative areas and postal code it belongs according to their centroid. As such, it is a slightly simplified streets list. If a street might cross multiple cities or postal codes for example, it will solely be listed in the "main one" (according to its center). For more precise addresses, see below.&lt;/p&gt;

&lt;p&gt;Note that "suburb" may be empty depending on the size of the city. This is normal since not all cities are further divided into suburbs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Houses
&lt;/h3&gt;

&lt;p&gt;Houses is a dataset listing each house (anything with a house number) individually, including its coordinates and the administrative areas it lies within.&lt;/p&gt;

&lt;h3&gt;
  
  
  Addresses
&lt;/h3&gt;

&lt;p&gt;In this case, the houses are "merged" into streets with house numbers. Unlike the "streets" approach, it results in a more fine-grained dataset.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;it includes only streets with at least a single house (number)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it differentiates between street sections with house number ranges belonging to different administrative areas or postal codes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it differentiates between different sides of the street (with odd/even house numbers) belonging to different administrative areas or postal codes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it has boundaries&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example of such an address file for Austria.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;postal_code&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;city&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;street&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;x_min&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;x_max&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;y_min&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;y_max&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;house_min&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;house_max&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;house_odd&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;house_even&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;Vienna&lt;/td&gt;
&lt;td&gt;Weihburggasse&lt;/td&gt;
&lt;td&gt;16.375769&lt;/td&gt;
&lt;td&gt;16.375769&lt;/td&gt;
&lt;td&gt;48.205242&lt;/td&gt;
&lt;td&gt;48.205242&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;Wien&lt;/td&gt;
&lt;td&gt;Abraham-a-Sancta-Clara-Gasse&lt;/td&gt;
&lt;td&gt;16.362970&lt;/td&gt;
&lt;td&gt;16.363213&lt;/td&gt;
&lt;td&gt;48.209789&lt;/td&gt;
&lt;td&gt;48.209910&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;Wien&lt;/td&gt;
&lt;td&gt;Akademiestraße&lt;/td&gt;
&lt;td&gt;16.370855&lt;/td&gt;
&lt;td&gt;16.372425&lt;/td&gt;
&lt;td&gt;48.200877&lt;/td&gt;
&lt;td&gt;48.203575&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;Wien&lt;/td&gt;
&lt;td&gt;Albertinaplatz&lt;/td&gt;
&lt;td&gt;16.368138&lt;/td&gt;
&lt;td&gt;16.369344&lt;/td&gt;
&lt;td&gt;48.204084&lt;/td&gt;
&lt;td&gt;48.204750&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;Wien&lt;/td&gt;
&lt;td&gt;Alte Walfischgasse&lt;/td&gt;
&lt;td&gt;16.371740&lt;/td&gt;
&lt;td&gt;16.371740&lt;/td&gt;
&lt;td&gt;48.203559&lt;/td&gt;
&lt;td&gt;48.203559&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;...&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;147137&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9991&lt;/td&gt;
&lt;td&gt;Gemeinde Dölsach&lt;/td&gt;
&lt;td&gt;Waidachweg&lt;/td&gt;
&lt;td&gt;12.825955&lt;/td&gt;
&lt;td&gt;12.827117&lt;/td&gt;
&lt;td&gt;46.830659&lt;/td&gt;
&lt;td&gt;46.831055&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;147138&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9991&lt;/td&gt;
&lt;td&gt;Gemeinde Dölsach&lt;/td&gt;
&lt;td&gt;Wenzl PLatz&lt;/td&gt;
&lt;td&gt;12.841072&lt;/td&gt;
&lt;td&gt;12.841634&lt;/td&gt;
&lt;td&gt;46.826521&lt;/td&gt;
&lt;td&gt;46.826902&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;147139&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9992&lt;/td&gt;
&lt;td&gt;Gemeinde Iselsberg-Stronach&lt;/td&gt;
&lt;td&gt;Großglockner Straße&lt;/td&gt;
&lt;td&gt;12.841043&lt;/td&gt;
&lt;td&gt;12.858008&lt;/td&gt;
&lt;td&gt;46.833271&lt;/td&gt;
&lt;td&gt;46.854501&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;206&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;147140&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9992&lt;/td&gt;
&lt;td&gt;Gemeinde Iselsberg-Stronach&lt;/td&gt;
&lt;td&gt;Iselsberg&lt;/td&gt;
&lt;td&gt;12.835091&lt;/td&gt;
&lt;td&gt;12.855994&lt;/td&gt;
&lt;td&gt;46.833822&lt;/td&gt;
&lt;td&gt;46.846260&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;212&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;147141&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9992&lt;/td&gt;
&lt;td&gt;Gemeinde Iselsberg-Stronach&lt;/td&gt;
&lt;td&gt;Stronach&lt;/td&gt;
&lt;td&gt;12.849133&lt;/td&gt;
&lt;td&gt;12.858230&lt;/td&gt;
&lt;td&gt;46.826562&lt;/td&gt;
&lt;td&gt;46.833270&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;td&gt;True&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;146322 rows × 11 columns&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It may not be perfect, for example, the first line with a misinterpreted city name is quite mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "Big Data"
&lt;/h3&gt;

&lt;p&gt;Dealing with large data is challenging. It's not thousands of points, it's not millions, it's many billions of points, lines, polygons and relations.&lt;/p&gt;

&lt;p&gt;Seems like a detail? Well, for example, you cannot even load the planet's data at once in memory. It's simply too big.&lt;/p&gt;

&lt;p&gt;You cannot just "do as you please" with inefficient code. Every line of code, every operation, must be crafted with care, well thought out, and fine-tuned to keep processing time and memory to a minimum.&lt;/p&gt;

&lt;p&gt;As an example, just for processing the data of a single country, even 32Gb RAM is not enough for larger countries and it takes many hours with the current code, despite best efforts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Producing precise country extracts
&lt;/h3&gt;

&lt;p&gt;There are sites like &lt;a href="http://geofabrik.de"&gt;geofabrik.de&lt;/a&gt; providing country extracts to download. However, they turned out to be not precise enough for me. They use "simplified country border polygons" that are "cutting corners" and therefore missing addresses in areas near the borders. So I had to "split the planet" myself.&lt;/p&gt;

&lt;p&gt;To do so, the first step was to extract &lt;em&gt;exact&lt;/em&gt; country boundaries. Interestingly, these might change over time. Usually, it's minor modifications like slightly adjusting the border or correcting mistakes. But sometimes the border might move a bit more in "unstable" parts of the world. The point here is that these borders are not "definitive" but evolve slightly over time.&lt;/p&gt;

&lt;p&gt;The next step is splitting the world into country extracts. Here again, it cannot be naively done in a single step. Doing so, even 256Gb RAM would not suffice to split at once. So the splitting must be done in multiple steps: first in continents, then in regions, then in countries so that it fits in a "reasonable" amount of memory.&lt;/p&gt;

&lt;p&gt;And cutting whole continents with a super precise boundary constituted from millions of points is not efficient either. On the other hand, computing the total bounds of the continent is pointless too. For example, the outer bounds of just France would cover almost the whole world since it possesses many islands around the world as part of its territory. You get the point, some extra work must be done to simplify the geometry without losing stuff but without including too much either.&lt;/p&gt;

&lt;p&gt;Then, there are ways or area relations that cross boundaries. Some things from the raw data are not always clear whether it's a "closed line" or an "area", and so on. It's full of technical details which make even producing what look like simple "country extracts" challenging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Heterogenous data
&lt;/h3&gt;

&lt;p&gt;The OpenStreetMap &lt;a href="https://wiki.openstreetmap.org/wiki/Planet.osm"&gt;raw data&lt;/a&gt; is not a homogenous clearly defined dataset. It is a huge amount of points, lines and relations, each with completely arbitrary properties. For example, a statue might be a point with metadata indicating when it was built, and from whom, along with some tourist guide number. Depending on where you look on the map, you may also notice different habits of mappers using a diversified arsenal of "&lt;a href="https://wiki.openstreetmap.org/wiki/Tags"&gt;tags&lt;/a&gt;" to describe things and the community as a whole has different opinions on how to do things, for example with &lt;a href="https://wiki.openstreetmap.org/wiki/Addresses"&gt;addresses&lt;/a&gt;, which often have local flavours.&lt;/p&gt;

&lt;p&gt;If you dig into the raw data of OpenStreetMap, you will find interesting things. For example, you will find tags like &lt;code&gt;addr:street=...&lt;/code&gt; and &lt;code&gt;addr:city=...&lt;/code&gt; which sounds promising. These are also very simple (and quick) to extract since it's attached directly to the data. Great right? Well, it would be this data was complete but it is far from. Depending on the country you are looking at, it might be mostly widespread or barely used. Even if it's there, the coverage and the content are usually quite fuzzy. For example, the street might be named "Wall Street" on one building while another building uses "Wall St.". Likewise, the city in one building may be "N.Y. City" while another uses "New York". Postcodes may also be written in individual houses, but not match the postal boundary accurately, etc. This makes processing these tags directly error-prone. It's better than nothing but there are ways to make it better.&lt;/p&gt;

&lt;p&gt;Namely what we did. Spatial joins of houses/streets points into administrative/postal areas in order to extract the most information possible. If those areas are not mapped, a fallback to the tags is used, but only as "fallback" since they are usually not that precise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual labor
&lt;/h3&gt;

&lt;p&gt;Doing this is quite some work. It's not just running a process and be done with it. It's craftsmanship where you change a few lines of code and manually inspect the results. Just checking if more streets/houses/addresses are produced is not sufficient either. It might be that the output is of worse quality because street names are duplicated or listed in the wrong "areas" or some other data mistakes. It might also be that the "couple of lines change" works perfectly for one country but breaks in another because of local differences, like for example the presence or absence of postal codes.&lt;/p&gt;

&lt;p&gt;Sometimes, you also see odd things in the data. When this happens you usually spend some time to investigate "why" it is so. Is it the raw map data that is strange? Is it some situation you did not think of? Is there a bug in the code? Is some third-party library not working as expected?... It's really full of weird things, from buildings on the map having mistakenly used "national boundaries" tags to sudden performance drops in third-party libraries when calling a certain function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Addresses are crazy
&lt;/h2&gt;

&lt;p&gt;Below, I will illustrate how addresses are crazy. It's not something that is homogenous worldwide. It's full of regional quirks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is it a country or not?
&lt;/h3&gt;

&lt;p&gt;You may think that something as basic as countries and boundaries is clear-cut. But it is not. Take Kosovo for example.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--A1rlbfsU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Europe-Republic_of_Kosovo.svg/250px-Europe-Republic_of_Kosovo.svg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A1rlbfsU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/Europe-Republic_of_Kosovo.svg/250px-Europe-Republic_of_Kosovo.svg.png" alt="Location in Europe" width="250" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For half of the world (marked in red), Kosovo constitutes a province of Serbia, while for the other half of the world (marked in blue) Kosovo is recognized as an independent country.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hzPz7wPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/2/23/Kosovo_relations.svg/400px-Kosovo_relations.svg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hzPz7wPa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/2/23/Kosovo_relations.svg/400px-Kosovo_relations.svg.png" alt="" width="400" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;...and that's not unique to Kosovo. There are plenty of regions in the world where territory is disputed, where border shift with local wars and where sovereignty depends on who you ask.&lt;/p&gt;

&lt;p&gt;What stance do I take here? I simply use the list of countries as defined by the united nations, defined by their country codes &lt;a href="https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2"&gt;ISO 3166-1&lt;/a&gt;. It might not be ideal, but it is pragmatic.&lt;/p&gt;

&lt;h3&gt;
  
  
  A city with many borders
&lt;/h3&gt;

&lt;p&gt;On a small scale level, borders can be crazy too. For example, check out the little town of &lt;a href="https://en.wikipedia.org/wiki/Baarle-Nassau"&gt;Baarle-Nassau&lt;/a&gt;, located in the south of the Netherlands, near the Belgium border.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KXAmv5Nb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Baarle-Nassau_-_Baarle-Hertog-en.svg/365px-Baarle-Nassau_-_Baarle-Hertog-en.svg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KXAmv5Nb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/3/3c/Baarle-Nassau_-_Baarle-Hertog-en.svg/365px-Baarle-Nassau_-_Baarle-Hertog-en.svg.png" alt="" width="365" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This town contains 22 small &lt;a href="https://en.wikipedia.org/wiki/Exclave"&gt;exclaves&lt;/a&gt; of the Belgian town &lt;a href="https://en.wikipedia.org/wiki/Baarle-Hertog"&gt;Baarle-Hertog&lt;/a&gt;, some of which contain counter-exclaves of Nassau. The borders cross streets in the middle, sometimes multiple times and a single house might have a Belgium address on one side and a Netherlands address on the other. As you see, extracting addresses can quickly become challenging. ;)&lt;/p&gt;

&lt;h3&gt;
  
  
  A city center without street names
&lt;/h3&gt;

&lt;p&gt;Not all addresses are based on "streets". Take a look a &lt;a href="https://en.wikipedia.org/wiki/Mannheim"&gt;Mannheim&lt;/a&gt; for example. There, the city center is divided like a &lt;a href="https://en.wikipedia.org/wiki/Mannheim#Block_numbering_and_computer_mapping"&gt;big grid&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bclfedjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/2/2d/Mannheim_Quadratstadt_beschriftet.png/708px-Mannheim_Quadratstadt_beschriftet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bclfedjf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://upload.wikimedia.org/wikipedia/commons/thumb/2/2d/Mannheim_Quadratstadt_beschriftet.png/708px-Mannheim_Quadratstadt_beschriftet.png" alt="File:Mannheim Quadratstadt beschriftet.png" width="708" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There, each "block" has an identifier, like "C3" while the streets are unnamed. Likewise, house number does not belong to a "street" but to a block. In other words, your address might be "C3, 17" if you live in building 17 of block "C3".&lt;/p&gt;

&lt;h3&gt;
  
  
  Fancy house numbers
&lt;/h3&gt;

&lt;p&gt;Do you want to use regexes to filter valid house numbers? Well, that might not really work out. For example, the following image is a valid Vietnamese house number, near the coasts of Ho Chi Minh city.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qj5aW1iU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://qph.cf2.quoracdn.net/main-qimg-28c36359aa26de2b8c2d68a1b52de29d-pjlq" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qj5aW1iU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://qph.cf2.quoracdn.net/main-qimg-28c36359aa26de2b8c2d68a1b52de29d-pjlq" alt="" width="600" height="375"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;The world is full of surprises. Also regarding addresses, it's full of diversity and local quirks and I believe there is nothing that does not exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Honorable mentions
&lt;/h2&gt;

&lt;p&gt;There are also two noticeable open source projects trying to bring addresses to the public domain.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://openaddresses.io"&gt;&lt;strong&gt;openaddresses.io&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is probably the most famous one. It works by running various "scraping scripts" against various "raw data sources". The result of this approach has few drawbacks though, directly related to its approach.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the licensing is problematic. Basically, it says "use this data according to the license from the data source" ...which is not obvious, since the original issue is not directly linked, often in native language and the licensing terms makes the usage of this data questionable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the coverage is lacking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the scraping sometimes breaks or is outdated because of changes in the raw source&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;despite being "open", some things are obfuscated and make reproducing or direct downloads difficult&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6L01cPri--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9s76uc80d9e9rdylfj00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6L01cPri--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9s76uc80d9e9rdylfj00.png" alt="Image description" width="598" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://osmnames.org"&gt;&lt;strong&gt;osmnames.org&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite less known, this is IMHO a better source of addresses. It is based on addresses extraction from OpenStreetMap and therefore has worldwide coverage and a homogenous license: the "ODbL - Open Database License".&lt;/p&gt;

&lt;p&gt;The only drawback it has IMHO is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the lack of postal codes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the &lt;code&gt;admin_level&lt;/code&gt; mapping not ideal&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lack of postal codes may seem like a detail, but it is crucial for addresses. Without it, addresses are simply incomplete. Since this project is open source, I also tried to improve it by adding postal code (see issue) but it turned out too be too difficult/challenging for me. Mostly because I am unfamiliar with PostGIS. The code however, is of quality. This lead me to the current project.&lt;/p&gt;

&lt;p&gt;The second issue is more subtle, and leads to missing addresses in some countries because administrative boundaries are not properly mapped. Also, the code is not suited for experimentation and from my understanding, there were ways to "get more" out of the raw OpenStreetMap data dumps than how they did it.&lt;/p&gt;

</description>
      <category>openstreetmap</category>
      <category>addresses</category>
    </item>
  </channel>
</rss>
