<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tanay Karnik</title>
    <description>The latest articles on DEV Community by Tanay Karnik (@tanay).</description>
    <link>https://dev.to/tanay</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%2F388716%2Fc0d7cead-69bb-4dd9-bb5e-71d42314ea45.jpg</url>
      <title>DEV Community: Tanay Karnik</title>
      <link>https://dev.to/tanay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanay"/>
    <language>en</language>
    <item>
      <title>A closer look at the SpacetimeDB v2 benchmark: Is it really 23x faster than SQLite?</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Sun, 22 Mar 2026 10:43:48 +0000</pubDate>
      <link>https://dev.to/tanay/a-closer-look-at-the-spacetimedb-v2-benchmark-is-it-really-23x-faster-than-sqlite-2l9f</link>
      <guid>https://dev.to/tanay/a-closer-look-at-the-spacetimedb-v2-benchmark-is-it-really-23x-faster-than-sqlite-2l9f</guid>
      <description>&lt;p&gt;A good way to market a new infrastructure tool is to loudly announce that it is a thousand times faster than whatever you are currently using. This is a very effective strategy because nobody wants to be the person using a slower database.&lt;/p&gt;

&lt;p&gt;A few weeks ago, SpacetimeDB launched their v2 with exactly this premise. They released a slick video explaining that traditional web architecture—where your server talks to your database over a network—is fundamentally broken. The solution, they argue, is to just put your application code inside the database.&lt;/p&gt;

&lt;p&gt;And to prove it, they showed a chart. This chart is basically a graveyard of modern infrastructure. They lined up Postgres, PlanetScale, Convex, CockroachDB and a few others, and declared victory over all of them.&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%2Fri6mf38b54qienb7jvdl.jpg" 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%2Fri6mf38b54qienb7jvdl.jpg" alt="SpacetimeDB v2 benchmark chart" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First of all, you have got to love the cheeky little magnifying glass icons they added next to Convex, PlanetScale, and CockroachDB. Just to drive home how microscopic their numbers supposedly are.&lt;/p&gt;

&lt;p&gt;But if you ignore that for a second, something else immediately stands out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;TPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SpacetimeDB (Rust)&lt;/td&gt;
&lt;td&gt;167,915&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpacetimeDB (JS)&lt;/td&gt;
&lt;td&gt;104,485&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js + SQLite&lt;/td&gt;
&lt;td&gt;7,416&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That 7K TPS number for Node.js + SQLite is so clearly, obviously off that I just had to clone their code and try to reproduce the results myself.&lt;/p&gt;

&lt;p&gt;Unlike the other databases on that list, SpacetimeDB and a Node.js + SQLite server share the exact same architectural paradigm: &lt;strong&gt;colocated compute and storage&lt;/strong&gt;. The application and the database live on the exact same machine. There is no network boundary. Which makes the gap between them look suspicious.&lt;/p&gt;

&lt;p&gt;Naturally, a benchmark chart like this &lt;a href="https://www.reddit.com/r/theprimeagen/comments/1rf87kw/debunking_spacetimedbs_performance_claims/" rel="noopener noreferrer"&gt;raised eyebrows&lt;/a&gt;, &lt;a href="https://gist.github.com/brandonpollack23/77d4d741e9af56ef3a9fc338f68d2ec6" rel="noopener noreferrer"&gt;sparked skepticism&lt;/a&gt;, and &lt;a href="https://lobste.rs/s/1igwdg/fastest_database_world_spacetimedb_2_0" rel="noopener noreferrer"&gt;generated plenty of discussion&lt;/a&gt;. Vincent, an ex-PlanetScale engineer, wrote &lt;a href="https://strn.cat/w/articles/spacetime/" rel="noopener noreferrer"&gt;a great technical teardown&lt;/a&gt; shortly after the launch. He raised good questions about high availability, cluster deployments, and the perils of forcing your application and database to fight for the same CPU and RAM. Valid points.&lt;/p&gt;

&lt;p&gt;He also reasonably questioned whether SpacetimeDB was skipping durability—the part where the database actually promises your data is safely written to the hard drive before returning a success message—to hit these astronomical numbers.&lt;/p&gt;

&lt;p&gt;But they aren't. SpacetimeDB has a flag (&lt;code&gt;withConfirmedReads&lt;/code&gt;), which was enabled during the benchmark, that ensures transactions are durably committed—actually flushed to the disk—before the server acknowledges the client.&lt;/p&gt;

&lt;p&gt;So the secret sauce isn't skipping durability; it's something else. That SQLite number was still very wrong, and I needed to find out why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bare metal and broken code
&lt;/h2&gt;

&lt;p&gt;I'm going to run the benchmark on my personal Linux machine. I have a 13th Gen Intel Core i7-13700K, 32GB memory, and a local NVMe SSD.&lt;/p&gt;

&lt;p&gt;Since we are about to talk a lot about database durability and disk bottlenecks, here is a quick &lt;code&gt;dd&lt;/code&gt; test to establish a baseline for this disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fsync_test &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4k &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nv"&gt;oflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dsync
&lt;span class="go"&gt;1000+0 records in
1000+0 records out
4096000 bytes (4.1 MB, 3.9 MiB) copied, 0.683005 s, 6.0 MB/s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is this doing?&lt;/strong&gt;&lt;br&gt;
We are copying small blocks of data one after the other synchronously to the&lt;br&gt;
  disk. Divide the total time taken by the number of blocks copied, and you&lt;br&gt;
  roughly get the write latency of the disk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;1000 blocks copied in 0.683s. That's a write latency of &lt;strong&gt;683 microseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Roughly 5x faster than your usual EBS volume, which makes sense—EBS is network-attached storage with far better durability guarantees than a local NVMe drive. For comparison, here's the same test on EBS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fsync_test &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4k &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nv"&gt;oflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dsync
&lt;span class="go"&gt;1000+0 records in
1000+0 records out
4096000 bytes (4.1 MB, 3.9 MiB) copied, 3.8657 s, 1.1 MB/s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3.86ms per write.&lt;/p&gt;

&lt;p&gt;Still, my local SSD is relatively slow. If you use a good bare-metal server (like the AWS EC2 i7i series), you could get write latencies down to &lt;strong&gt;76 microseconds&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/mnt/nvme-data/fsync_test &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4k &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nv"&gt;oflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dsync
&lt;span class="go"&gt;1000+0 records in
1000+0 records out
4096000 bytes (4.1 MB, 3.9 MiB) copied, 0.0760506 s, 53.9 MB/s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is write latency so important? More on that soon.&lt;/p&gt;

&lt;p&gt;We can now clone their repository, install all the dependencies, set the env variables, run the &lt;code&gt;prep&lt;/code&gt; script, and reproduce the results, right?&lt;/p&gt;

&lt;p&gt;Wrong. The benchmark—especially for their own SpacetimeDB test—was broken out of the box. I &lt;a href="https://x.com/tanayvk/status/2031775069593276874" rel="noopener noreferrer"&gt;ranted about it on X&lt;/a&gt;, and their founder Tyler invited me to their Discord to help get things running. Their engineering team was polite and responsive. They acknowledged a few bugs, some old code that broke during their v2 migration, and &lt;a href="https://github.com/clockworklabs/SpacetimeDB/commit/90b9e06ed23e920a60317be8a246587398ce30bf" rel="noopener noreferrer"&gt;shipped a patch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Except, the benchmark was &lt;em&gt;still&lt;/em&gt; broken.&lt;/p&gt;

&lt;p&gt;So, I'm only going to focus on the SQLite numbers. Once SpacetimeDB fixes their benchmarking code, I'll update the results here. I have no doubt that 100K–160K TPS is possible with their architecture. But as we'll see, it's possible for SQLite, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial results and more broken code
&lt;/h2&gt;

&lt;p&gt;Running the benchmark is easy. First, we run the Node.js + SQLite RPC server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm tsx src/rpc-servers/sqlite-rpc-server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we run the benchmark client with the default settings SpacetimeDB used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_rpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benchmark runs for 10 seconds. Concurrency of 50 means we'll start 50 simultaneous connections (workers) to our RPC server. There's also a hidden &lt;code&gt;maxInflightPerWorker&lt;/code&gt; parameter, which is the number of open, unresolved requests a worker can hold at a time. We aren't passing it explicitly just yet, but keep it in mind—it’s going to become relevant soon.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on alpha=1.5&lt;/strong&gt;&lt;br&gt;
  In a Zipfian distribution, &lt;code&gt;alpha&lt;/code&gt; controls skew—higher values mean more users&lt;br&gt;
  hit the same accounts, which raises contention. In a multi-writer database,&lt;br&gt;
  that kills throughput through lock contention. But SQLite and SpacetimeDB are&lt;br&gt;
  both single-writer systems. There are no locks. So contention actually&lt;br&gt;
  &lt;em&gt;helps&lt;/em&gt;: repeated hits on the same accounts warm the CPU cache. I'm keeping&lt;br&gt;
  &lt;code&gt;alpha=1.5&lt;/code&gt; because that's what SpacetimeDB used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The first run
&lt;/h3&gt;

