{"id":10432,"date":"2026-07-20T12:20:21","date_gmt":"2026-07-20T17:20:21","guid":{"rendered":"https:\/\/master.dev\/blog\/?p=10432"},"modified":"2026-07-20T12:20:23","modified_gmt":"2026-07-20T17:20:23","slug":"durable-objects-on-cloudflare","status":"publish","type":"post","link":"https:\/\/master.dev\/blog\/durable-objects-on-cloudflare\/","title":{"rendered":"Durable Objects on Cloudflare"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">This is a post on one of Cloudflare&#8217;s coolest features: Durable Objects. We&#8217;ll introduce what they are, how they work, and walk through a reasonably realistic use case for them.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cloudflare Workers Review<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I&#8217;ve written about Cloudflare Workers previously, with an <a href=\"https:\/\/master.dev\/blog\/introduction-to-cloudflare-workers-for-web-apps\/\">introduction to them<\/a> and a post about some of the slightly unorthodox things you have to do to <a href=\"https:\/\/master.dev\/blog\/cloudflare-workers-and-hyperdrive-with-tanstack-start\/\">use a database with them<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We won&#8217;t rehash all of that here, but briefly, Cloudflare Workers are like AWS Lambda functions, except they have much, much lower latency. There are significant differences between those technologies, to be clear. But the high-level elevator pitch is that workers spin up extremely quickly, with virtually no &#8220;cold start&#8221; to satisfy requests against your web application. And these workers spin up as much as needed, giving you built-in horizontal scaling, no matter how spiky your traffic is at any given time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Missing From Workers Alone?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">State. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Anonymous Cloudflare workers that can spin up, serve your request, and die off are fantastic for satisfying spiky, growing traffic. But they&#8217;re terrible for managing long-running state. How could they? They&#8217;re the absolute opposite of long-running, so they&#8217;re not capable of managing long-running state.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What are Durable Objects?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.cloudflare.com\/products\/durable-objects\/\">Durable Objects<\/a> are Cloudflare&#8217;s answer to this. Durable Objects are stateful worker instances that include their own persistent storage (either SQLite or Cloudflare&#8217;s key-value storage). Durable Objects are, well, durable. They go idle when not in use, but when requests against them resume, they come back to life with access to their persistent storage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cloudflare guarantees that each Durable Object instance is active in only one location at a time. While active, it can cache state in memory for fast access, with the source of truth in its persistent storage (SQLite or KV storage).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Plus, WebSocket Support<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ll get into the specifics, but Durable Objects also ship with <em>built-in<\/em> WebSocket support. You can set up multiple socket connections, post and receive messages, and so on.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Our Use Case: a Shopping Cart<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s set up a Durable Object to hold the shopping cart for a hypothetical e-commerce site. We\u2019ll create one Durable Object instance for each user. The user&#8217;s cart contents will follow them around to any browser on any device (so long as they&#8217;re logged in).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sure, you could do the same thing with Postgres, but our Durable Object will come with some other nice features: anytime a user adds an item to the cart, we&#8217;ll use that web socket feature we just mentioned to broadcast to all devices that the cart has changed, and that the cart contents should refresh.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plus, our Durable Object will cache the cart&#8217;s contents in memory while active, avoiding what would otherwise be a round trip to the database powering your web app.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Setting Up a Durable Object<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">As we did <a href=\"https:\/\/master.dev\/blog\/introduction-to-cloudflare-workers-for-web-apps\/\">in the intro<\/a>, we&#8217;ll use <a href=\"https:\/\/tanstack.com\/start\/latest\">TanStack Start<\/a> here too. <a href=\"https:\/\/github.com\/arackaf\/cloudflare-do-blog-post\">The repo for everything is here<\/a> in case I don&#8217;t explain or show anything clearly.<\/p>\n\n\n\n\n  <div class=\"course\">\n          <div class=\"course-image\">\n        <a href=\"https:\/\/master.dev\/courses\/tanstack\/\">          <img decoding=\"async\"\n            src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/tanstack-course.webp?fit=500%2C500&#038;ssl=1\"\n            alt=\"TanStack Start &amp; TanStack Query\"\n          >\n        <\/a>      <\/div>\n    \n    <div class=\"course-text\">\n      \n              <h3 class=\"course-name\">\n                      <a href=\"https:\/\/master.dev\/courses\/tanstack\/\">TanStack Start &amp; TanStack Query<\/a>\n                  <\/h3>\n      \n      \n              <div class=\"course-author\">\n                      <img decoding=\"async\"\n              class=\"course-author-image\"\n              src=\"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/adam.webp?fit=250%2C250&#038;ssl=1\"\n              alt=\"Adam Rackis\"\n            >\n                    <div class=\"course-author-meta\">\n                          <span class=\"course-author-name\">Adam Rackis<\/span>\n                                      <span class=\"course-author-company\">Spotify<\/span>\n                      <\/div>\n        <\/div>\n      \n    <\/div>\n  <\/div>\n\n\n\n\n<h3 class=\"wp-block-heading\">Create the Durable Object<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A Durable Object is created from a JavaScript class with the appropriate base class.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { DurableObject } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"cloudflare:workers\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CartDO<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">DurableObject<\/span> <\/span>{}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ll add methods to it in a bit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Custom Server Entrypoint<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We need to ensure our Durable Object is exported with our application bundle. The recommended way of doing that is with a custom server entrypoint. We&#8217;ll create a <code>src\/server.ts<\/code> file.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> handler <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\/server-entry\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  fetch: handler.fetch,\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This alone does nothing and simply recreates what TanStack Start does out of the box. The point of that is so we can <em>export<\/em> our Durable Object from this same entrypoint. We do that with a standard old JavaScript export.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> handler <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\/server-entry\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Export Durable Objects<\/span>\n<span class=\"hljs-keyword\">export<\/span> { CartDO } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/durable-obj\/Cart\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> {\n  fetch: handler.fetch,\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">And now we change the <code>main<\/code> field in our Wrangler file (remember: <a href=\"https:\/\/developers.cloudflare.com\/workers\/wrangler\/\">Wrangler<\/a> is the CLI tool from Cloudflare for managing our Workers settings) from the default of:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-string\">\"main\"<\/span>: <span class=\"hljs-string\">\"@tanstack\/react-start\/server-entry\"<\/span>,<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">To:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-string\">\"main\"<\/span>: <span class=\"hljs-string\">\"src\/server.ts\"<\/span>,<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">More Wrangler Additions<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">That custom server entrypoint was a one-time change that <em>allows us<\/em> to then start adding in Cloudflare goodies like durable objects (naturally), queues, or cron schedules. Check <a href=\"https:\/\/developers.cloudflare.com\/workers\/framework-guides\/web-apps\/tanstack-start\/?utm_source=chatgpt.com#custom-entrypoints\">Cloudflares docs<\/a> for more info.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So now we&#8217;ll add a <code>durable_objects<\/code> section with a binding to the Durable Object we added above.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">  <span class=\"hljs-string\">\"durable_objects\"<\/span>: {\n    <span class=\"hljs-attr\">\"bindings\"<\/span>: &#91;\n      {\n        <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"CART_DO\"<\/span>,\n        <span class=\"hljs-attr\">\"class_name\"<\/span>: <span class=\"hljs-string\">\"CartDO\"<\/span>,\n      },\n    ],\n  },<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Then, somewhat annoyingly, we need to add a migration for it as well.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\">  <span class=\"hljs-string\">\"migrations\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"tag\"<\/span>: <span class=\"hljs-string\">\"v1\"<\/span>,\n      <span class=\"hljs-attr\">\"new_sqlite_classes\"<\/span>: &#91;<span class=\"hljs-string\">\"CartDO\"<\/span>],\n    },\n  ],<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Building our Durable Object<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To avoid dumping too much code at you all at once, let&#8217;s sketch out a minimal version of our Shopping Cart, just to see how everything works and fits together. Let&#8217;s create the following methods:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code><strong>getCart()<\/strong><\/code> to retrieve cart contents. We&#8217;ll hard-code static data for now, and wire up SQLite in a moment.<\/li>\n\n\n\n<li><code><strong>addItem()<\/strong><\/code> to add an item to the cart, and then broadcast a WebSocket message that the cart has updated, so any browser tabs this user has open will update. Again, we&#8217;ll only do the latter for now, before setting up SQLite in a moment.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Then we&#8217;ll need a method to set up a WebSocket connection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And even though we won&#8217;t need to send WebSocket messages from the browser to the Durable Object (only from the Durable Object to the browser), we&#8217;ll set that up too so we can see how it works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With that, here&#8217;s the initial sketch of our Durable Object.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { DurableObject } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"cloudflare:workers\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">class<\/span> CartDO <span class=\"hljs-keyword\">extends<\/span> DurableObject {\n  getCart() {\n    <span class=\"hljs-keyword\">return<\/span> {\n      items: &#91;\n        { id: <span class=\"hljs-number\">1<\/span>, name: <span class=\"hljs-string\">\"Building Microservices\"<\/span> },\n        { id: <span class=\"hljs-number\">2<\/span>, name: <span class=\"hljs-string\">\"Standing Desk\"<\/span> },\n      ],\n    };\n  }\n  addItem() {\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> socket of <span class=\"hljs-keyword\">this<\/span>.ctx.getWebSockets()) {\n      <span class=\"hljs-keyword\">try<\/span> {\n        socket.send(\n          <span class=\"hljs-built_in\">JSON<\/span>.stringify({\n            <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"cart-updated\"<\/span>,\n          }),\n        );\n      } <span class=\"hljs-keyword\">catch<\/span> {\n        <span class=\"hljs-comment\">\/\/ The socket may have disconnected before Cloudflare observed it.<\/span>\n        socket.close(<span class=\"hljs-number\">1011<\/span>, <span class=\"hljs-string\">\"Unable to send cart update\"<\/span>);\n      }\n    }\n  }\n  fetch(request: Request): Response {\n    <span class=\"hljs-keyword\">if<\/span> (request.headers.get(<span class=\"hljs-string\">\"Upgrade\"<\/span>) !== <span class=\"hljs-string\">\"websocket\"<\/span>) {\n      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-string\">\"Expected WebSocket\"<\/span>, {\n        status: <span class=\"hljs-number\">426<\/span>,\n      });\n    }\n\n    <span class=\"hljs-keyword\">const<\/span> pair = <span class=\"hljs-keyword\">new<\/span> WebSocketPair();\n    <span class=\"hljs-keyword\">const<\/span> client = pair&#91;<span class=\"hljs-number\">0<\/span>];\n    <span class=\"hljs-keyword\">const<\/span> server = pair&#91;<span class=\"hljs-number\">1<\/span>];\n\n    <span class=\"hljs-keyword\">this<\/span>.ctx.acceptWebSocket(server);\n\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-literal\">null<\/span>, {\n      status: <span class=\"hljs-number\">101<\/span>,\n      webSocket: client,\n    });\n  }\n  webSocketMessage(socket: WebSocket, message: <span class=\"hljs-built_in\">string<\/span> | <span class=\"hljs-built_in\">ArrayBuffer<\/span>) {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"received on DO\"<\/span>, message);\n    socket.send(<span class=\"hljs-built_in\">JSON<\/span>.stringify({ message: <span class=\"hljs-string\">\"Message received\"<\/span>, originalMessage: message }));\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Let&#8217;s walk through that.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>getCart()<\/code> method, for now, just returns some static data. We&#8217;ll fix that in a moment. The <code>addItem()<\/code> method is where we broadcast our WebSocket message. We access all subscribed sockets like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> socket <span class=\"hljs-keyword\">of<\/span> <span class=\"hljs-keyword\">this<\/span>.ctx.getWebSockets()) {<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Then call <code>socket.send<\/code>. Simple and humble.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>fetch<\/code> method may seem surprising, but that&#8217;s how we set up a new WebSocket subscriber against our Durable Object. We check the headers to ensure it is indeed a WebSocket setup and error out if not; we&#8217;re free to accept and reply to any manner of fetch requests, of course, but none apply to this use case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cloudflare provides the primitives for creating WebSockets.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> pair = <span class=\"hljs-keyword\">new<\/span> WebSocketPair();\n<span class=\"hljs-keyword\">const<\/span> client = pair&#91;<span class=\"hljs-number\">0<\/span>];\n<span class=\"hljs-keyword\">const<\/span> server = pair&#91;<span class=\"hljs-number\">1<\/span>];\n\n<span class=\"hljs-keyword\">this<\/span>.ctx.acceptWebSocket(server);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The <code>WebSocketPair<\/code> function is a Cloudflare runtime global. We set up the connection, and then call <code>this.ctx.acceptWebSocket(server);<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lastly, <code>webSocketMessage<\/code> is the method we use to <em>receive<\/em> WebSocket messages from the client. This is a special, reserved name Durable Objects classes have for this purpose.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Our Durable Object<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">How do we consume those Durable Object methods from our app?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, let&#8217;s write a helper function to get an instance of our Durable Object.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { getCurrentUser } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/current-user\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { env } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"cloudflare:workers\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getCartForCurrentUser = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> user = <span class=\"hljs-keyword\">await<\/span> getCurrentUser();\n\n  <span class=\"hljs-keyword\">const<\/span> { CART_DO } = env;\n  <span class=\"hljs-keyword\">const<\/span> cartId = CART_DO.idFromName(user.id);\n  <span class=\"hljs-keyword\">const<\/span> cart = CART_DO.get(cartId);\n\n  <span class=\"hljs-keyword\">return<\/span> cart;\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">For our use case, we want one Durable Object per user to hold that user&#8217;s cart contents. So we grab our user (this isn&#8217;t a post on auth, so user credentials are just hard-coded) grab our <code>env<\/code> object, and pull out our <code>CART_DO<\/code> object from there. Remember, <code>CART_DO<\/code> was wired up in Wrangler.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json\"><span class=\"hljs-string\">\"durable_objects\"<\/span>: {\n  <span class=\"hljs-attr\">\"bindings\"<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">\"name\"<\/span>: <span class=\"hljs-string\">\"CART_DO\"<\/span>,\n      <span class=\"hljs-attr\">\"class_name\"<\/span>: <span class=\"hljs-string\">\"CartDO\"<\/span>,\n    },\n  ],\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><code>CART_DO.idFromName<\/code> allows us to retrieve a globally unique, internal ID for the durable Object instance we want from a string identifier, and then <code>CART_DO.get<\/code> takes that internal ID and gets us the actual, live proxy to interact with the Durable Object instance.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We handle this interaction on the server, so we&#8217;ll need some server functions. Here are two for the <code>getCart<\/code> \/ <code>addItem<\/code> calls.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> addItem = createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> }).handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> cart = <span class=\"hljs-keyword\">await<\/span> getCartForCurrentUser();\n  <span class=\"hljs-keyword\">await<\/span> cart.addItem();\n});\n\n<span class=\"hljs-keyword\">const<\/span> getCart = createServerFn({ method: <span class=\"hljs-string\">\"GET\"<\/span> }).handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> cart = <span class=\"hljs-keyword\">await<\/span> getCartForCurrentUser();\n  <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> cart.getCart();\n  <span class=\"hljs-keyword\">return<\/span> { items: result.items };\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Get the cart object and call the methods. Simple.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setting Up the Socket Connection<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To set up the WebSocket connection, we need to send an HTTP request that upgrades the connection to WebSocket. So we&#8217;ll create a TanStack API endpoint that receives that HTTP request, and simply forwards it to the Durable Object<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-comment\">\/\/ routes\/api\/cart\/subscribe.tsx<\/span>\n<span class=\"hljs-keyword\">import<\/span> { createFileRoute } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-router\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { getCartForCurrentUser } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"#\/server\/getCartForCurrentUser\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> Route = createFileRoute(<span class=\"hljs-string\">\"\/api\/cart\/subscribe\"<\/span>)({\n  server: {\n    handlers: {\n      GET: <span class=\"hljs-keyword\">async<\/span> ({ request }) =&gt; {\n        <span class=\"hljs-keyword\">const<\/span> cart = <span class=\"hljs-keyword\">await<\/span> getCartForCurrentUser();\n        <span class=\"hljs-keyword\">return<\/span> cart.fetch(request);\n      },\n    },\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Then a little boilerplate to call into the right place.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">openWebSocket<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>&lt;WebSocket&gt;<span class=\"hljs-function\">(<span class=\"hljs-params\">(<span class=\"hljs-params\">res<\/span>, <span class=\"hljs-params\">rej<\/span><\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> protocol = location.protocol === <span class=\"hljs-string\">\"https:\"<\/span> ? <span class=\"hljs-string\">\"wss:\"<\/span> : <span class=\"hljs-string\">\"ws:\"<\/span>;\n\n    <span class=\"hljs-keyword\">const<\/span> socket = <span class=\"hljs-keyword\">new<\/span> WebSocket(<span class=\"hljs-string\">`<span class=\"hljs-subst\">${protocol}<\/span>\/\/<span class=\"hljs-subst\">${location.host}<\/span>\/api\/cart\/subscribe`<\/span>);\n\n    socket.addEventListener(<span class=\"hljs-string\">\"open\"<\/span>, <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n      <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"WebSocket connected\"<\/span>);\n      res(socket);\n    });\n\n    socket.addEventListener(<span class=\"hljs-string\">\"error\"<\/span>, <span class=\"hljs-function\"><span class=\"hljs-params\">event<\/span> =&gt;<\/span> {\n      <span class=\"hljs-built_in\">console<\/span>.error(<span class=\"hljs-string\">\"WebSocket error:\"<\/span>, event);\n      rej(event);\n    });\n\n    socket.addEventListener(<span class=\"hljs-string\">\"close\"<\/span>, <span class=\"hljs-function\"><span class=\"hljs-params\">event<\/span> =&gt;<\/span> {\n      <span class=\"hljs-built_in\">console<\/span>.error(<span class=\"hljs-string\">\"WebSocket closed:\"<\/span>, event);\n    });\n  });\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">We can call this from the browser and get back our socket object.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">openWebSocket().then(<span class=\"hljs-function\"><span class=\"hljs-params\">socket<\/span> =&gt;<\/span> {\n  setSocket(socket);\n  <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"Socket open\"<\/span>);\n\n  socket.addEventListener(<span class=\"hljs-string\">\"message\"<\/span>, event =&gt; {\n    <span class=\"hljs-built_in\">console<\/span>.log(event);\n  });\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Once we have our socket, we can call <code>.send<\/code> on it as desired.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you&#8217;d like to see the full code as of this initial setup, check out the tag initial-setup in the <a href=\"https:\/\/github.com\/arackaf\/cloudflare-do-blog-post\">repo<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wiring Up SQLite<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The only missing piece is <a href=\"https:\/\/sqlite.org\/\">SQLite<\/a> from within our Durable Object. To interact with our SQLite database, we access the <code>ctx<\/code> object that exists on our object, as a result of inheriting from the <code>DurableObject<\/code> base class.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>ctx.storage.sql<\/code> function gives us an object with an <code>exec<\/code> method which we can use to execute SQL commands. In our constructor, we&#8217;ll set up our table.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">  <span class=\"hljs-keyword\">constructor<\/span>(<span class=\"hljs-params\">ctx: DurableObjectState, env: Env<\/span>) {\n    <span class=\"hljs-keyword\">super<\/span>(ctx, env);\n\n    ctx.blockConcurrencyWhile(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n      ctx.storage.sql.exec(<span class=\"hljs-string\">`\n        CREATE TABLE IF NOT EXISTS cart_items (\n          id TEXT PRIMARY KEY,\n          name TEXT NOT NULL,\n          description TEXT NOT NULL,\n          price REAL NOT NULL,\n          category TEXT NOT NULL,\n          image TEXT NOT NULL,\n          quantity INTEGER NOT NULL DEFAULT 1\n        )\n      `<\/span>);\n    });\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This isn&#8217;t the most resilient design, but it should suffice for a simplified blog post.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>ctx.blockConcurrencyWhile<\/code> function ensures no other requests are served by this individual Durable Object instance until the code inside has completed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this example <code>sql.exec()<\/code> is synchronous, so nothing could interleave while it&#8217;s running anyway. But we still wrap initialization in <code>blockConcurrencyWhile<\/code> because constructors often grow over time (loading state, awaiting storage, migrations, etc.), and this guarantees no requests are dispatched until initialization is finished.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Running Queries<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">From here, we can implement our <code>getCart()<\/code> method<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">  getCart(): CartContents {\n    <span class=\"hljs-keyword\">const<\/span> rows = <span class=\"hljs-keyword\">this<\/span>.ctx.storage.sql\n      .exec&lt;CartItemRow&gt;(\n        <span class=\"hljs-string\">`\n        SELECT\n          id,\n          name,\n          description,\n          price,\n          category,\n          image,\n          quantity\n        FROM cart_items\n        ORDER BY name\n      `<\/span>,\n      )\n      .toArray();\n\n    <span class=\"hljs-keyword\">const<\/span> items: CartItem&#91;] = rows.map(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">row<\/span><\/span>) =&gt;<\/span> ({\n      ...row,\n      lineTotal: row.price * row.quantity,\n    }));\n\n    <span class=\"hljs-keyword\">return<\/span> {\n      items,\n      totalItems: rows.reduce(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">sum<\/span>, <span class=\"hljs-params\">row<\/span><\/span>) =&gt;<\/span> sum + row.quantity, <span class=\"hljs-number\">0<\/span>),\n      totalPrice: rows.reduce(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">sum<\/span>, <span class=\"hljs-params\">row<\/span><\/span>) =&gt;<\/span> sum + row.price * row.quantity, <span class=\"hljs-number\">0<\/span>),\n    };\n  }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">And our <code>addItem<\/code> method:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">async<\/span> addItem(item: Product) {\n  <span class=\"hljs-keyword\">this<\/span>.ctx.storage.sql.exec(\n    <span class=\"hljs-string\">`\n      INSERT INTO cart_items (\n        id,\n        name,\n        description,\n        price,\n        category,\n        image,\n        quantity\n      )\n      VALUES (?, ?, ?, ?, ?, ?, 1)\n\n      ON CONFLICT(id) DO UPDATE SET quantity = quantity + 1\n    `<\/span>,\n    item.id,\n    item.name,\n    item.description,\n    item.price,\n    item.category,\n    item.image,\n  );\n\n  <span class=\"hljs-keyword\">this<\/span>.#sendCartUpdatedEvent();\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Having a <code>clearCart<\/code> method is pretty handy.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">async<\/span> clearCart() {\n  <span class=\"hljs-keyword\">this<\/span>.ctx.storage.sql.exec(<span class=\"hljs-string\">`DELETE FROM cart_items`<\/span>);\n  <span class=\"hljs-keyword\">this<\/span>.#sendCartUpdatedEvent();\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Other Details<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s plenty of other small details to handle: setting up the web socket connection when the app loads, wiring up react-query to read the cart contents, invalidating the query whenever we get a socket notification that the cart changed, and of course, wiring up the UI for things like adding an item, clearing the cart, etc.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This post is already long, so <a href=\"https:\/\/github.com\/arackaf\/cloudflare-do-blog-post\">check the repo<\/a> if you&#8217;re curious how I implemented those things.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Future Tweaks<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Remember, Durable Objects are single-threaded and only run in one location at a time. That means it would be safe for us to cache our cart in memory. Our SQLite queries are incredibly fast, but if we ever wanted to, we could add something like <code>this.cachedCartContent<\/code> (or <code>this.#cachedCartContents<\/code> to make it private), update it whenever we modify the cart, and return it, if present, instead of querying SQLite.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping Up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We&#8217;ve already seen all the big pieces here: how to set up WebSocket connections; how to send and receive WebSocket messages; how to set up and manage our SQLite database; and how to grab and call methods on our durable object from our TanStack app.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From here, it&#8217;s just a matter of connecting everything. As I said, you can <a href=\"https:\/\/github.com\/arackaf\/cloudflare-do-blog-post\">check the repo<\/a> for those details, but to show that this does in fact work, here&#8217;s a video of the browser tabs all syncing their carts correctly.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe loading=\"lazy\" title=\"VideoPress Video Player\" aria-label=\"VideoPress Video Player\" width=\"500\" height=\"283\" src=\"https:\/\/videopress.com\/embed\/3DQUKbGX?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0\" frameborder=\"0\" allowfullscreen data-resize-to-parent=\"true\" allow=\"clipboard-write; presentation\"><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1770107250'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<h2 class=\"wp-block-heading\">What About Unauthenticated Users?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We faked authentication for this app to show how each user would grab their own Durable Object. But what about a real e-commerce site where you&#8217;re much more likely to just buy things without logging in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One likely solution would be to set a cookie with some manner of UUID for the user, use that as their temporary <code>userId<\/code>, and use that to sync their shopping cart between tabs. Sure, it won&#8217;t work across devices, but that&#8217;s more than fine for typical usage, and better than some actual websites I&#8217;ve shopped from \u2014 I&#8217;m looking at you, Bonobos.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Concluding Thoughts<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cloudflare is a delight to develop with. Workers are already a fantastic primitive to ship web applications on top of. With durable objects, a whole host of additional use cases are beautifully unlocked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Hopefully, this post has given you the tools necessary to take advantage.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is a post on one of Cloudflare&#8217;s coolest features: Durable Objects. We&#8217;ll introduce what they are, how they work, and walk through a reasonably realistic use case for them. Cloudflare Workers Review I&#8217;ve written about Cloudflare Workers previously, with an introduction to them and a post about some of the slightly unorthodox things you [&hellip;]<\/p>\n","protected":false},"author":21,"featured_media":10464,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[334,501,516,240,517],"class_list":["post-10432","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-cloudflare","tag-cloudflare-workers","tag-durable-objects","tag-tanstack","tag-websockets"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/master.dev\/blog\/wp-content\/uploads\/2026\/07\/do.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10432","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/comments?post=10432"}],"version-history":[{"count":11,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10432\/revisions"}],"predecessor-version":[{"id":10475,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/posts\/10432\/revisions\/10475"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media\/10464"}],"wp:attachment":[{"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/media?parent=10432"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/categories?post=10432"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/master.dev\/blog\/wp-json\/wp\/v2\/tags?post=10432"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}