Vibes DIY
Vibes DIY / Blog
From the build log

The three doors

We have been building a lot of event "picker" apps lately — one per conference or festival, each one a schedule you can heart sessions in, see what your friends are going to, and subscribe to as a live calendar. Pickathon first, then the same skin stamped onto DEF CON 34, JuliaCon, EuroSciPy, State of the Map, the Oregon Country Fair, IETF 126. The social machinery ports verbatim every time. The only real work in a new one is the feed adapter: teach the app to read this event's schedule.

Which sounds trivial. It's a public schedule. There's a URL. You fetch the JSON.

Three times that assumption walked us straight into a wall — and each wall was a different door that was locked. By the third, closing them in order had become a runbook. This is the story of the three doors, and the thing they taught us that we should have known from the start.

A weathered door with the number three painted on it, in a violet duotone wash.
The real question was never "is there JSON?" It was "who can open the door — from where your code actually runs?"

The first door: the browser

The obvious place to fetch a schedule is where the app lives — App.jsx, in the visitor's browser. One fetch(), parse, render. For a good number of feeds that is genuinely all it takes.

For the rest, the browser door is locked by CORS. A feed that serves clean JSON but sends no Access-Control-Allow-Origin header simply can't be read from another origin's page — the browser refuses to hand your code the response. This is not about whether the data exists or what shape it's in; we learned that the hard way by actually curling every URL on a research table of about eighteen candidate feeds. The desk research said "likely JSON, verify before ingest." Curling flipped roughly half of it: paths that were supposed to be missing were already live, a "documented" API needed N+1 requests where an undocumented agenda.json did it in one, one "real" feed was an empty shell — and the thing that actually split the buildable list in two wasn't the schema at all. It was whether the feed sent CORS headers. The schema tells you how to parse it. The CORS header tells you where your code is allowed to run.

The second door: the worker

When the browser is locked out, you move the fetch to the server. Every vibe can ship a backend.js that runs on our edge, and a backend fetch() handler is where the request can be admitted server-side. But here's the part it took us a beat to internalize: the default egress lane deliberately mirrors the browser — it forwards only what the target API's own CORS policy would let a browser do, so a no-CORS feed does not get an automatic pass just because the fetch moved to the worker. What the server lane adds is that admission becomes possible: a host on the platform's curated egress list (or reached through an owner-blessed exemption) can send a real request the browser never could. So door two isn't "the CORS check vanishes" — it's "there is now a place the request is allowed to be made from, once the host is admitted."

Get posts like this in your inbox

One email field. Real updates. No algorithm required.

Then DEF CON happened, and it slammed even that door. info.defcon.org doesn't just withhold CORS headers — it 403s the platform's egress too, at the IP/ASN level. Getting the host admitted wouldn't have helped: it isn't inspecting your request, it's refusing your whole address space. (Very on-brand for a hacker con.) The IETF side-meetings board did the same thing a couple of weeks later: clean JSON at its /_data endpoint, no CORS, and a worker-egress 403. Second door closed, twice.

The third door: the database

Here is the move that surprised us. When the app can't reach the data from the browser and the worker can't reach it either, there is still one writer the platform trusts completely: the app's owner. So the data walks in through the app's own synced database.

An owner-side refresher script — running in an environment that can reach the feed — fetches the schedule, slims it down, and writes it into the vibe's Firefly database as snapshot chunk documents. The app's access.js routes those chunks to a channel that's owner-writable, client-invisible, and backend-readable: strangers can't read them (so nobody sees a half-written board) and strangers can't write them (so nobody can poison the schedule). The backend's scheduled tick assembles the chunks in module state, and the same /_api endpoint serves the assembled snapshot — still preferring the live upstream automatically if the block ever lifts.

js// access.js — the snapshot chunks live in a channel only the owner can write
// and only the server can read. Same gate that keeps strangers out of your
// notes keeps strangers out of the served schedule.
if (doc.type === "schedule-chunk") {
  if (!user?.isOwner) throw { forbidden: "owner writes the schedule" };
  return { channels: ["schedule-private"] }; // client-invisible
}

The lovely part is that nothing downstream had to know. Once the snapshot assembles into the same event shape the app already used, the hearts, the friends-are-going view, the merged favorites timeline, the "Right Now / Up Next" strip, and the live calendar subscription all just worked — the data source's weirdness stayed quarantined in one adapter.

When a workaround becomes a genre

The first time we did this (DEF CON) it was a scramble. The second time (the Oregon Country Fair, whose host also predictably 403s worker egress) we reached for the shape we already had. The third time (IETF side meetings) it took about an hour end to end, because by then the three doors were a written-down ladder:

  1. Try the browser. If the feed sends CORS headers, you're done in App.jsx.
  2. Move to the worker. backend.js gives you a server-side lane — but the default egress still mirrors browser CORS, so a no-CORS host has to be admitted first (the curated platform list, or an owner blessing) before the fetch goes through.
  3. Walk it in through the database. If the host blocks worker egress too, an owner-written snapshot is the transport of last resort — and the same access function that protects your users' data protects the served feed.

"App portability" turned out to be mostly adapter-plus-palette, and the marginal cost of a new event app is set by its data source's weirdness, not its feature list — a cost that keeps shrinking, because each weird source hardens the ladder for the next one.

The honest ending

The third door is a workaround. It's a good workaround — durable, access-safe, genuinely necessary for hosts that refuse our whole address space — but it asks the app owner to run a script on a timer to keep a "live" schedule live, and that's a moving part that shouldn't exist for the common case.

The common case is a feed that only fails the browser door — no egress block, just missing CORS. And that case has a real answer, not a workaround: a server-side fetch in backend.js, where the request runs on our edge with the app's own secrets — keys that never touch the client — and passes its writes back through the same access.js gate as everything else. The one thing to be honest about is that the server lane is not a blanket CORS bypass: its default egress mirrors the browser, so a no-CORS host still has to be admitted — added to the curated platform list, or reached through an owner-blessed exemption — before the fetch goes through. Once it is, there's no owner cron, no snapshot chunks, no last-resort anything. Door two, admitted properly, is where most of these feeds should have lived from the beginning.

The reason they didn't is that the app builder still reaches for a browser fetch() by default, discovers CORS at runtime, and only then goes looking for the server. So we're making backend.js better documented and on by default — turning the server-side lane into the codegen default so the next event app tries the right door first, and the third door goes back to being what it should be: the rare, deliberate escape hatch for the hosts that lock every other one.

The takeaway is smaller than the three doors make it look. Whenever an app has to pull data from somewhere, the question that decides the whole architecture isn't "is there JSON?" It's who can open the door, from where your code actually runs.

Build an app that pulls real data

A feed, a schedule, an API — the backend that fetches it comes with the app.

Start building →

Enjoyed this? Get the next post by email

One email field. Real updates. No algorithm required.

Prefer a feed? RSS · Atom