&lt;p&gt;The first result I got surprised me.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_rpc.ts: {
  tps: 12682.8,
  samples: 126828,
  p50_ms: 3.457,
  p95_ms: 5.971,
  p99_ms: 9.431,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. &lt;strong&gt;12,682 TPS?&lt;/strong&gt; That is already almost double the 7,416 they show on their graph. &lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This number is also &lt;em&gt;impossibly&lt;/em&gt; wrong. Not because it's too low. Because it's too high.&lt;/p&gt;

&lt;p&gt;By default, SQLite runs in &lt;code&gt;journal_mode=delete&lt;/code&gt;. In this mode, SQLite does &lt;strong&gt;TWO&lt;/strong&gt; &lt;code&gt;fsync&lt;/code&gt; calls to the physical disk per transaction. First, it fsyncs the old data (the rollback journal). Then, it updates the main database and waits for a second fsync.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is fsync?&lt;/strong&gt;&lt;br&gt;
  When an application writes data, the operating system usually caches it in&lt;br&gt;
  memory and immediately says "done!" for speed. But if someone unplugs the&lt;br&gt;
  server right then, that data vanishes. To actually &lt;em&gt;guarantee&lt;/em&gt; durability, the&lt;br&gt;
  database must issue an &lt;code&gt;fsync&lt;/code&gt; system call. This forces the OS to physically&lt;br&gt;
  write the data to the storage drive, and the database halts until the hardware&lt;br&gt;
  returns a thumbs-up. That physical wait time is the disk latency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If my disk takes 683 µs per write, two fsyncs take 1,366 µs.&lt;/p&gt;

&lt;p&gt;1 second (1,000,000 µs) / 1,366 µs = &lt;strong&gt;a theoretical maximum of ~732 TPS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So how in the world is the benchmark reporting 12,000 TPS on a disk that physically maxes out at 732?&lt;/p&gt;

&lt;p&gt;I put my AI agent (opencode) to work debugging the benchmark's server code, and we found this:&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fromId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toId&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;account_missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...[balance math logic here]...&lt;/span&gt;

  &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newFrom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fromId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newTo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can you spot the issue with this Drizzle ORM code?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The update statements are never RUN.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drizzle requires you to call &lt;code&gt;.run()&lt;/code&gt; at the end of an update statement to actually execute the query against the database. Because it was missing, there was no execution. No disk writes. No waiting for &lt;code&gt;fsync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The benchmark wasn't measuring database writes. It was just measuring the speed of Node.js executing a database &lt;code&gt;SELECT&lt;/code&gt; and throwing some JSON in the garbage.&lt;/p&gt;

&lt;p&gt;So, I fixed the code, added the &lt;code&gt;.run()&lt;/code&gt; calls, and reran the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_rpc.ts: {
  tps: 360.8,
  samples: 3608,
  p50_ms: 133.759,
  p95_ms: 166.271,
  p99_ms: 276.991,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;360 TPS.&lt;/strong&gt; With a p50 latency of &lt;strong&gt;133ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now &lt;em&gt;this&lt;/em&gt; makes way more sense. Our theoretical max was ~732 TPS. Add in the Node.js event loop overhead, IPC, the HTTP roundtrip, and Drizzle's ORM overhead, and 360 TPS is exactly where you expect standard, double-fsync SQLite to land on this disk.&lt;/p&gt;

&lt;p&gt;We have found the true baseline. Now, let's fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting SQLite to 160K TPS
&lt;/h2&gt;

&lt;p&gt;We are at 360 TPS. SpacetimeDB is at 167,000 TPS. How do we bridge the gap?&lt;/p&gt;

&lt;p&gt;By giving SQLite the exact same architectural privileges that SpacetimeDB gave itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #1: Breaking the double-fsync
&lt;/h3&gt;

&lt;p&gt;Nobody running a high-throughput application uses SQLite in &lt;code&gt;journal_mode=delete&lt;/code&gt;. It is the absolute slowest configuration possible.&lt;/p&gt;

&lt;p&gt;The first step is turning on Write-Ahead Logging (&lt;code&gt;WAL&lt;/code&gt;). We'll keep &lt;code&gt;synchronous=FULL&lt;/code&gt;, which means SQLite still waits for the &lt;code&gt;fsync&lt;/code&gt; to complete before returning success. No durability shortcuts. We're still flushing every committed transaction to the physical drive.&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRAGMA journal_mode=WAL;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PRAGMA synchronous=FULL;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this matter? In the default mode, SQLite has to wait for two physical disk writes. In WAL mode, SQLite simply appends the new transaction to the end of the Write-Ahead Log (&lt;code&gt;fsync&lt;/code&gt; #1) and returns success immediately. The actual main database file is updated later in the background.&lt;/p&gt;

&lt;p&gt;We still preserve 100% of our durability guarantees—if the server crashes, SQLite recovers all committed transactions from the WAL—but we just halved our physical disk wait time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_rpc.ts: {
  tps: 1201,
  samples: 12010,
  p50_ms: 40.319,
  p95_ms: 49.439,
  p99_ms: 84.799,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1,201 TPS.&lt;/strong&gt; By removing exactly one &lt;code&gt;fsync&lt;/code&gt;, we more than tripled our throughput. Our latency also dropped from 133ms down to &lt;strong&gt;40ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Pause here if you'd like. Can you figure out why the latency dropped so drastically from removing just 0.683ms of disk wait time?)&lt;/em&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;From our earlier calculation, if 2 &lt;code&gt;fsync&lt;/code&gt;s gave us a theoretical cap of 732 TPS, 1 &lt;code&gt;fsync&lt;/code&gt; should get us to ~1,464 TPS. We're still slightly short. The bottleneck has moved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #2: Apples, oranges, and network protocols
&lt;/h3&gt;

&lt;p&gt;SpacetimeDB keeps using the phrase "apples-to-apples".&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%2Fa6gjclia6807mhur9gqn.jpg" 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%2Fa6gjclia6807mhur9gqn.jpg" alt="SpacetimeDB apples-to-apples claim" width="800" height="233"&gt;&lt;/a&gt;&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%2F2qkyisbohl1n6za5ktf8.jpg" 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%2F2qkyisbohl1n6za5ktf8.jpg" alt="Apples-to-apples benchmark framing" width="765" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But if you look at the benchmark setup, SpacetimeDB's apple is a runtime communicating over an active, stateful WebSocket connection. The connection opens once, and thousands of transactions flow through the pipe.&lt;/p&gt;

&lt;p&gt;The SQLite apple is a Node.js server forced to spin up a brand-new, stateless HTTP request lifecycle for &lt;em&gt;every single RPC call&lt;/em&gt;. TCP handshake, JSON parse, HTTP handler, DB query, JSON serialize, HTTP response. Over and over again.&lt;/p&gt;

&lt;p&gt;Is this a database benchmark? Or an HTTP overhead benchmark?&lt;/p&gt;

&lt;p&gt;If you care about latency and throughput, this is a workable problem. You don't need a new database runtime to keep your application code and storage on the same machine. It's straightforward to rewrite the SQLite RPC to use WebSockets instead of HTTP.&lt;/p&gt;

&lt;p&gt;So, I wrote a new SQLite RPC server that communicates over WebSockets instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's try it out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 1478.6,
  samples: 14786,
  p50_ms: 31.663,
  p95_ms: 37.983,
  p99_ms: 64.991,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1,478 TPS.&lt;/strong&gt; Our TPS went up, and our latency dropped again to &lt;strong&gt;31ms&lt;/strong&gt;, because we stopped forcing Node.js to construct and parse HTTP headers on every request.&lt;/p&gt;

&lt;p&gt;Look at the math. SQLite is a single-writer database. My disk takes exactly &lt;strong&gt;0.683 milliseconds&lt;/strong&gt; to execute a single &lt;code&gt;fsync&lt;/code&gt;.&lt;br&gt;
1,000ms / 0.683ms = &lt;strong&gt;~1,464 TPS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We've stripped away all the overhead. We're now bottoming out directly against the physical write latency of the NVMe SSD.&lt;/p&gt;
&lt;h3&gt;
  
  
  Fix #3: The physics of batching
&lt;/h3&gt;

&lt;p&gt;We are at ~1,500 TPS. Nowhere near 160,000.&lt;/p&gt;

&lt;p&gt;At first, this seems like a hard wall. If durability costs 0.683ms per write, aren't we capped? We can't make the disk faster.&lt;/p&gt;

&lt;p&gt;Shouldn't SpacetimeDB also hit the same wall? Are they doing some insane Rust wizardry to speed up the disk?&lt;/p&gt;

&lt;p&gt;No. That's impossible.&lt;/p&gt;

&lt;p&gt;Think about it like transportation. We have a bridge, and crossing it takes exactly 0.683ms. A bicycle crosses the bridge carrying one passenger (transaction). You can only move ~1,464 passengers across per second.&lt;/p&gt;

&lt;p&gt;The only way to move more people without breaking the speed limit is to upgrade the vehicle. You swap the bike for a bus. The bus &lt;em&gt;also&lt;/em&gt; takes 0.683ms to cross the bridge. The bridge speed hasn't changed. But the bus can now carry 100 passengers at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batching&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once we know what to do. It's straightforward to implement in Node.js.&lt;/p&gt;

&lt;p&gt;I added a simple batching queue: accumulate up to &lt;code&gt;10&lt;/code&gt; requests, or wait &lt;code&gt;2&lt;/code&gt; milliseconds—whichever comes first—then flush the entire batch with a single &lt;code&gt;fsync&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cross your fingers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 13438,
  samples: 134380,
  p50_ms: 2.861,
  p95_ms: 5.111,
  p99_ms: 7.403,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;13,438 TPS.&lt;/strong&gt; And our p50 latency dropped to &lt;strong&gt;2.8ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The math checks out. Our disk maxes out at ~1,464 &lt;code&gt;fsyncs&lt;/code&gt; per second. Group 10 transactions per &lt;code&gt;fsync&lt;/code&gt; (1,464 × 10) and our physical ceiling is 14,640 TPS. We're nearly there.&lt;/p&gt;

&lt;p&gt;But wait—our latency dropped to 2.8ms. We introduced extra waiting with batching. Why is it &lt;em&gt;lower&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;Because of head-of-line blocking. With 50 concurrent workers, every request waits in a single queue. Without batching, each request waits for all the individual fsyncs ahead of it—the median request sits behind roughly 25 separate disk writes. With a batch size of 10, those 50 requests collapse into at most 5 groups. The median request now waits for a handful of batches instead of a long chain of individual disk writes. That's why the queue time collapses.&lt;/p&gt;

&lt;p&gt;So, if batching 10 gets us 13K TPS, batching 100 should get us 130K, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 14467.8,
  samples: 144678,
  p50_ms: 2.047,
  p95_ms: 3.769,
  p99_ms: 5.047,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmm. &lt;strong&gt;14,467 TPS.&lt;/strong&gt; We barely moved.&lt;/p&gt;

&lt;p&gt;Remember that &lt;code&gt;maxInflightPerWorker&lt;/code&gt; parameter I mentioned at the start and told you to keep in mind? Here it is.&lt;/p&gt;

&lt;p&gt;The default SQLite benchmark restricts the client to &lt;code&gt;maxInflightPerWorker=1&lt;/code&gt;. With 50 concurrent workers, there are never more than &lt;strong&gt;50&lt;/strong&gt; requests sitting at the server at any given moment.&lt;/p&gt;

&lt;p&gt;Our bus holds 100 people. But there are only 50 people at the bus stop.&lt;/p&gt;

&lt;p&gt;The bus never fills up. The database sits idle, waiting for the 2ms timeout to fire and the next trickle of requests to arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The SpacetimeDB Secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SpacetimeDB sets &lt;code&gt;maxInflightPerWorker&lt;/code&gt; to 1 for the SQLite connector. But they set it to &lt;strong&gt;16,384&lt;/strong&gt; for their own benchmark.&lt;/p&gt;

&lt;p&gt;How did SpacetimeDB get 167K TPS? By keeping the bus stop permanently packed. With 16,384 in-flight requests per worker, batches fill instantly and throughput is maximized.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Note: In their demo, they show only TPS—not p50, p95, or p99. Flooding the server with in-flight requests maximizes throughput while destroying latency. 50 workers × 16,384 in-flight = 819,200 unresolved requests in memory.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We now have three knobs to play with: batch size, batch timeout, and in-flight requests per worker. Let's dial them up gradually.&lt;/p&gt;

&lt;p&gt;With batch=100 and 8 in-flight per worker, the bus stop is finally busy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_INFLIGHT_PER_WORKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 51753,
  samples: 517531,
  p50_ms: 6.439,
  p95_ms: 10.119,
  p99_ms: 12.607,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;51,753 TPS.&lt;/strong&gt; With a p50 of 6.4ms.&lt;/p&gt;

&lt;p&gt;You don't need 800,000 concurrent requests to saturate a single-writer database. You just need enough to keep the batching window full. Let's push further—batch=200, inflight=40:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_INFLIGHT_PER_WORKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;40 pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 109508,
  samples: 1095080,
  p50_ms: 14.767,
  p95_ms: 31.407,
  p99_ms: 37.087,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;109,508 TPS.&lt;/strong&gt; Doubling the batch size roughly doubled the throughput. Let's keep going—batch=1000, inflight=80:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1000 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_INFLIGHT_PER_WORKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 137528,
  samples: 1375280,
  p50_ms: 22.287,
  p95_ms: 46.687,
  p99_ms: 52.415,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;137,528 TPS.&lt;/strong&gt; The gains are compressing. We're running into a CPU ceiling now, not a disk one. Let's try batch=2000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_INFLIGHT_PER_WORKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 157436,
  samples: 1574359,
  p50_ms: 23.135,
  p95_ms: 29.999,
  p99_ms: 35.519,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;157,436 TPS.&lt;/strong&gt; And finally, batch=4000, inflight=120:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 &lt;span class="nv"&gt;SQLITE_WS_RPC_BATCH_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2 pnpm tsx src/rpc-servers/sqlite-ws-rpc-server.ts
&lt;span class="gp"&gt;tanay:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MAX_INFLIGHT_PER_WORKER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120 pnpm tsx src/cli.ts &lt;span class="nt"&gt;--seconds&lt;/span&gt; 10 &lt;span class="nt"&gt;--concurrency&lt;/span&gt; 50 &lt;span class="nt"&gt;--alpha&lt;/span&gt; 1.5 &lt;span class="nt"&gt;--connectors&lt;/span&gt; sqlite_ws_rpc
&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;sqlite_ws_rpc.ts: {
  tps: 163075,
  samples: 1630751,
  p50_ms: 31.935,
  p95_ms: 45.023,
  p99_ms: 55.711,
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;163,075 TPS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We did it. We touched SpacetimeDB's benchmark numbers, maintained durable disk writes, and used nothing but Node.js and out of the box SQLite + Drizzle ORM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did it stop at 160K?
&lt;/h3&gt;

&lt;p&gt;If the disk can do 1,464 &lt;code&gt;fsyncs&lt;/code&gt; per second and we're batching 4,000 requests per &lt;code&gt;fsync&lt;/code&gt;, shouldn't we easily hit millions of TPS? And why did a batch size of 100 only give us 51K instead of the expected 140K? Why doesn't the throughput scale linearly?&lt;/p&gt;

&lt;p&gt;Because we traded the disk bottleneck for a CPU bottleneck.&lt;/p&gt;

&lt;p&gt;Node.js is single-threaded, and &lt;code&gt;better-sqlite3&lt;/code&gt; is synchronous. When SQLite calls &lt;code&gt;fsync&lt;/code&gt;, it blocks the entire Node.js process. The event loop freezes. No JSON parsing. No SQL execution. Just waiting.&lt;/p&gt;

&lt;p&gt;Think about a 1-second time budget. Your process splits that second between two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sleeping while the disk fsyncs.&lt;/li&gt;
&lt;li&gt;Running your actual application code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you try to hit 140,000 TPS with a batch size of 100, you need 1,400 &lt;code&gt;fsyncs&lt;/code&gt; per second.&lt;br&gt;
1,400 × 0.683ms = &lt;strong&gt;956 milliseconds&lt;/strong&gt; frozen.&lt;/p&gt;

&lt;p&gt;You have &lt;strong&gt;44 milliseconds&lt;/strong&gt; left to parse 140,000 JSON requests. 0.314 microseconds / request. Physically impossible.&lt;/p&gt;

&lt;p&gt;But with a batch size of 4,000, you only need 40 &lt;code&gt;fsyncs&lt;/code&gt; per second to reach 160,000 TPS.&lt;br&gt;
40 × 0.683ms = &lt;strong&gt;27 milliseconds&lt;/strong&gt; frozen.&lt;/p&gt;

&lt;p&gt;You've bought yourself &lt;strong&gt;973 milliseconds&lt;/strong&gt; of pure CPU time.&lt;/p&gt;

&lt;p&gt;At 163,000 TPS, our single Node.js thread has exactly &lt;strong&gt;6.25 microseconds&lt;/strong&gt; per transaction to parse the WebSocket JSON, construct the query, traverse the B-Tree, and serialize the response. We haven't just maxed out the disk; we've maxed out the processor.&lt;/p&gt;

&lt;p&gt;Could we go faster? Yes. Drop Drizzle ORM for raw &lt;code&gt;better-sqlite3&lt;/code&gt; prepared statements, or swap the &lt;code&gt;ws&lt;/code&gt; library for a C++ implementation like &lt;code&gt;uWebSockets.js&lt;/code&gt;, and you'd squeeze tens of thousands more transactions out of this single core. You could also offload the database writes to a dedicated worker thread—keeping JSON parsing and WebSocket I/O on the main thread while a separate thread handles SQLite calls and blocks on &lt;code&gt;fsync&lt;/code&gt;—so the two never compete for the same CPU time. Bridging the final gap between Node.js and a native Rust runtime is entirely possible. But 163K TPS proves the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;To recap: here is the actual story of the benchmark, told through the data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th&gt;Inflight/Worker&lt;/th&gt;
&lt;th&gt;TPS&lt;/th&gt;
&lt;th&gt;p50 Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fake Benchmark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;12,682&lt;/td&gt;
&lt;td&gt;3.5ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;True Default SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;360&lt;/td&gt;
&lt;td&gt;133ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WAL Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,201&lt;/td&gt;
&lt;td&gt;40ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebSockets&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,478&lt;/td&gt;
&lt;td&gt;32ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batching (Batch=10)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;13,438&lt;/td&gt;
&lt;td&gt;2.9ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch=100, Inflight=8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;51,753&lt;/td&gt;
&lt;td&gt;6.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch=200, Inflight=40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;109,508&lt;/td&gt;
&lt;td&gt;14.8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch=1000, Inflight=80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;137,528&lt;/td&gt;
&lt;td&gt;22.3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch=2000, Inflight=80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;157,436&lt;/td&gt;
&lt;td&gt;23.1ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch=4000, Inflight=120&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;163,075&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;31.9ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SpacetimeDB (JS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16,384&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;104,485&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SpacetimeDB (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16,384&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;167,915&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SpacetimeDB's Node.js + SQLite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7,416&lt;/td&gt;
&lt;td&gt;not disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Standard, boring tech performs beautifully when you configure it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;If SQLite can hit 163K TPS on a single machine, why does SpacetimeDB exist?&lt;/p&gt;

&lt;p&gt;Not for the performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  SpacetimeDB as a product
&lt;/h3&gt;

&lt;p&gt;SpacetimeDB is a cool piece of tech. But step back and think about what the WASM reducer model is actually solving: running user-generated code safely, supporting multi-tenant platforms where you can't trust what's being executed. Stuff that &lt;em&gt;they&lt;/em&gt; need for their Maincloud offering.&lt;/p&gt;

&lt;p&gt;Their v2 launch tried to expand the original pitch to general web apps—but deploying a web app and deploying a game server are two completely different problems.&lt;/p&gt;

&lt;p&gt;And even for multiplayer games: if it's your own code on your own server, there's no untrusted code to sandbox. The WASM runtime buys you nothing. At that point, I'd rather roll my own application layer with SQLite or RocksDB as a storage engine than learn a proprietary runtime and get locked into it.&lt;/p&gt;

&lt;p&gt;The licensing is also worth noting. SpacetimeDB is BSL-licensed. The open source release is enough to run a single node—that's it. Clustering, replication, anything that looks like a real multi-node production setup is closed source and part of their Maincloud offering. The open-source version is primarily just marketing for that.&lt;/p&gt;

&lt;p&gt;There are also rough edges throughout, and I wouldn't recommend it for anything serious today.&lt;/p&gt;

&lt;h3&gt;
  
  
  On durability and production
&lt;/h3&gt;

&lt;p&gt;163K TPS on a local NVMe is fun. But it is not a production setup. If that drive dies, the data is gone.&lt;/p&gt;

&lt;p&gt;EBS is the obvious next step, but EBS has its &lt;a href="https://planetscale.com/blog/the-real-fail-rate-of-ebs" rel="noopener noreferrer"&gt;own failure story that PlanetScale wrote about&lt;/a&gt;. Also worth knowing: EBS doesn't replicate across availability zones. If cross-AZ replication matters to you, EFS handles this reasonably well—SQLite on EFS with a single writer is a workable setup.&lt;/p&gt;

&lt;p&gt;There's also a harder structural problem with the colocation pattern itself. Vertical scaling gets painful when your application server and your database are competing for the same CPU and RAM on the same machine. It's not a dealbreaker, but it's a real constraint.&lt;/p&gt;

&lt;p&gt;One of the few offerings that actually does this pattern well is Cloudflare Durable Objects. You get low-latency local access with real durability guarantees. Because they replicate writes across a quorum before acknowledging a response (or "communicating with the outside world") through something they call output gates. They scale horizontally. And there's less lock-in than you'd expect since you write plain TypeScript and SQLite with an ORM like Drizzle if you like.&lt;/p&gt;




&lt;p&gt;Infrastructure marketing will always try to convince you that your stack is broken and their new paradigm is the only way forward. But before you rewrite your backend, look at their code. You might just find they forgot to call &lt;code&gt;.run()&lt;/code&gt;.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;SpacetimeDB's benchmark docs &lt;a href="https://github.com/clockworklabs/SpacetimeDB/blob/abbcec4ab357b956f6a2d498a666c1516edcb355/templates/keynote-2/README.md#hardware-configuration" rel="noopener noreferrer"&gt;list two server options&lt;/a&gt;: a PhoenixNAP bare-metal i9-14900k or a GCP c4-standard-32 with RAID 0 across five local SSDs. It's unclear which they used. Either way, both run fast local storage—NVMe or local SSDs in RAID 0—putting disk writes roughly in the ~20–100 µs range. That's far faster my dev machine, which makes this initial local result even more suspicious. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;strong&gt;Why did the latency drop so drastically?&lt;/strong&gt; Because of head-of-line blocking. With 50 concurrent workers, request #50 is stuck in a queue behind 49 others. In &lt;code&gt;delete&lt;/code&gt; mode (double fsync = 1.3ms per request), the 50th request waits ~65ms just to start. Switch to WAL (single fsync = 0.68ms) and the line moves twice as fast—request #50 only waits ~34ms. Batching takes this further by collapsing the queue even more aggressively. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>database</category>
      <category>sqlite</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Cloudflare Durable Objects &amp; Nuxt: Building a Real-Time Chat App</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Wed, 09 Apr 2025 14:10:03 +0000</pubDate>
      <link>https://dev.to/tanay/cloudflare-durable-objects-nuxt-building-a-real-time-chat-app-pii</link>
      <guid>https://dev.to/tanay/cloudflare-durable-objects-nuxt-building-a-real-time-chat-app-pii</guid>
      <description>&lt;p&gt;This has been the most requested topic ever since I started working on Nuxflare.&lt;/p&gt;

&lt;p&gt;What are Cloudflare Durable Objects? How can we leverage them with Nuxt?&lt;/p&gt;

&lt;p&gt;Durable Objects are powerful and surprisingly affordable, but they can also seem a bit mysterious at first. I thought I'd break them down in a series of posts explaining how to get the most out of them.&lt;/p&gt;

&lt;p&gt;Here's a preview of what we're going to build:&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%2Faw3qignx1jhlnkme3oil.jpg" 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%2Faw3qignx1jhlnkme3oil.jpg" alt="Screenshot of the chat app" width="800" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it out live: &lt;a href="https://websockets-demo.nuxflare.com" rel="noopener noreferrer"&gt;https://websockets-demo.nuxflare.com&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Check out the GitHub repo: &lt;a href="https://github.com/nuxflare/durable-websockets" rel="noopener noreferrer"&gt;https://github.com/nuxflare/durable-websockets&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Let's get started
&lt;/h2&gt;

&lt;p&gt;I'm using Bun for its cool DX, but you can follow along with npm, pnpm, or yarn—whatever you're most comfortable with.&lt;/p&gt;

&lt;p&gt;First, let's create a new Nuxt project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx nuxi@latest init durable-websockets &lt;span class="nt"&gt;-t&lt;/span&gt; v4-compat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're using &lt;a href="https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-4" rel="noopener noreferrer"&gt;Nuxt 4 compatibility mode&lt;/a&gt;, which allows us to use the Nuxt 4 project structure, getting ready for it when it releases.&lt;/p&gt;

&lt;p&gt;Now, let's install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx nuxi@latest module add ui vueuse &lt;span class="c"&gt;# Nuxt UI v3, @vueuse/core&lt;/span&gt;
bun i &lt;span class="nt"&gt;-D&lt;/span&gt; nuxflare @cloudflare/workers-types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use &lt;a href="https://nuxflare.com/docs/nuxflare" rel="noopener noreferrer"&gt;Nuxflare&lt;/a&gt; to deploy two things to Cloudflare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The main Nuxt app to Cloudflare Workers.&lt;/li&gt;
&lt;li&gt; The WebSockets server with Durable Objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While you &lt;em&gt;can&lt;/em&gt; follow this tutorial without Nuxflare, there's really no reason not to use it. Even if you're comfortable writing &lt;code&gt;wrangler.toml&lt;/code&gt; configuration yourself, Nuxflare makes it easy to create and destroy resources and deploy to multiple environments. You still retain full control to customize your Wrangler configuration however you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up Nuxflare
&lt;/h2&gt;

&lt;p&gt;Let's initialize Nuxflare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun nuxflare init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nuxflare will ask you several questions about your project name, custom domains (for dev and prod deployments), and GitHub Actions setup. Everything is optional.&lt;/p&gt;

&lt;p&gt;I'm using &lt;code&gt;websockets-demo.nuxflare.com&lt;/code&gt; as the production domain and leaving the development domain empty to use the default Cloudflare Workers subdomain.&lt;/p&gt;

&lt;p&gt;For GitHub Actions, I'm using the "Production deployments only" preset, which means code updates to the &lt;code&gt;main&lt;/code&gt; branch in the &lt;code&gt;nuxflare/durable-websockets&lt;/code&gt; repository will automatically deploy to &lt;code&gt;websockets-demo.nuxflare.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Nuxflare CLI will also prompt you to create and configure a &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; for deploying resources to Cloudflare.&lt;/p&gt;

&lt;p&gt;Once complete, Nuxflare will install development dependencies like &lt;code&gt;sst&lt;/code&gt; (&lt;a href="https://sst.dev" rel="noopener noreferrer"&gt;SST.dev&lt;/a&gt;) and &lt;code&gt;wrangler&lt;/code&gt;, which are needed to manage Cloudflare resources. You'll also notice a new folder called &lt;code&gt;nuxflare&lt;/code&gt; and a file called &lt;code&gt;sst.config.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Feeling overwhelmed? Don't worry about it. For the most part, you can completely ignore the &lt;code&gt;nuxflare&lt;/code&gt; folder—it's where all the infrastructure-as-code "wizardry" happens. Just commit everything to source control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Infrastructure
&lt;/h2&gt;

&lt;p&gt;Now let's configure the infrastructure for our app by updating the &lt;code&gt;run()&lt;/code&gt; function inside &lt;code&gt;sst.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Nuxt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./nuxflare/nuxt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./nuxflare/worker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;prodDomain&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;devDomain&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;devDomain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Create WebSockets worker&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;websocketsUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebSockets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./websockets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;durableObjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WebSockets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bindingName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WEBSOCKETS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Create Nuxt app and pass the WebSockets URL&lt;/span&gt;
  &lt;span class="nc"&gt;Nuxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;extraVars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;NUXT_PUBLIC_WEBSOCKETS_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;websocketsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As mentioned, we're creating two components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; The WebSockets server with Durable Objects.&lt;/li&gt;
&lt;li&gt; The Nuxt app that connects to the WebSockets server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the Nuxt app, we specify &lt;code&gt;dir: "."&lt;/code&gt;, and Nuxflare will build and deploy it with Cloudflare Workers. For the WebSockets server, we specify a &lt;code&gt;websockets&lt;/code&gt; directory with &lt;code&gt;index.ts&lt;/code&gt; as the worker entrypoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Durable Objects
&lt;/h2&gt;

&lt;p&gt;So, what's the deal with Durable Objects?&lt;/p&gt;

&lt;p&gt;A Durable Object is essentially a class. You can create multiple instances of it, and each instance manages its own state and handles its own WebSocket connections.&lt;/p&gt;

&lt;p&gt;What makes Durable Objects special is that they're automatically placed close to the user. For example, in our chat app, when a user creates a new room, it creates a new Durable Object instance on a Cloudflare server closest to that user. When others join the same chat room (from anywhere in the world), they connect to that same Durable Object instance.&lt;/p&gt;

&lt;p&gt;This is powerful because you can focus on writing your real-time logic without worrying about server deployment, while users still get the best possible latency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: A Durable Object—at least for now—doesn't relocate once created. If the first user who creates a Durable Object instance is geographically distant from later users, those users might experience suboptimal latency. This limitation is difficult to solve with alternative approaches too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So yeah, a Durable Object is a class, and we're calling ours &lt;code&gt;"WebSockets"&lt;/code&gt;. But what's a binding name?&lt;/p&gt;

&lt;p&gt;Cloudflare has this thing: everything that gets used through a Worker has to go through a "binding"—whether it's KV, D1, Queues, or Durable Objects. For each feature, you need a binding name.&lt;/p&gt;

&lt;p&gt;We're saying: if any Worker needs to use the &lt;code&gt;"WebSockets"&lt;/code&gt; Durable Object, it should do so through a binding called &lt;code&gt;"WEBSOCKETS"&lt;/code&gt; (&lt;code&gt;env.WEBSOCKETS&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating our Durable Objects Class
&lt;/h2&gt;

&lt;p&gt;Now that we understand the basics, let's write our Durable Objects class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebSockets&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our Durable Object needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Handle WebSocket connection requests (when a Worker identifies a Durable Object instance that should handle a WebSocket connection).&lt;/li&gt;
&lt;li&gt; Process incoming WebSocket messages (when a user sends a message in the chat).&lt;/li&gt;
&lt;li&gt; Handle when a WebSocket connection closes (cleaning up state for that connection).&lt;/li&gt;
&lt;li&gt; Expose a &lt;code&gt;publish&lt;/code&gt; function to broadcast messages to specific users connected via WebSockets.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Handling WebSocket Requests
&lt;/h3&gt;

&lt;p&gt;For #1, let's write a &lt;code&gt;fetch&lt;/code&gt; function. We'll receive the full &lt;code&gt;request&lt;/code&gt; object from the Worker. From there, we should check if it's a WebSockets request, and if it is, we create a WebSockets pair. A WebSocket pair is like walkie-talkies: for each client that opens a WebSockets connection with the server, the server keeps one node and gives the other to the client. Then we can use the node on the server to send and receive messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebSockets&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;upgrade&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;websocket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractRoomAndUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;protocols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sec-websocket-protocol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="nx"&gt;protocols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// remove the room:userId from protocols&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webSocketPair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocketPair&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webSocketPair&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;serializeAttachment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acceptWebSocket&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;webSocket&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;protocols&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sec-websocket-protocol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;protocols&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="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error in websocket fetch:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still a few more things to unpack. Let's go one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication with the WebSocket Protocols Header
&lt;/h3&gt;

&lt;p&gt;Let's talk about the WebSocket protocols header hack we're using for authentication.&lt;/p&gt;

&lt;p&gt;WebSockets don't natively support passing authentication data in the initial handshake like HTTP headers. However, there's a workaround: the WebSocket protocol header can include custom protocols as comma-separated values.&lt;/p&gt;

&lt;p&gt;We're encoding our room and user ID in base64 and passing it as the first protocol. The server extracts this information to determine which room the user wants to join and to identify them in the chat.&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;// NOTE: in a real-world scenario, the token should instead be JWT or similar&lt;/span&gt;
&lt;span class="c1"&gt;// from which we could extract and validate room/user/topic and such&lt;/span&gt;
&lt;span class="c1"&gt;// or, the info can even be stored inside a KV&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractRoomAndUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;protocolHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sec-websocket-protocol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;protocolHeader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing sec-websocket-protocol header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;protocolHeader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid sec-websocket-protocol format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Room and User ID must be separated by a colon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Read this Stack Overflow post for more info about WebSocket authentication: &lt;a href="https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the purposes of this tutorial, we are doing no "validation" on the server to make sure the user is who they say they are. This won't work in production where we should probably use a JWT token or similar that can be verified on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket Hibernation and Serialize/Deserialize Attachment
&lt;/h3&gt;

&lt;p&gt;With Durable Objects, you pay for the time they're active.&lt;/p&gt;

&lt;p&gt;This can get expensive with WebSockets because if users are connected to a chat room but not actively chatting, the Durable Object sits idle, costing you money while it maintains the WebSocket connection state.&lt;/p&gt;

&lt;p&gt;Thankfully, Cloudflare offers WebSocket Hibernation. This means if a Durable Object has active WebSocket connections but isn't processing messages, it can "hibernate," and you won't pay for the inactive time. The WebSocket connection with the client remains open, and if there's activity, the Durable Object is "revived" to handle it.&lt;/p&gt;

&lt;p&gt;Durable Objects let you store a small amount of state (2KB) for each WebSocket that gets restored when it comes back online. This should contain minimal information to identify the client. For storing more info, you should use the Durable Objects Storage API, which lets you use a full-fledged KV store for persistence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling WebSocket Events
&lt;/h3&gt;

&lt;p&gt;For handling WebSocket messages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; We first identify the client based on the object we attach to each client.&lt;/li&gt;
&lt;li&gt; We support two types of incoming messages: one where a user announces (or changes) their name, and another where the user can send chat messages.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebSockets&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;webSocketMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ArrayBuffer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserializeAttachment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Validate message type and size&lt;/span&gt;
    &lt;span class="c1"&gt;// [...]&lt;/span&gt;
    &lt;span class="k"&gt;try&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;WebSocketMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid chat message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`name:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`name:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown message type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Message processing error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid message format&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;webSocketClose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;_wasClean&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserializeAttachment&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`name:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm using the Durable Objects Storage API for storing the user name with &lt;code&gt;await this.ctx.storage.put&lt;/code&gt; and &lt;code&gt;await this.ctx.storage.get&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When a WebSocket connection is closed, we clear the KV for that user.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Publish Function
&lt;/h3&gt;

&lt;p&gt;To send messages to the users connected to the room, we are using the &lt;code&gt;publish&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;We loop through all the WebSockets connected to our Durable Object instance and send them the message payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebSockets&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;websockets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWebSockets&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;for &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;ws&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deserializeAttachment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;publish err&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: The room check here is redundant because we are anyway creating a Durable Object instance for each room. But the redundancy is intentional in case we later change the logic for creating Durable Object instances.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Writing our Worker
&lt;/h2&gt;

&lt;p&gt;Now that our Durable Objects are ready to manage state and handle WebSocket connections, let's write the Cloudflare Workers code that uses these Durable Objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;WorkerEntrypoint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBSOCKETS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;DurableObjectNamespace&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebSockets&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractRoomAndUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;stub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// infer durable object instance from room name&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error in worker fetch:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WEBSOCKETS&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;DurableObjectNamespace&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebSockets&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// infer durable object instance from room name&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are using &lt;code&gt;WEBSOCKETS&lt;/code&gt; as the binding name to find the Durable Objects class.&lt;/p&gt;

&lt;p&gt;Durable Objects don't have a &lt;code&gt;create&lt;/code&gt; method to create an instance. So we just use &lt;code&gt;get&lt;/code&gt;. A Durable Object gets created on the go, if it's accessed for the first time.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;idFromName&lt;/code&gt; function takes a string to generate an ID for the instance such that it will generate the same ID for the same string (and different IDs for different strings). Like we wanted, we use the room name to uniquely identify the Durable Objects.&lt;/p&gt;

&lt;p&gt;We also expose a &lt;code&gt;publish&lt;/code&gt; method for the worker that passes things off to the &lt;code&gt;this.publish&lt;/code&gt; method in the Durable Object. This is useful because we can then call this method from some other worker with a service binding, and this will give us a way to send impromptu "server" messages to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Frontend with &lt;code&gt;useWebSocket&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the repository, we have a basic chat UI using Nuxt UI v3 and Tailwind. The important part is in the &lt;code&gt;Messages.vue&lt;/code&gt; component where we connect to the WebSockets server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;chatRoom&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useWebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;useRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;websocketsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;onConnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're using &lt;code&gt;useWebSocket&lt;/code&gt; from &lt;code&gt;@vueuse/core&lt;/code&gt; because it automatically handles reconnections and heartbeats for us.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;websocketsUrl&lt;/code&gt;, we have our &lt;code&gt;runtimeConfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// [...]&lt;/span&gt;
  &lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;websocketsUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://localhost:8787&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// default used for dev&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We specify a default value for development, but we override this when deploying to Cloudflare Workers in the &lt;code&gt;sst.config.ts&lt;/code&gt;:&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;// [...]&lt;/span&gt;
&lt;span class="nc"&gt;Nuxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;extraVars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NUXT_PUBLIC_WEBSOCKETS_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;websocketsUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;I'm currently working on an update to make development easier for Nuxflare. This will simplify using Durable Objects and service bindings in dev mode. The challenge is that you need a separate workers dev server running for these features. Dev mode for D1, KV, R2, etc., is already supported with &lt;code&gt;@nuxt-hub/core&lt;/code&gt; and &lt;code&gt;nitro-cloudflare-dev&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deploying Our Application
&lt;/h2&gt;

&lt;p&gt;To deploy, simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun nuxflare deploy &lt;span class="nt"&gt;--stage&lt;/span&gt; hello &lt;span class="c"&gt;# Use whatever stage you like&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also completely remove a stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun nuxflare remove &lt;span class="nt"&gt;--stage&lt;/span&gt; hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! You now have a real-time chat application running on Cloudflare Workers with Durable Objects and Nuxt. Pretty cool, right?&lt;/p&gt;

&lt;p&gt;This only scratches the surface of what you can do with Durable Objects. In future posts, I'll dive deeper into more advanced stuff.&lt;/p&gt;

&lt;p&gt;Let me know (&lt;a href="https://x.com/tanayvk" rel="noopener noreferrer"&gt;X&lt;/a&gt;, &lt;a href="https://discord.gg/e8Wg8Zp2yc" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;) if you have any questions or if there's anything specific about Durable Objects you'd like me to cover next!&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Go...
&lt;/h2&gt;

&lt;p&gt;Hey, if you've read this far, I'm guessing you found this useful.&lt;/p&gt;

&lt;p&gt;I've been working &lt;strong&gt;Nuxflare Pro&lt;/strong&gt; – it's a complete Nuxt + Cloudflare starter kit that saves you tons of setup time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a &lt;strong&gt;one-time purchase&lt;/strong&gt; with &lt;strong&gt;lifetime access&lt;/strong&gt; to all future Nuxflare products&lt;/li&gt;
&lt;li&gt;You'll be directly supporting my work to keep building in open-source and create content (like this post)&lt;/li&gt;
&lt;li&gt;You'll get early access to everything I create for the Nuxt and Cloudflare ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Speaking of which...&lt;/strong&gt; I'm currently building Nuxflare Landing – an AI coding optimized landing page builder that works seamlessly with Nuxt UI and Nuxt Content. As a Pro member, you'll get it completely free when it launches (soon).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nuxflare.com/pro" rel="noopener noreferrer"&gt;Check out Nuxflare Pro&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No pressure at all – and thanks for reading either way.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Announcing Nuxflare 0.2: UX improvements, better NuxtHub compatibility, custom domains, GitHub Actions support</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Fri, 07 Mar 2025 20:56:59 +0000</pubDate>
      <link>https://dev.to/tanay/announcing-nuxflare-02-ux-improvements-better-nuxthub-compatibility-custom-domains-github-fi7</link>
      <guid>https://dev.to/tanay/announcing-nuxflare-02-ux-improvements-better-nuxthub-compatibility-custom-domains-github-fi7</guid>
      <description>&lt;p&gt;I'm super excited to share the release of &lt;strong&gt;Nuxflare 0.2.2&lt;/strong&gt; with you today.&lt;/p&gt;

&lt;p&gt;Here's what's new:&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Better CLI Experience
&lt;/h2&gt;

&lt;p&gt;We've completely rebuilt the CLI to make it easier to use. Getting started is now as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuxflare init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fid8w1ricgt1rqdvnjdk7.jpg" 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%2Fid8w1ricgt1rqdvnjdk7.jpg" alt="Image description" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just run this command in your project and follow the simple steps to set up and deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌐 Custom Domains
&lt;/h2&gt;

&lt;p&gt;You can now easily set up your own domain during the initial setup (with &lt;code&gt;nuxflare init&lt;/code&gt;).&lt;br&gt;
This domain is used when you deploy to production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuxflare deploy &lt;span class="nt"&gt;--production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want separate domains for testing? No problem! Set up a dev domain and use stages:&lt;br&gt;
For example, if your dev domain is &lt;code&gt;dev.nuxflare.com&lt;/code&gt; and you deploy to stage &lt;code&gt;tanay&lt;/code&gt;, your app will be at &lt;code&gt;tanay.dev.nuxflare.com&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuxflare deploy &lt;span class="nt"&gt;--stage&lt;/span&gt; tanay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔄 Works Like NuxtHub CLI
&lt;/h2&gt;

&lt;p&gt;If you've used NuxtHub before, you'll feel right at home. We now support familiar commands like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nuxflare logs &lt;span class="nt"&gt;--production&lt;/span&gt;  &lt;span class="c"&gt;# see real-time remote logs&lt;/span&gt;
nuxflare open &lt;span class="nt"&gt;--stage&lt;/span&gt; tanay  &lt;span class="c"&gt;# quickly view your deployed site&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🤖 GitHub Actions Support
&lt;/h2&gt;

&lt;p&gt;Automate everything with our new GitHub Actions support! You can now set up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual deployments&lt;/li&gt;
&lt;li&gt;Automatic production deployments with the main branch&lt;/li&gt;
&lt;li&gt;Create preview deployments for each PR&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗️ Cleaner Project Structure
&lt;/h2&gt;

&lt;p&gt;We now create a dedicated &lt;code&gt;nuxflare&lt;/code&gt; folder for all the technical stuff, keeping your main &lt;code&gt;sst.config.ts&lt;/code&gt; file clean and simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 Works With Monorepos
&lt;/h2&gt;

&lt;p&gt;You can now use Nuxflare in projects with multiple Nuxt apps—more about this soon. This is how &lt;a href="https://nuxflare.com/pro" rel="noopener noreferrer"&gt;Nuxflare Pro&lt;/a&gt; works.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ More Reliable Than Ever
&lt;/h2&gt;

&lt;p&gt;We've fixed lots of issues to make everything work better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Smoother deployments&lt;/li&gt;
&lt;li&gt;Better resource handling&lt;/li&gt;
&lt;li&gt;Cleaner cleanup when you remove things&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Give Nuxflare 0.2 a try today and let me know what you think! I'd love to hear your feedback as we keep making Nuxflare better together.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>webdev</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Nuxt Deployment Guide: How to Deploy Nuxt to Cloudflare</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Sat, 22 Feb 2025 15:06:48 +0000</pubDate>
      <link>https://dev.to/tanay/nuxt-deployment-guide-how-to-deploy-nuxt-to-cloudflare-2a7j</link>
      <guid>https://dev.to/tanay/nuxt-deployment-guide-how-to-deploy-nuxt-to-cloudflare-2a7j</guid>
      <description>&lt;p&gt;When I first tried deploying a Nuxt app to Cloudflare, I wanted speed, simplicity, and control—here’s the guide I wish I had back then.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare + Nuxt?
&lt;/h2&gt;

&lt;p&gt;Cloudflare is awesome for deploying Nuxt apps. Here's why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app runs on Cloudflare Workers, which means it's scalable and deployed right at the edge (close to your users) with super-low cold-start times.&lt;/li&gt;
&lt;li&gt;The Nuxt community has built first-class support for Cloudflare.

&lt;ul&gt;
&lt;li&gt;Nuxt is actually one of the few frameworks specifically optimized to work with Cloudflare.&lt;/li&gt;
&lt;li&gt;Under the hood, Nuxt uses something called &lt;a href="https://github.com/unjs/unenv" rel="noopener noreferrer"&gt;unenv&lt;/a&gt; to make sure everything plays nicely with the Workers runtime.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  The NuxtHub Story
&lt;/h2&gt;

&lt;p&gt;You might have heard of &lt;a href="https://hub.nuxt.com" rel="noopener noreferrer"&gt;NuxtHub&lt;/a&gt;—it's an amazing tool for deploying Nuxt apps to Cloudflare. With just one command (&lt;code&gt;npx nuxthub deploy&lt;/code&gt;), you can get your app up and running. Pretty cool, right?&lt;/p&gt;

&lt;p&gt;NuxtHub offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Super simple deployments with a single command.&lt;/li&gt;
&lt;li&gt;Great developer experience with strong support for Cloudflare (D1, KV, AI, R2, Vectorize) through the &lt;a href="https://github.com/nuxt-hub/core" rel="noopener noreferrer"&gt;@nuxt-hub/core&lt;/a&gt; module.&lt;/li&gt;
&lt;li&gt;Cloudflare bindings setup, and database migrations.&lt;/li&gt;
&lt;li&gt;Dev tools to manage your database, blob storage, and KV storage while you're developing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where NuxtHub Falls Short: Why I Built Nuxflare
&lt;/h2&gt;

&lt;p&gt;While NuxtHub is great, it has a few limitations that were bugging me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't have full control over how your resources are deployed.

&lt;ul&gt;
&lt;li&gt;You can't deploy certain things like &lt;a href="https://developers.cloudflare.com/durable-objects/" rel="noopener noreferrer"&gt;Durable Objects&lt;/a&gt; (a powerful Cloudflare offering for real-time apps and persisting state in serverless edge deployments), queues, cron triggers, etc.&lt;/li&gt;
&lt;li&gt;You can't set up custom Cloudflare bindings (e.g., &lt;a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/" rel="noopener noreferrer"&gt;service bindings&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;While they have a generous free tier now, that could change anytime.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;That's why I created Nuxflare. I wanted to keep all the great stuff from NuxtHub but give developers complete control over their deployments.&lt;/p&gt;

&lt;p&gt;Here's what Nuxflare offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's completely &lt;a href="https://github.com/nuxflare/cli" rel="noopener noreferrer"&gt;open-source&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;It supports multi-stage deployments (dev, production, staging, etc.).&lt;/li&gt;
&lt;li&gt;It works with the NuxtHub core module, so you still get all those great developer tools and features.&lt;/li&gt;
&lt;li&gt;It uses open-source tools like &lt;a href="https://sst.dev" rel="noopener noreferrer"&gt;SST.dev&lt;/a&gt; and &lt;a href="https://pulumi.com" rel="noopener noreferrer"&gt;Pulumi&lt;/a&gt; for deploying and managing resources (infrastructure as code).

&lt;ul&gt;
&lt;li&gt;Using Cloudflare's APIs and Wrangler under the hood, so you don't have to expose access to your Cloudflare account.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Bonus: You can mix in components from other cloud providers like AWS, Neon, PlanetScale, Azure, or Vercel (I'll write more about using SST.dev with Nuxflare soon).&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Think of NuxtHub as having two parts: their open-source module (the good stuff for developers) and their deployment platform. Nuxflare lets you keep using that first part while giving you more flexibility with the deployment side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Coming soon: Our next major Nuxflare release will include GitHub Actions integration and even better NuxtHub compatibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Ready to Deploy?
&lt;/h2&gt;

&lt;p&gt;I'll walk you through the whole process of getting your Nuxt app up and running on Cloudflare using Nuxflare.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Clone an Example NuxtHub Project
&lt;/h3&gt;

&lt;p&gt;First, let's grab a template that shows what Nuxflare can do.&lt;/p&gt;

&lt;p&gt;This template demonstrates NuxtHub core features like AI, Blob, Database, Cache, KV, and Vectorize. We can get all this to work with zero config using Nuxflare.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/RihanArfan/chat-with-pdf.git
&lt;span class="nb"&gt;cd &lt;/span&gt;chat-with-pdf
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Set Up Nuxflare
&lt;/h3&gt;

&lt;p&gt;Run this command and follow the prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxflare init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll ask you for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your project name.&lt;/li&gt;
&lt;li&gt;Your preferred package manager (npm, yarn, or pnpm).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a special file called &lt;code&gt;sst.config.ts&lt;/code&gt; that handles your deployment with SST.dev.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Get Your Cloudflare Access Ready
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create an API token using &lt;a href="https://dash.cloudflare.com/profile/api-tokenshttps://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22:%22ai%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22vectorize%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22d1%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_r2%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_kv_storage%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_scripts%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22memberships%22,%22type%22:%22read%22%7D,%7B%22key%22:%22user_details%22,%22type%22:%22read%22%7D%5D&amp;amp;name=Nuxflare" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&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%2F1fzo6ru68wm7hjefhcar.jpg" 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%2F1fzo6ru68wm7hjefhcar.jpg" alt="Cloudflare API Token Screenshot" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set it as an environment variable in your shell:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_token_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Deploy Your App
&lt;/h3&gt;

&lt;p&gt;Ready to go live? Run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxflare deploy &lt;span class="nt"&gt;--stage&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You can specify different stage names to deploy copies of your app.&lt;br&gt;
This is useful when deploying development, staging, production, or more environments in the same account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Working on Your App Locally
&lt;/h2&gt;

&lt;p&gt;When you're developing, you can run the Nuxt dev server manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To connect to your remote Cloudflare resources (e.g., database, KV, storage, AI):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxflare dev &lt;span class="nt"&gt;--stage&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts up your development server and connects it to all your Cloudflare services.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Under the hood, Nuxflare sets the &lt;code&gt;NUXT_HUB_PROJECT_URL&lt;/code&gt; and &lt;code&gt;NUXT_HUB_PROJECT_SECRET_KEY&lt;/code&gt; environment variables to connect to your remote Cloudflare resources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cleaning Up
&lt;/h2&gt;

&lt;p&gt;Want to remove all resources? Just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxflare remove &lt;span class="nt"&gt;--stage&lt;/span&gt; dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm working on some exciting updates including GitHub Actions support and better NuxtHub compatibility. Stay tuned!&lt;/p&gt;

&lt;p&gt;If you're building something serious, check out &lt;a href="https://nuxflare.com/pro" rel="noopener noreferrer"&gt;Nuxflare Pro&lt;/a&gt;. It comes with extras like team auth, role-based access control, emails, payments, notifications and real-time features.&lt;/p&gt;

&lt;p&gt;Need help? &lt;a href="https://discord.gg/e8Wg8Zp2yc" rel="noopener noreferrer"&gt;Join our Discord community&lt;/a&gt;—we're always happy to help out!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building something cool with Nuxflare? I'd love to hear about it! Drop me a note or share it in our Discord.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>webdev</category>
      <category>infrastructureascode</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Nuxflare Auth: A lightweight self-hosted auth server built with Nuxt, Cloudflare and OpenAuth.js</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Mon, 13 Jan 2025 10:21:25 +0000</pubDate>
      <link>https://dev.to/tanay/nuxflare-auth-a-lightweight-self-hosted-auth-server-built-with-nuxt-cloudflare-and-openauthjs-1dnd</link>
      <guid>https://dev.to/tanay/nuxflare-auth-a-lightweight-self-hosted-auth-server-built-with-nuxt-cloudflare-and-openauthjs-1dnd</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/nuxflare/auth" rel="noopener noreferrer"&gt;Nuxflare Auth&lt;/a&gt; is a modern, lightweight, self-hosted authentication server designed to make adding auth to your apps a breeze. Built with &lt;a href="https://nuxt.com" rel="noopener noreferrer"&gt;Nuxt 3&lt;/a&gt;, &lt;a href="https://cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare Workers&lt;/a&gt;, and &lt;a href="https://openauth.js.org/" rel="noopener noreferrer"&gt;OpenAuth.js&lt;/a&gt;, it bundles everything you need in one place.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nuxflare" rel="noopener noreferrer"&gt;
        nuxflare
      &lt;/a&gt; / &lt;a href="https://github.com/nuxflare/auth" rel="noopener noreferrer"&gt;
        auth
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A modern, lightweight, self-hosted auth server built with Cloudflare, Nuxt, and OpenAuth.js.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/nuxflare/auth./images/demo.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fnuxflare%2Fauth.%2Fimages%2Fdemo.png" alt="Nuxflare Auth Login Screen"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Nuxflare Auth&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A modern, lightweight, self-hosted auth server built with &lt;a href="https://cloudflare.com" rel="nofollow noopener noreferrer"&gt;Cloudflare&lt;/a&gt;, &lt;a href="https://nuxt.com" rel="nofollow noopener noreferrer"&gt;Nuxt&lt;/a&gt;, and &lt;a href="https://openauth.js.org/" rel="nofollow noopener noreferrer"&gt;OpenAuth.js&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What's This?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Nuxflare Auth lets you add authentication to your apps without the headache. It's a monorepo that bundles everything you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A slick auth UI built with Nuxt 3 and &lt;a href="https://ui.nuxt.com" rel="nofollow noopener noreferrer"&gt;@nuxt/ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backend auth magic running on Cloudflare Workers&lt;/li&gt;
&lt;li&gt;A ready-to-use example so you can see how it all fits together&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;🔒 Complete authentication UI including:
&lt;ul&gt;
&lt;li&gt;Code-based login&lt;/li&gt;
&lt;li&gt;Password-based login&lt;/li&gt;
&lt;li&gt;Forgot password flow&lt;/li&gt;
&lt;li&gt;User registration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🔑 OAuth2 authentication with GitHub and Google (easily add more providers)&lt;/li&gt;
&lt;li&gt;✉️ Emails using Resend (or use any other provider)&lt;/li&gt;
&lt;li&gt;⚡ Lightning-fast, powered by Cloudflare's edge network&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project Layout&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;packages/
  ├── auth-frontend/   # auth UI components
  ├── emails/          # react email templates
  ├── example-client/  # example nuxt client
  └── functions/       # cloudflare workers
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Prerequisites&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Before getting started, you'll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;pnpm&lt;/li&gt;
&lt;li&gt;A Cloudflare account&lt;/li&gt;
&lt;li&gt;OAuth credentials from Google…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nuxflare/auth" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;With Nuxt, there are already good auth modules like &lt;a href="https://github.com/atinux/nuxt-auth-utils" rel="noopener noreferrer"&gt;nuxt-auth-utils&lt;/a&gt; and &lt;a href="https://github.com/sidebase/nuxt-auth" rel="noopener noreferrer"&gt;sidebase-auth&lt;/a&gt;.&lt;br&gt;
So, what’s different about Nuxflare Auth?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled Auth:&lt;/strong&gt; Nuxflare Auth lets you deploy the auth server and auth UI (built with Nuxt UI) separately from your main app.
This means you can easily reuse your auth server to work with multiple client-side apps (built with Nuxt or not), external APIs, mobile apps, and more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encourages Monorepo Architecture:&lt;/strong&gt; By splitting your Nuxt app and auth module, Nuxflare Auth keeps the bundle size minimal—perfect for deployments to Cloudflare Workers, which have strict size limits: 3 MB for the free plan and 10 MB for the paid plan.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/
  ├── auth-frontend/   # auth UI components
  ├── emails/          # react email templates
  ├── example-client/  # example nuxt client
  └── functions/       # cloudflare workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Deploying Nuxflare Auth
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;pnpm&lt;/li&gt;
&lt;li&gt;A Cloudflare account&lt;/li&gt;
&lt;li&gt;OAuth credentials from Google and GitHub&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; API key for sending emails&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repository and install dependencies:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/nuxflare/auth nuxflare-auth
   &lt;span class="nb"&gt;cd &lt;/span&gt;nuxflare-auth
   pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create and Configure API Token:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;a. Create a Cloudflare API token with the required permissions using &lt;a href="https://dash.cloudflare.com/profile/api-tokens?permissionGroupKeys=%5B%7B%22key%22:%22workers_r2%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_kv_storage%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22workers_scripts%22,%22type%22:%22edit%22%7D,%7B%22key%22:%22memberships%22,%22type%22:%22read%22%7D,%7B%22key%22:%22user_details%22,%22type%22:%22read%22%7D%5D&amp;amp;name=Nuxflare%20Auth" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.\&lt;br&gt;
   b. Set the &lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GahXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Configure your secrets:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# OAuth stuff&lt;/span&gt;
   pnpm sst secret &lt;span class="nb"&gt;set &lt;/span&gt;GoogleClientID your_client_id
   pnpm sst secret &lt;span class="nb"&gt;set &lt;/span&gt;GoogleClientSecret your_client_secret
   pnpm sst secret &lt;span class="nb"&gt;set &lt;/span&gt;GithubClientID your_client_id
   pnpm sst secret &lt;span class="nb"&gt;set &lt;/span&gt;GithubClientSecret your_client_secret

   &lt;span class="c"&gt;# For emails&lt;/span&gt;
   pnpm sst secret &lt;span class="nb"&gt;set &lt;/span&gt;ResendApiKey your_resend_api_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Configure your &lt;code&gt;fromEmail&lt;/code&gt; in &lt;code&gt;sst.config.ts&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fromEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi@nuxflare.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="c1"&gt;// ...&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start local development:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy to production:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pnpm sst deploy &lt;span class="nt"&gt;--stage&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example Client App
&lt;/h2&gt;

&lt;p&gt;The repository includes a simple example client app at &lt;code&gt;packages/example-client&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The API for the composables is very similar to &lt;code&gt;nuxt-auth-utils&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionState&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;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAccessTokenCookie&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;refreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRefreshTokenCookie&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;clear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;loggedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should point the client to the endpoint of your deployed auth instance:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```typescript [packages/example-client/app/utils/auth.ts]&lt;br&gt;
const client = createClient({&lt;br&gt;
  clientID: "nuxt",&lt;br&gt;
  issuer: "&lt;a href="https://authdemo.nuxflare.com" rel="noopener noreferrer"&gt;https://authdemo.nuxflare.com&lt;/a&gt;", // &amp;lt;-- replace this with your endpoint&lt;br&gt;
});&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


---

Thanks for reading!
If you encounter any issues or have suggestions, please [open an issue](https://github.com/nuxflare/auth/issues) on our GitHub repository.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>nuxt</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Nuxt Authorization: How to Implement Team Role-Based Access Control in Nuxt 3</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Wed, 04 Dec 2024 16:33:26 +0000</pubDate>
      <link>https://dev.to/tanay/nuxt-authorization-how-to-implement-team-role-based-access-control-in-nuxt-3-23l8</link>
      <guid>https://dev.to/tanay/nuxt-authorization-how-to-implement-team-role-based-access-control-in-nuxt-3-23l8</guid>
      <description>&lt;p&gt;If you're building a multi-tenant SaaS in Nuxt 3, you'll need a robust permissions system.&lt;br&gt;
Here's how I built a type-safe RBAC system that scales from small teams to enterprise, using Prisma and tRPC.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Barbapapazes/nuxt-authorization" rel="noopener noreferrer"&gt;nuxt-authorization&lt;/a&gt; for defining abilities&lt;/li&gt;
&lt;li&gt;Prisma for the database layer&lt;/li&gt;
&lt;li&gt;tRPC for type-safe API calls&lt;/li&gt;
&lt;li&gt;Works with any auth provider (&lt;a href="https://github.com/sidebase/nuxt-auth" rel="noopener noreferrer"&gt;sidebase/nuxt-auth&lt;/a&gt; in this example)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Basic Setup
&lt;/h2&gt;

&lt;p&gt;First, install the authorization module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpx nuxi@latest module add nuxt-authorization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Client-Side Authorization
&lt;/h2&gt;

&lt;p&gt;Set up a plugin to resolve the user on the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNuxtPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization-resolver&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;resolveClientUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Server-Side Authorization
&lt;/h2&gt;

&lt;p&gt;Similarly for the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&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="s2"&gt;#auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineNitroPlugin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;nitroApp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;nitroApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$authorization&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;resolveServerUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defining Type-Safe Abilities
&lt;/h2&gt;

&lt;p&gt;Here's how we define shared abilities that work on both client and server:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasTeamPermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listTeams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAbility&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getTeamDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAbility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateTeamDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineAbility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;hasTeamPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEAMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Database Schema
&lt;/h2&gt;

&lt;p&gt;Your Prisma schema needs to support roles and permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model TeamMembership {
  id     String @id @default(cuid())
  role Role @relation(fields: [roleId], references: [id])
  // [...]
}

model Role {
  id           String  @id @default(cuid())
  teamId       String?
  name         String
  description  String?
  isDefault    Boolean @default(false)
  isSystemRole Boolean @default(false)
  permissions Permission[]
  // [...]
}

model Permission {
  id          String  @id @default(cuid())
  title       String
  description String?
  action      String
  roleId      String
  // [...]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using Abilities in Components
&lt;/h2&gt;

&lt;p&gt;Check permissions in your Vue components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Can&lt;/span&gt; &lt;span class="na"&gt;:ability=&lt;/span&gt;&lt;span class="s"&gt;"deleteTeamAbility"&lt;/span&gt; &lt;span class="na"&gt;:args=&lt;/span&gt;&lt;span class="s"&gt;"[team?.id || '']"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Protected content here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Can&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Type-Safe API Authorization
&lt;/h2&gt;

&lt;p&gt;Create a tRPC procedure for checking abilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;abilityProcedure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;protectedProcedure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ability&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;BouncerAbility&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BouncerArgs&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ability&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;BouncerAbility&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="na"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BouncerArgs&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Ability&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TRPCError&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FORBIDDEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not authorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it in your API routes:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;abilityProcedure&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;teamIdentifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;getTeamDetails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Protected logic here&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this works well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fully type-safe from database to UI&lt;/li&gt;
&lt;li&gt;No external authorization service needed&lt;/li&gt;
&lt;li&gt;Works seamlessly with any auth provider&lt;/li&gt;
&lt;li&gt;Scales from simple to complex permission structures&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Want to see this RBAC system in action? This exact implementation is part of my Nuxt SaaS boilerplate.&lt;/p&gt;

&lt;p&gt;If you're building a multi-tenant SaaS, &lt;a href="https://saas-boilerplate.dev" rel="noopener noreferrer"&gt;check it out&lt;/a&gt;—it comes with everything you need: type-safe APIs using tRPC, team management, authentication, billing, and more. Every feature is built with the same attention to developer experience as this permissions system.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>typescript</category>
      <category>prisma</category>
      <category>authjs</category>
    </item>
    <item>
      <title>Sending Emails in Nuxt 3: How I Handle Emails in My SaaS Boilerplate</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Wed, 27 Nov 2024 15:40:40 +0000</pubDate>
      <link>https://dev.to/tanay/sending-emails-in-nuxt-3-how-i-handle-emails-in-my-saas-boilerplate-5ecn</link>
      <guid>https://dev.to/tanay/sending-emails-in-nuxt-3-how-i-handle-emails-in-my-saas-boilerplate-5ecn</guid>
      <description>&lt;p&gt;If you're building anything serious in Nuxt 3, you'll need to send emails.&lt;br&gt;
Here's my setup that lets you switch between Resend, SendGrid, or any other provider without rewriting code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.adonisjs.com/guides/digging-deeper/mail" rel="noopener noreferrer"&gt;AdonisJS Mail&lt;/a&gt; (I &lt;a href="https://www.npmjs.com/package/@tanayvk/mailer" rel="noopener noreferrer"&gt;made it work outside AdonisJS&lt;/a&gt; so it works with any JS backend)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vuemail.net/" rel="noopener noreferrer"&gt;Vue Email&lt;/a&gt; for templates&lt;/li&gt;
&lt;li&gt;Works with any email provider (Resend, AWS SES, Mailgun, Sparkpost, Brevo, Custom SMTP)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Basic Configuration
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateMailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateMailer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi@saas-boilerplate.dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SaaS Boilerplate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;mailers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESEND_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.resend.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="c1"&gt;// Easy to add more providers!&lt;/span&gt;
      &lt;span class="na"&gt;sendgrid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;transports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendgrid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SENDGRID_API_KEY&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Email Templates with Vue Email
&lt;/h2&gt;

&lt;p&gt;Here's a simple magic link email template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Html&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Body&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;Container&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Heading&amp;gt;&lt;/span&gt;Sign In to Your Account&lt;span class="nt"&gt;&amp;lt;/Heading&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Text&amp;gt;&lt;/span&gt;Hi &lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;,&lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Text&amp;gt;&lt;/span&gt;Click the button below to sign in:&lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Section&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;"signInLink"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sign In&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Section&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Text&amp;gt;&lt;/span&gt;
          Or copy this link: 
          &lt;span class="nt"&gt;&amp;lt;Link&lt;/span&gt; &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;"signInLink"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;signInLink&lt;/span&gt; &lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/Link&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Hr&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Text&amp;gt;&lt;/span&gt;This link expires in 1 hour.&lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/Container&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/Html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&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;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Hr&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="s2"&gt;@vue-email/components&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;signInLink&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;withDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signInLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com/sign-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sending Emails
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MagicLinkSignIn&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/emails/MagicLinkSignIn.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMailer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Magic Link Sign In&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MagicLinkSignIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;signInLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cool Features from AdonisJS Mail
&lt;/h2&gt;

&lt;p&gt;AdonisJS Mail comes with tons of powerful features that we can use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Transport Support&lt;/strong&gt;: Switch between email providers with one line of code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar Events&lt;/strong&gt;: Attach calendar invites to your emails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Attachments&lt;/strong&gt;: Easily attach files, streams, or buffers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML/Text Templates&lt;/strong&gt;: Support for both HTML and plain text emails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, attaching a calendar invite is as simple as:&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;icalEvent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Team Meeting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this works well
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Switch providers anytime&lt;/li&gt;
&lt;li&gt;Write templates in Vue&lt;/li&gt;
&lt;li&gt;Easy to test&lt;/li&gt;
&lt;li&gt;All the power of AdonisJS Mail in any JS backend&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Want to learn more about what AdonisJS Mail can do? Check out their &lt;a href="https://docs.adonisjs.com/guides/digging-deeper/mail" rel="noopener noreferrer"&gt;detailed documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This email setup is part of &lt;a href="https://saas-boilerplate.dev" rel="noopener noreferrer"&gt;my Nuxt SaaS boilerplate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're building a SaaS, check it out—it comes with type-safe APIs using tRPC, enterprise-grade RBAC (role-based access control), and production-ready features like auth, team management, and billing.&lt;/p&gt;

&lt;p&gt;Every feature is built with the same attention to developer experience as this email system.&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>javascript</category>
      <category>vue</category>
      <category>saas</category>
    </item>
    <item>
      <title>Building a Card with Gradient Hover Effect Inspired by Nuxt UI</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Thu, 31 Oct 2024 04:05:54 +0000</pubDate>
      <link>https://dev.to/tanay/building-a-card-with-gradient-hover-effect-inspired-by-nuxt-ui-2j</link>
      <guid>https://dev.to/tanay/building-a-card-with-gradient-hover-effect-inspired-by-nuxt-ui-2j</guid>
      <description>&lt;p&gt;I came across an amazing card hover effect on the &lt;a href="https://ui.nuxt.com" rel="noopener noreferrer"&gt;Nuxt UI website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, it’s a pro component—so I decided to build it myself!&lt;/p&gt;

&lt;p&gt;I tried getting some inspiration by inspecting their code, but it was more complex than I expected.&lt;br&gt;
I'm no CSS wizard, so I came up with a much simpler version of this effect.&lt;/p&gt;

&lt;p&gt;Here’s a tutorial on how to make a card with a cool gradient hover effect using TailwindCSS.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Set up a simple card component
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"['bg-neutral-950', props.class || '']"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Let’s start by building a basic card component with a black background, arranged in a grid.&lt;br&gt;&lt;br&gt;
This setup will be our testing ground.&lt;/p&gt;

&lt;p&gt;I'm using Vue here, but this can be easily replicated with React, Vanilla JS, or any other framework.&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%2Ff6ibsnlk026hmt2o8aak.gif" 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%2Ff6ibsnlk026hmt2o8aak.gif" alt="Step 1 Demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. Add a circular gradient that follows the mouse
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"target"&lt;/span&gt;
    &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"cssVars"&lt;/span&gt;
    &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"['shine bg-neutral-950', props.class || '']"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&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;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&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="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;useMouseInElement&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="s2"&gt;@vueuse/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMouseInElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;cssVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nc"&gt;.shine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="m"&gt;300px&lt;/span&gt; &lt;span class="nb"&gt;circle&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="m"&gt;#6366f1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The next step is adding a circular gradient that follows the mouse.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, add a &lt;code&gt;shine&lt;/code&gt; class to the card &lt;code&gt;div&lt;/code&gt; and set it up with a &lt;code&gt;radial-gradient&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This gradient is centered at CSS variables &lt;code&gt;--x&lt;/code&gt; and &lt;code&gt;--y&lt;/code&gt;, going from bright (any color) at the center to fully transparent, with a 300px radius (adjustable).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make the gradient follow the mouse, I’m using a &lt;code&gt;useMouseElement&lt;/code&gt; composable. Here, &lt;code&gt;elementX&lt;/code&gt; and &lt;code&gt;elementY&lt;/code&gt; are the mouse coordinates relative to the position of the target element.&lt;/p&gt;

&lt;p&gt;Depending on your framework, you could use a different hook or add a &lt;code&gt;mousemove&lt;/code&gt; event listener to update these CSS variables.&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%2Fv8b0qrzg1yu4wsbgvmld.gif" 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%2Fv8b0qrzg1yu4wsbgvmld.gif" alt="Step 2 Demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  3. Stacking divs
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"target"&lt;/span&gt;
    &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"cssVars"&lt;/span&gt;
    &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"['p-[2px] shine bg-neutral-950', props.class || '']"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full bg-neutral-950/80"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&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;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&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="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;useMouseInElement&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="s2"&gt;@vueuse/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMouseInElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;cssVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nc"&gt;.shine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="m"&gt;300px&lt;/span&gt; &lt;span class="nb"&gt;circle&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="m"&gt;#6366f1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I know what you’re thinking—it doesn’t look like the final card effect at all.&lt;/p&gt;

&lt;p&gt;The trick to achieving the right look is to stack another &lt;code&gt;div&lt;/code&gt; on top with a small amount of padding (I used 2px). &lt;/p&gt;

&lt;p&gt;There are other ways to achieve this effect, like using &lt;code&gt;::before&lt;/code&gt; pseudo-elements or Tailwind rings, but I found this to be the simplest and cleanest approach.&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%2F8w86u3ad65xlag2jjcq1.gif" 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%2F8w86u3ad65xlag2jjcq1.gif" alt="Step 3 Demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Finishing touches
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
    &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"target"&lt;/span&gt;
    &lt;span class="na"&gt;:style=&lt;/span&gt;&lt;span class="s"&gt;"cssVars"&lt;/span&gt;
    &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"['rounded-[15px] p-[2px] shine', props.class || '']"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
      &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"rounded-[13px] w-full h-full bg-gradient-to-b from-neutral-800/50 to-neutral-950/50 bg-neutral-950/80"&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&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;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&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="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;useMouseInElement&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="s2"&gt;@vueuse/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;class&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMouseInElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;cssVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementX&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;elementY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nc"&gt;.shine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="m"&gt;300px&lt;/span&gt; &lt;span class="nb"&gt;circle&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="m"&gt;#6366f1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally, give the outer &lt;code&gt;div&lt;/code&gt; a subtle background gradient and rounded borders.&lt;/p&gt;

&lt;p&gt;To keep the padding and border-radius looking clean, I added 2px to the border radius of the outer &lt;code&gt;div&lt;/code&gt; (general tip: adjust by &lt;code&gt;padding * 1.141&lt;/code&gt; for a proportional look).&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%2F25neu4c3c3yjc406z3fm.gif" 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%2F25neu4c3c3yjc406z3fm.gif" alt="Step 4 Demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Here's the GitHub repo to see the code in action:&lt;br&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tanayvk" rel="noopener noreferrer"&gt;
        tanayvk
      &lt;/a&gt; / &lt;a href="https://github.com/tanayvk/shiny-cards" rel="noopener noreferrer"&gt;
        shiny-cards
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Shiny Cards&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;Nuxt shiny card hover effect.&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tanayvk/shiny-cards" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>tailwindcss</category>
      <category>ui</category>
    </item>
    <item>
      <title>How I built PeerSplit: A free, peer-to-peer expense-splitting app—from idea to launch in just 2 weeks</title>
      <dc:creator>Tanay Karnik</dc:creator>
      <pubDate>Tue, 22 Oct 2024 07:00:00 +0000</pubDate>
      <link>https://dev.to/tanay/how-i-built-peersplit-a-free-peer-to-peer-expense-splitting-app-from-idea-to-launch-in-just-2-weeks-386m</link>
      <guid>https://dev.to/tanay/how-i-built-peersplit-a-free-peer-to-peer-expense-splitting-app-from-idea-to-launch-in-just-2-weeks-386m</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;PeerSplit&lt;/strong&gt;—a free, peer-to-peer alternative to Splitwise—in just two weeks, from idea to launch!&lt;/p&gt;

&lt;p&gt;PeerSplit is a local-first app for splitting group expenses. It works offline, is 100% free and private, and doesn't require signups or any personal data.&lt;/p&gt;

&lt;p&gt;Here’s how I built it and everything I learned along the way.&lt;/p&gt;

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

&lt;p&gt;I’ve relied on Splitwise for years to manage expenses with friends and roommates. But with its recent daily transaction limits and intrusive ads, it’s become frustrating to use.&lt;/p&gt;

&lt;p&gt;I wanted a free, privacy-first alternative that didn't require servers to store or sync data. I wouldn't trust my expenses with a third-party server.&lt;/p&gt;

&lt;p&gt;After working on peer-to-peer, local-first projects like a workout tracker and a distraction-free writing app, I realized I could apply the same approach to expense splitting.&lt;/p&gt;

&lt;p&gt;That’s how PeerSplit was born. I started designing the app.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the UI with Nuxt + Nuxt UI
&lt;/h2&gt;

&lt;p&gt;I suck at designing UIs.&lt;/p&gt;

&lt;p&gt;A few months ago, I wouldn't have thought I could build a UI as polished as PeerSplit's (some people even say it has better UX than Splitwise).&lt;/p&gt;

&lt;p&gt;So, how did I manage to do it? &lt;a href="https://ui.nuxt.com" rel="noopener noreferrer"&gt;Nuxt UI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nuxt UI is gorgeous, and it has amazing developer experience (DX).&lt;/p&gt;

&lt;p&gt;It also comes with other useful Nuxt modules like &lt;code&gt;@nuxt/icon&lt;/code&gt;, &lt;code&gt;@nuxtjs/tailwindcss&lt;/code&gt;, and &lt;code&gt;@nuxtjs/colormode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All I had to do was pick a primary color, and I had all the components I needed—icons, dark mode, and everything else—to bring PeerSplit’s UI together.&lt;/p&gt;




&lt;h2&gt;
  
  
  cr-sqlite for local syncing 🔗
&lt;/h2&gt;

&lt;p&gt;For local data storage and syncing, I went with &lt;a href="https://github.com/vlcn-io/cr-sqlite" rel="noopener noreferrer"&gt;cr-sqlite&lt;/a&gt;, which builds on wa-sqlite and uses CRDTs (conflict-free replicated data types).&lt;/p&gt;

&lt;p&gt;CRDTs are great for peer-to-peer systems because they handle conflicts automatically—so users can work offline, and when they reconnect, changes merge seamlessly.&lt;/p&gt;

&lt;p&gt;However, cr-sqlite doesn't sync changes over the network by itself. It only provides APIs to export and merge changes. You need to manually send those changes between devices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gun.js for peer-to-peer syncing 🌐
&lt;/h2&gt;

&lt;p&gt;To handle secure peer-to-peer syncing, I used &lt;a href="https://github.com/amark/gun" rel="noopener noreferrer"&gt;Gun.js&lt;/a&gt;, which provides a peer-to-peer, distributed graph database.&lt;/p&gt;

&lt;p&gt;Gun’s gun.user API lets me create encrypted nodes for each group. All changes for a group are stored on that node and synced only with group members, keeping everything private.&lt;/p&gt;

&lt;p&gt;When a user performs an action, I take the changes exported from cr-sqlite and push them to the node. When the user comes back online, Gun syncs the new changes, keeping everyone up-to-date.&lt;/p&gt;

&lt;p&gt;Implementing this in a performant way was tricky. For more details, you can check out the source code &lt;a href="https://github.com/tanayvk/peersplit" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simplifying debts 💰
&lt;/h2&gt;

&lt;p&gt;One cool feature of Splitwise (and now PeerSplit) is "simplifying debts."&lt;/p&gt;

&lt;p&gt;Here’s how it works: If A owes B and B owes C, A can just pay C directly to potentially reduce the number of repayments.&lt;/p&gt;

&lt;p&gt;In PeerSplit, I first calculate the net balance for each person. Then I sort those balances and suggest payments one by one to bring at least one person’s balance to zero each time.&lt;/p&gt;

&lt;p&gt;This sorting ensures that everyone sees the same repayments on their devices.&lt;/p&gt;

&lt;p&gt;It’s not 100% optimal (some groups might still have up to n-1 payments), but it works well in most cases.&lt;/p&gt;

&lt;p&gt;An optimal solution would be exponential to calculate and would only save a few payments. So this was the best tradeoff for simplicity and speed!&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groupGetPayments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;groupGetBalances&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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;balances&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  PWA
&lt;/h2&gt;

&lt;p&gt;I wanted PeerSplit to function as an offline app, but I didn’t want to go through the hassle of building multiple native applications or dealing with the lengthy process of publishing them on app stores. So, opting for a Progressive Web App (PWA) was the clear choice.&lt;/p&gt;

&lt;p&gt;A PWA combines the best of web and mobile apps, allowing users to install it on their devices while still enjoying offline capabilities.&lt;/p&gt;

&lt;p&gt;To transform my Nuxt app into a PWA, I used &lt;a href="https://github.com/vite-pwa/vite-plugin-pwa" rel="noopener noreferrer"&gt;vite-pwa&lt;/a&gt;.&lt;br&gt;
I designed an SVG logo in Figma and used it to generate all the necessary PWA assets through vite-pwa’s asset generator.&lt;/p&gt;

&lt;p&gt;After that, I configured the PWA manifest, and vite-pwa automatically set up the service worker for me.&lt;/p&gt;

&lt;p&gt;I configured Nuxt to prerender all the routes, so that my app could fully function offline.&lt;/p&gt;



&lt;p&gt;And that's a wrap. Thanks for reading!&lt;/p&gt;
&lt;h2&gt;
  
  
  Product Hunt Launch
&lt;/h2&gt;

&lt;p&gt;PeerSplit just launched on Product Hunt! It's my first launch, and I’d love your support and feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://producthunt.com/products/peer-split" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;Check out PeerSplit on Product Hunt&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;PeerSplit is fair-source, so feel free to contribute or submit feature requests on GitHub.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tanayvk" rel="noopener noreferrer"&gt;
        tanayvk
      &lt;/a&gt; / &lt;a href="https://github.com/tanayvk/peersplit" rel="noopener noreferrer"&gt;
        peersplit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      PeerSplit is a free, local-first, peer-to-peer app that helps you easily and privately split and track group expenses.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;PeerSplit&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;PeerSplit&lt;/strong&gt; is a free, local-first, peer-to-peer app that helps you easily and privately split and track group expenses.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;💯 &lt;strong&gt;100% Free&lt;/strong&gt; — No sign-up required&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;Local-First&lt;/strong&gt; — Works fully offline&lt;/li&gt;
&lt;li&gt;📱 &lt;strong&gt;Cross-Platform PWA&lt;/strong&gt; — Use it on mobile, desktop, or laptop&lt;/li&gt;
&lt;li&gt;🔒 &lt;strong&gt;Peer-to-Peer&lt;/strong&gt; — Collaborate with friends while keeping your data private&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Simple UX&lt;/strong&gt; — A smooth and minimal interface that stays out of your way&lt;/li&gt;
&lt;li&gt;🌙 &lt;strong&gt;Dark and Light Modes&lt;/strong&gt; — Switch between themes to match your preferences&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Import/Export&lt;/strong&gt; — Import from Splitwise and export data to CSV&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Planned Features&lt;/h2&gt;
&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;🔢 &lt;strong&gt;Advanced Bill Splitting&lt;/strong&gt; — Add an itemized bill as a single expense.&lt;/li&gt;

&lt;li&gt;🧾 &lt;strong&gt;Bill Scanning&lt;/strong&gt; — Automatically scan and split bills by taking a picture.&lt;/li&gt;

&lt;li&gt;💱 &lt;strong&gt;Multi-Currency Support&lt;/strong&gt; — Handle expenses in different currencies with real-time conversion rates.&lt;/li&gt;

&lt;li&gt;📝 &lt;strong&gt;Transaction Notes &amp;amp; Comments&lt;/strong&gt; — Add notes and comments for each transaction to keep…&lt;/li&gt;

&lt;/ul&gt;
&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tanayvk/peersplit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>javascript</category>
      <category>nuxt</category>
      <category>devlog</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
