How-to

Serve a WREN site on your own domain

Your site already has a public URL like /orgs/{slug}/tree/mysite/index.html. A few lines of Cloudflare Worker turn that into https://www.example.com/.

Before you start

  • The tree is public: a principal: "*" read rule on tree:mysite (see Publish a site). Check it by opening https://<wren-host>/orgs/{slug}/tree/mysite/index.html in a private window.
  • Your domain's DNS is on Cloudflare (any plan), and you have wrangler installed.

The Worker

It maps paths onto the tree, adds index.html for directory URLs (WREN doesn't), and forwards only what the browser needs. No API key is involved, because the tree is public.

js · src/index.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === "/mcp") {                            // AI agents: this domain's MCP endpoint
      const headers = new Headers();
      for (const h of ["content-type", "accept", "authorization", "mcp-protocol-version"]) {
        const v = request.headers.get(h); if (v) headers.set(h, v);
      }
      return fetch(env.MCP, { method: request.method, headers, body: request.method === "POST" ? request.body : undefined });
    }
    if (request.method !== "GET" && request.method !== "HEAD") {
      return new Response("Method not allowed", { status: 405 });
    }
    let path = url.pathname;
    if (path.endsWith("/")) path += "index.html";            // directory → index.html
    else if (!/\.[a-z0-9]+$/i.test(path)) path += "/index.html"; // /about → /about/index.html

    const upstream = `${env.UPSTREAM}${path}${url.search}`;
    const res = await fetch(upstream, {
      headers: { Accept: request.headers.get("Accept") ?? "*/*" },  // WREN negotiates bytes vs JSON on Accept
    });

    const out = new Response(res.body, res);
    out.headers.delete("set-cookie");
    return out;
  },
};
toml · wrangler.toml
name = "mysite"
main = "src/index.js"
compatibility_date = "2026-10-01"
routes = [{ pattern = "www.example.com", custom_domain = true }]

[vars]
UPSTREAM = "https://wren.aemwip.com/orgs/oak-spur-spur/tree/mysite"
MCP      = "https://wren.aemwip.com/orgs/oak-spur-spur/mcp?tree=mysite"

Rewrite /about to /about/index.html only if your site uses folder-style pages. If it has flat files like /about.html, drop that line.

Deploy it

bash
npx wrangler deploy
curl -sI https://www.example.com/ | head -5      # 200, content-type: text/html

From now on you only deploy to WREN (wren deploy, then wren promote). The Worker doesn't change.

AI agents on your domain

The /mcp route in the Worker gives your domain its own MCP endpoint, scoped to what the domain shows: https://www.example.com/mcp.

  • Without a key (anyone's agent): read-only, published content only, and only the tree named in ?tree=. Other trees and collections of the org stay invisible. To also expose some collections, list them: …/mcp?tree=mysite&collections=results,standings.
  • With a key (your own agents): full tools, but only for keys of this org. A key from another org gets 403, so the domain can't be used as a door into a different org.

Without the Worker, the same endpoint is https://<wren-host>/orgs/{slug}/mcp?tree=mysite. See Connect AI agents with MCP.

Data on the same domain

Pages can call WREN's public data API directly, since public routes send Access-Control-Allow-Origin: *:

js
const API = "https://wren.aemwip.com/api/v1/orgs/oak-spur-spur";
const { items } = await (await fetch(`${API}/results?limit=100`)).json();

Or with wren.js: <script src="https://wren.aemwip.com/wren.js" data-base="https://wren.aemwip.com/api/v1/orgs/oak-spur-spur"></script>. If you'd rather keep everything on your domain, add a second route in the Worker (e.g. /data/* → /api/v1/orgs/{slug}/*). Public data still needs no key.

Caching

  • WREN sends Cache-Control: public, max-age=60, stale-while-revalidate=86400 on successful public reads, and no-store on errors, so a 404 from before a deploy isn't kept.
  • Cloudflare's own settings can override that. If files stay stale after a promote, check the zone's Browser Cache TTL and Cache Rules, or add ?v=<hash> to asset URLs in your HTML.
  • WREN negotiates content on the Accept header (file bytes vs JSON node) and sends Vary: Accept. Pass Accept through, as the Worker above does.

Don't

  • Don't proxy private data through the Worker with an API key unless you add your own access control. Anyone who can reach the route can read what the key can read. If the data is meant for everyone, give it a public read rule instead.
  • Don't put an API key in page code. Keys are long-lived and can write.
  • Don't change the org slug without updating UPSTREAM.