<?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: ibr0r</title>
    <description>The latest articles on DEV Community by ibr0r (@ibr0r).</description>
    <link>https://dev.to/ibr0r</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4120955%2F4fbc3bf2-82bb-4abb-bb17-76642130722f.png</url>
      <title>DEV Community: ibr0r</title>
      <link>https://dev.to/ibr0r</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ibr0r"/>
    <language>en</language>
    <item>
      <title>Your browser tab as a backend</title>
      <dc:creator>ibr0r</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:02:41 +0000</pubDate>
      <link>https://dev.to/ibr0r/your-browser-tab-as-a-backend-1cai</link>
      <guid>https://dev.to/ibr0r/your-browser-tab-as-a-backend-1cai</guid>
      <description>&lt;p&gt;I was testing a signup flow on my phone. The API it needed did not exist yet, so I had &lt;code&gt;json-server&lt;/code&gt; running on my laptop, which meant the phone got &lt;code&gt;ECONNREFUSED&lt;/code&gt; and I got to spend twenty minutes on my local IP, my router, and a firewall rule I had forgotten about. This happens to me maybe once a month.&lt;/p&gt;

&lt;p&gt;The usual fixes all ask for something. &lt;code&gt;json-server&lt;/code&gt; is local only. Hosted mock services want an account, and your fake data goes on their machines. &lt;code&gt;ngrok&lt;/code&gt; works, but pointing a public tunnel at my laptop to serve four rows of fake users feels like a lot.&lt;/p&gt;

&lt;p&gt;What I actually wanted: type &lt;code&gt;users&lt;/code&gt;, get a URL, open that URL from anything.&lt;/p&gt;

&lt;p&gt;So the question was where the data lives. Every answer I came up with put it on a server, and then I had to think about accounts and storage and deleting things later. Then I tried the answer that sounds wrong at first: leave the data in the tab. Don't move it anywhere. Just make the tab reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;An HTTP request cannot reach a browser tab. A tab can open a WebSocket to a server, and after that the server can push it messages. That is the whole trick, running backwards.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant C as curl / phone / anything
    participant W as Worker
    participant D as Durable Object
    participant T as Your tab

    T-&amp;gt;&amp;gt;D: WebSocket connect (holds it open)
    C-&amp;gt;&amp;gt;W: GET /mock/k7Qx9/users
    W-&amp;gt;&amp;gt;D: route by mock id
    D-&amp;gt;&amp;gt;T: { method: "GET", path: "/users" }
    T-&amp;gt;&amp;gt;D: { status: 200, body: [...] }
    D-&amp;gt;&amp;gt;W: response
    W-&amp;gt;&amp;gt;C: 200 application/json&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When you open the app, the tab opens a WebSocket to a Durable Object keyed by your mock id. That object exists to hold the socket and nothing else. A request to &lt;code&gt;/mock/k7Qx9/users&lt;/code&gt; hits a Worker, the Worker hands it to the Durable Object with that id, the object writes the request down the socket, and it waits.&lt;/p&gt;

&lt;p&gt;Meanwhile the tab is doing what a tiny backend does. It reads the request, looks up the collection in IndexedDB, and writes back a response object.&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;async&lt;/span&gt; &lt;span class="nf"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;ws&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="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&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;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;ws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workspace_offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;503&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;reqId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;reply&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;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;resolve&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="mi"&gt;2000&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;pending&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="nx"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="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="s1"&gt;request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;msg&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;pending&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="nx"&gt;reqId&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;reply&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;buildResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workspace_timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&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;raw&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;msg&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;raw&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;response&lt;/span&gt;&lt;span class="dl"&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;pending&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reqId&lt;/span&gt;&lt;span class="p"&gt;)?.(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Durable Object gets that message, matches it back to the pending HTTP request by id, and returns it. The caller sees a normal JSON response and has no idea any of this happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why stateless matters here
&lt;/h2&gt;

&lt;p&gt;Nothing in the relay is persisted. The Durable Object holds one open socket and a map of requests it is currently waiting on. Close the tab and the socket dies, the object has nothing left to hold, and the endpoint stops answering.&lt;/p&gt;

&lt;p&gt;That is not a limitation I worked around. It is the reason there is no login. There is no account because there is nothing stored to attach an account to, and no database because your fake data never leaves your machine. The relay is a wire, not a store.&lt;/p&gt;

&lt;p&gt;I keep going back and forth on whether "stateless" is even the right word for something that holds a live socket. It holds state for as long as a request is in flight and not one moment longer. If someone has a better term I would like to hear it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is bad at
&lt;/h2&gt;

&lt;p&gt;Your endpoint dies with the tab. If you close the laptop mid-demo, the demo is over. For a mock API I think that is fine, and it is a real problem if you expected otherwise, which is why I am putting it in bold rather than in a FAQ at the bottom.&lt;/p&gt;

&lt;p&gt;Latency is worse than a normal mock server. Every request does a round trip to the edge, down a socket to your machine, and back. Fine for clicking through a UI. Not fine for a load test.&lt;/p&gt;

&lt;p&gt;Do not put anything real behind it. It is a mock API. It has no auth, and the only thing protecting your endpoint is that the id is hard to guess, which is not the same as security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ended up useful
&lt;/h2&gt;

&lt;p&gt;Testing on a real phone, which is what started this. Handing a QA person a URL that returns exactly the broken state I need them to see. Showing a client a flow at a coffee shop without deploying anything. In each case the thing I wanted to share was a state, not a service, and the state was already sitting in my browser.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://apeeye.ibr0r.com" rel="noopener noreferrer"&gt;apeeye.ibr0r.com&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/ibr0r0/apeeye" rel="noopener noreferrer"&gt;github.com/ibr0r0/apeeye&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>api</category>
    </item>
  </channel>
</rss>
