Trust the target, bless the person
When every vibe got a backend, the next ask was obvious: let backend.js call out. Webhooks answer in, but a backend that can't fetch() can't post to Slack, can't hit GitHub, can't talk to the API your app is about.
The obvious design is just as obvious: let each app's owner allowlist the hosts their backend may reach. It died in one sentence from the design review: the owner is an attacker in some cases.
Signup here is self-serve. Anyone with an email can be a vibe owner, which means any policy an owner sets for themselves is attacker-set policy. An owner-managed allowlist isn't a security control — it's a form the attacker fills out on the way to using your platform as a fetch farm. Trust has to come from somewhere that isn't the tenant. The design that shipped (#3048, PR #3074, full spec in the repo) found three such places.
Lane one: trust the target
The scaling gate is CORS-parity: the egress proxy behaves like a browser. It forwards a request only if the target's own CORS policy would have allowed the same cross-origin call from the vibe's web origin. That buys the invariant that carries the whole design:
Backend egress ≤ what every visitor's browser can already do from the vibe's frontend.
Your App.jsx can already fetch("https://api.github.com/...") from a million browsers. Moving the same call server-side grants no new network reachability — what the backend does gain is execution context, since ctx.secrets lives there and the browser call ran bare. It's the mirror image of cors-anywhere: that tool bypassed CORS and became an abuse magnet; this one only permits what CORS already permits. The world's public APIs maintain the allowlist for us, and nobody approves anything.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
The catch is that "behaves like a browser" is a discipline, not a vibe. The proxy strips every header a browser couldn't send (Cookie, Host, Origin, the whole sec-* family), presents the vibe's real origin, sends a genuine OPTIONS preflight for non-simple requests, and verifies Access-Control-Allow-Origin on every response — even when the preflight verdict was cached (the cache key is the full browser preflight-cache key, per-resource, never per-host, because /public may admit what /admin does not). Skip any one of those and ACAO: * stops being proof the target opted in and starts being a hole.
Lane two: the platform list, where adding a host is a PR
Some APIs are server-to-server by nature and send no CORS headers at all — LinkedIn's REST API is the canonical case. For those there's a curated list, and the interesting part is its form: an in-repo config module, compiled into the worker, where adding a host is a pull request. Named reviewer, diff, history, revert — the approval workflow already existed, it's called code review.
The granularity fight was worth having. The unit of approval is the exact FQDN, never the registrable domain — blessing an eTLD+1 like github.io blesses every attacker's user content, and an S3 apex blesses every attacker's bucket. Path narrowing is optional (endpoint-exact when the vetted feature calls exactly one endpoint), not mandatory — required paths churn on every /v1 → /v2 bump with zero security gain, and path matching is where proxies classically get bypassed, so when it is used it runs against the WHATWG-parsed, non-decoded pathname.
Lane three: bless the person
The long tail — the weird endpoint one trusted builder needs — kept trying to become an approval queue. Three over-designed rounds (per-vibe lists, owner host lists with CORS co-gates) collapsed into the actual idea: approve people once, not hosts forever. A company-set flag on the owner's account grants their vibes unrestricted https egress. It's the platform vouching for a person, set only by an admin-gated operation with no self-service path. The same machinery gives you blocked for free — an abuse circuit-breaker that kills all egress for an owner across every vibe they have.
Under all three lanes sits a floor nobody escapes, blessed or not: no private ranges, no cloud metadata endpoints, no IP-literal hosts, and none of the platform's own domains — the backend must never re-enter the control plane with the Durable Object's network position. Plus rate caps and a User-Agent that names the vibe, so an abuse report attributes to a tenant instead of to "Cloudflare." The caps are what make reactive moderation — flip blocked when something smells — honest rather than naive.
The implementation closed a hole on the way in
One more thing surfaced only when the code met review. The Durable Object's fetch router used to fall through to the _api handler-invoke lane as a catch-all — and the isolate's globalOutbound delivers every handler fetch() to that same router. Handler code could forge internal headers and invoke the platform's own request path directly, skipping the worker's sanitization. Making the router explicit-predicate (a review ask from Charlie) forced the question "what makes a request internal?" — and the only honest answer was cryptographic: the worker now stamps an internal secret on its own forwards, and everything unauthenticated is, by construction, tenant egress and goes through the lanes above.
The egress feature shipped as a gate and ended up also being the audit. That's the quiet payoff of designing for a hostile owner: every assumption about who's calling gets written down, and the ones that were wrong get found.
Your backend, batteries included
Every Vibes app gets a server side — webhooks, cron, secrets, AI calls, and outbound fetch with the trust model already thought through.
Start building →