Skip to content

Run the site proxy

The site proxy is code your organization deploys in front of one of its sites. It receives every request for the site’s host and:

  1. serves its own script at /edgekit/proxy.js;
  2. forwards every other /edgekit/ request to your tenant host;
  3. fetches everything else from your site’s own server, which these pages call the upstream;
  4. on HTML pages, asks decide for your paywall feature on the server and, when the outcome does not allow the page, removes the gated part it finds before the page reaches the browser.

Your organization runs its own copy, one per site. EdgeKit does not deploy it, update it or roll it back. The code it runs is createProxy in @edgekitcloud/proxy.

  • A route for the whole host: every request for the site’s host must reach your copy. A narrower route of your own on the same host takes precedence for its paths, which then skip the proxy, and the proxy cannot detect this.
  • Your site’s origin in Allowed origins: the proxy’s decide call carries the page’s origin, and browser requests through /edgekit/ carry theirs. Set it on Delivery › Delivery › Sites, then deploy. See Allow your site’s origins.
  • An analytics route of your own: the proxy forwards /edgekit/a/t to your tenant host with the path unchanged, but the tenant host answers analytics only at /a/t. The event gets 200 with the body edgekit.cloud alive, so the tracker counts it as delivered, and it is lost. Forward /edgekit/a/* yourself, in front of the proxy, as in Route the paths. Or set the tracker’s collectorOrigin to your tenant host and its pathPrefix to an empty string, as in Call the tenant host directly.
  • A deployed feature named paywall on the site. It is the only feature the proxy asks for. If the deployment does not carry it, decide lists it as unknown and the proxy treats every page it decides as gated.
  • The settings below, as environment variables of your copy.

Every setting starts with EDGEKIT_. If a required setting is missing or invalid, the proxy still serves its script and passes every other request to your site unchanged. An invalid optional setting takes its default. Either way the proxy logs one warning per problem when it starts, to your copy’s own log.

Setting Required Default What it does
EDGEKIT_TENANT_HOST Yes Your tenant host, with or without https://. No path
EDGEKIT_SITE_ID Yes The site this copy fronts
EDGEKIT_UPSTREAM_URL Yes Your site’s own server. Only its origin is used; a path is ignored with a warning
EDGEKIT_BODY_SELECTOR No none CSS selector for the gated region
EDGEKIT_PREVIEW_CHILDREN No 2 How many child elements of the gated region a truncated page keeps
EDGEKIT_IGNORE_PATHS No none Regular expression tested against the path. A matching page is never decided
EDGEKIT_ACCESS_DEFAULT No metered The access fact for a page that does not mark itself free
EDGEKIT_SECTION_DEFAULT No empty The section fact for a page that declares none
EDGEKIT_FACT_OVERRIDES No {} JSON object from fact name to source, below
EDGEKIT_BYPASS_BOT_CATEGORIES No ["Search Engine Crawler"] JSON array of verified-bot categories that get the whole page
EDGEKIT_SIGN_IN_URL No none Absolute URL the panel links to for sign-in
EDGEKIT_SUBSCRIBE_URL No none Absolute URL the panel links to for subscribing
EDGEKIT_CLIENT_IP_KEY No none Secret. Signs each user’s IP address for EdgeKit’s rate limits, below

A JSON setting may be a JSON string or a native object or array.

Only a GET answered 200 with content-type: text/html, at most 2 MB (counted in characters), goes through the steps below. Every other response, including redirects, errors, assets and form posts, passes through byte for byte.

Fact Where it comes from
article_id The canonical link, else the article’s mainEntityOfPage or url in the page’s structured data, else the request path. The path only, never the host
sections Every articleSection in the structured data, else the article:section meta tag
section The first of sections, else EDGEKIT_SECTION_DEFAULT
access free when the structured data says isAccessibleForFree is true, else EDGEKIT_ACCESS_DEFAULT
published_at datePublished, else the article:published_time meta tag, as ISO 8601 UTC. Left out when the value found does not parse
page_url The request URL without its query string

EDGEKIT_FACT_OVERRIDES replaces any of these six. Each value is ld:<path> to read a field of the article’s structured data, <selector>@<attribute> to read the first matching element’s attribute, or a literal value.

The proxy holds no content-signing secret, so these facts reach decide unsigned, at the unverified trust level. A canvas that branches on trust level sees unverified.

The proxy posts to /edgekit/v1/decide on your tenant host for the paywall feature, with the facts, the page’s origin, the user’s Cookie, User-Agent, Accept-Language and Referer headers, and the country and IP headers your edge sets. A gift link in the page URL’s _gt parameter is sent too. The call has a 2-second deadline.

It skips decide when the request’s verified-bot category is in EDGEKIT_BYPASS_BOT_CATEGORIES, or when the path matches EDGEKIT_IGNORE_PATHS.

Mode When The page
bot A verified bot in a bypassed category Whole, decide not called, your caching headers kept
allow Decide allowed the page, or the path is ignored Whole
truncated A gated region was found and the outcome does not allow The first EDGEKIT_PREVIEW_CHILDREN child elements of the region are kept and the rest removed
overlay No gated region was found and the outcome does not allow Whole. The panel covers it in the browser, and the gated text stays in the page source
unavailable Decide timed out, could not be reached, answered an error status (such as 403 for an origin not in Allowed origins) or answered something other than a decision Whole, with no panel

The gated region is the first of these that applies:

  1. EDGEKIT_BODY_SELECTOR.
  2. A cssSelector the page’s structured data declares for a part that is not free, if it is a plain .class or #id that occurs in the page.
  3. Elements with the class edgekit-gated.

With none of them, the mode is overlay. A selector the proxy cannot parse also falls back to overlay, with a warning. EDGEKIT_BODY_SELECTOR is used as typed: if it matches nothing on the page, nothing is removed and the panel covers the page as in overlay. In truncated mode, text that sits directly in the region, outside any child element, is not removed.

In one pass over the page, the proxy:

  • adds to <head> a <script type="application/json" id="edgekit-decision"> with the decision, the facts and the mode, and <script type="module" src="/edgekit/proxy.js">;
  • in truncated mode, removes the region’s extra children and leaves an empty <div data-edgekit-panel> where the panel goes;
  • adds decide’s Set-Cookie headers;
  • when decide answered, sets Cache-Control: private, no-store and removes ETag, Last-Modified and Expires, so no shared cache stores one user’s page for another.

It keeps your security headers, such as Content-Security-Policy, Strict-Transport-Security and X-Frame-Options. Its script is served from your own origin, so script-src 'self' allows it. The panel is styled by an inline <style> element, so your style-src, or default-src when there is none, must allow 'unsafe-inline'. Otherwise the panel draws unstyled, and in overlay mode it no longer covers the page.

The script draws a panel in truncated and overlay modes and never decides again. The panel’s copy is built-in English text chosen by the outcome’s id. Its link is the url in the outcome’s value when there is one, else EDGEKIT_SUBSCRIBE_URL or EDGEKIT_SIGN_IN_URL.

What failed What the browser gets
Your tenant host, on an /edgekit/ request 502 with the text EdgeKit is unreachable
Your upstream 502 with the text Bad gateway
Decide, or its 2-second deadline The whole page, in unavailable mode
The rewrite, after decide answered Your upstream’s page, without decide’s cookies or headers

Any status your tenant host answers on an /edgekit/ request, such as a 429, passes through unchanged.

Decide’s browser path allows 200 requests per 10 seconds per client per site. EdgeKit sees requests from your copy, not from your users, so without EDGEKIT_CLIENT_IP_KEY all users of the site can share one client’s budget.

With the key, the proxy signs each user’s IP address on every request it sends to your tenant host, and EdgeKit counts each user on their own. EdgeKit issues the key for one site; ask for it. A signature that does not verify, or is more than 60 seconds old, is ignored, never refused: the request is then counted against the address that connected.