DEV Community

ibr0r
ibr0r

Posted on

Your browser tab as a backend

I was testing a signup flow on my phone. The API it needed did not exist yet, so I had json-server running on my laptop, which meant the phone got ECONNREFUSED 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.

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

What I actually wanted: type users, get a URL, open that URL from anything.

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.

The shape of it

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.

sequenceDiagram
    participant C as curl / phone / anything
    participant W as Worker
    participant D as Durable Object
    participant T as Your tab

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

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 /mock/k7Qx9/users 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.

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.

async relay(msg) {
  const ws = this.ctx.getWebSockets().find((w) => w.readyState === 1);
  if (!ws) return json({ error: 'workspace_offline' }, 503);

  const reqId = crypto.randomUUID();
  const reply = await new Promise((resolve) => {
    const timer = setTimeout(() => resolve(null), 2000);
    this.pending.set(reqId, (m) => { clearTimeout(timer); resolve(m); });
    ws.send(JSON.stringify({ type: 'request', reqId, ...msg }));
  });
  this.pending.delete(reqId);

  return reply ? buildResponse(reply) : json({ error: 'workspace_timeout' }, 504);
}

webSocketMessage(ws, raw) {
  const msg = JSON.parse(raw);
  if (msg.type === 'response') this.pending.get(msg.reqId)?.(msg);
}
Enter fullscreen mode Exit fullscreen mode

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.

Why stateless matters here

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.

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.

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.

What this is bad at

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.

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.

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.

Where it ended up useful

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.

Try it: apeeye.ibr0r.com
Source: github.com/ibr0r0/apeeye

Top comments (0)