Quickstart
Install the gateway in a Next.js app, wire it to your pages, connect the event sink, and verify with two curl commands. Target: under 30 minutes, no conversation required.
Last updated
Before you start
This walkthrough uses Next.js (Next 16 proxy.ts; the identical code works in a Next ≤15 middleware.ts). You need your product data reachable in-process or via a fast cache: the gateway sits on the edge hot path (p95 < 50ms compute budget), so source resolvers must not do slow origin round-trips.
On a different stack? The config in steps 2–4 is identical everywhere; only the mounting changes. See the Shopify, Node (Express/Fastify), and Cloudflare/edge adapter pages.
Installing with an agent? Start here
Copy this into Claude Code, Cursor, Copilot, or whatever you build with. It covers the serving half of this page, and it front-loads the two things that are easy to get wrong and hard to notice later: passing the fallthrough, and checking every URL you wired rather than the first one that worked.
Install @rebilder/gateway in this repository.
WHAT IT DOES
It is middleware. When an AI agent requests one of our URLs with
`Accept: text/markdown`, it answers with a clean markdown rendering of that
page's facts. Every other request — humans, Googlebot, anything that did not ask
for markdown — falls through to the existing pipeline untouched. Same URL, same
substance, different format.
STEPS
1. Read the README of the installed package before writing anything. Do not
work from memory of this prompt; the API is in
node_modules/@rebilder/gateway/README.md.
2. Add the dependency with the package manager this repo already uses.
3. Create a gateway config exporting a `GatewayConfig`. Its `sources` resolvers
map a URL to our own data. There are five source kinds and every page on
this site is one of them. Three are commerce shaped: `product`, `policies`,
`catalog`. Two are universal: `document` for any other page, such as a
guide, an article, a service, a location or an FAQ, and `collection` for an
index of documents. A site with no catalog wires only the universal two, and
a site with both wires all five. Do not skip a section of the site because
it is not commerce. Wire the adapter for this repo's framework (`/next`,
`/node` for Express and Fastify, `/edge` for Workers, `/shopify`).
4. Pass the existing fallthrough into the adapter rather than calling it
yourself. That is what puts `Vary: Accept` on the HTML response. Without it a
shared cache can serve markdown to a human or stale HTML to an agent, and it
will not show up until it is in production behind a CDN.
5. Add a `match` router to `sources` if you can: one pure, synchronous function
from URL to source kind. It keeps the hot path to a single lookup and it is
what lets the adapter advertise the markdown alternate.
RULES — these are not style preferences
- Never invent a substantive value. Prices, stock, shipping costs, return
windows, policy text and dates on a commerce page, and opening hours,
turnaround times, eligibility rules, what a service includes and whether
booking is required on any other page, are read from our source of truth and
passed through unchanged. If you cannot find where a value lives,
stop and ask me. Do not approximate, do not use a placeholder that looks
real, and do not write an example value into a resolver.
- A resolver returns `null` when the URL is not that kind of page. `null` means
"fall through to HTML", which is always a safe answer.
- Do not change what the HTML pages render. This is additive.
- Do not add a build step, a new service, or a runtime dependency.
WHEN YOU ARE DONE
Tell me which URL patterns you wired and which you deliberately skipped, then
run this against one real URL of EACH kind you wired — not just the first one:
npx rebilder diff <url> # shows the agent view and the browser view
npx rebilder check <url> # grades the page, names what is missing
An incomplete source map is the most common install defect and it is invisible
until an agent hits the page you missed. Both commands run locally, upload
nothing, and need no account.
If you cannot answer "which URLs now serve markdown", the install is not done.It does not wire the event sink: this copy of the prompt is the one published on npm, and it has no site to point at. Do step 4 yourself afterwards, or open Console → Install, where the same prompt is issued with your real site ID and the events section already in it.
Rather do it by hand? The rest of this page is the same install, written out.
1. Install
npm install @rebilder/gateway # pnpm add / yarn addOne package, zero external runtime dependencies. The gateway re-exports every type you need (ProductSource, PolicySource, CatalogItemSource, DocumentSource, CollectionSource, DetectionResult, RebilderEventV0, …), so nothing else is needed to make this compile. Step 4 adds one more name, @rebilder/events, because the event sink is a runtime import rather than a type: it is already resolved in your tree as a dependency of the gateway, and it carries no external runtime dependencies of its own.
2. Wire your source of truth
The gateway renders from your data and never invents content. Give it lookup functions from a URL to whatever that URL is: a product, a policy, a priced listing, a document (any other page, such as a guide, a service or an article), or a collection (an index of documents). Most sites are mostly documents. Here is a complete commerce example, and the Sources page carries the document and collection field tables (in a real site these functions read your database, CMS, or platform cache; the shape is what matters):
// lib/catalog.ts — your source of truth, typed with the gateway's re-exported types
import type { ProductSource, PolicySource } from '@rebilder/gateway'
const PRODUCTS: Record<string, ProductSource> = {
'/products/alpine-trail-pack-28l': {
url: 'https://store.example.com/products/alpine-trail-pack-28l',
title: 'Alpine Trail Pack 28L',
brand: 'Basecamp Supply Co',
description:
'A 28-liter technical daypack: suspended-mesh back panel, magnetic sternum buckle, 3L hydration sleeve, lifetime repair guarantee.',
price: { amount: 14800, currency: 'USD' }, // minor units — integer cents, never floats
availability: 'in_stock', // 'in_stock' | 'out_of_stock' | 'preorder' | 'backorder'
variants: [
{
id: 'BSC-ATP28-JUN',
title: 'Juniper Green',
price: { amount: 14800, currency: 'USD' },
availability: 'in_stock',
options: { Color: 'Juniper Green' },
},
{
id: 'BSC-ATP28-EMB',
title: 'Ember Orange',
price: { amount: 14800, currency: 'USD' },
availability: 'backorder',
options: { Color: 'Ember Orange' },
},
],
shipping: {
summary: 'Free US shipping on orders over $75; standard shipping $6.95.',
freeThreshold: { amount: 7500, currency: 'USD' },
regions: ['US', 'CA'],
etaDays: [3, 6], // [min, max] delivery estimate in days
},
returns: {
summary: '60-day returns. Items must be unused with tags attached.',
windowDays: 60,
url: 'https://store.example.com/policies/returns',
},
images: [
{ url: 'https://store.example.com/cdn/atp28-front.jpg', alt: 'Alpine Trail Pack 28L, front' },
],
attributes: { Capacity: '28 L', Weight: '1.12 kg (2.47 lb)' },
},
}
const POLICIES: PolicySource[] = [
{
title: 'Shipping policy',
url: 'https://store.example.com/policies/shipping',
body: 'Orders placed before 12pm PT ship the same business day. Free US shipping over $75.',
},
{
title: 'Returns policy',
url: 'https://store.example.com/policies/returns',
body: '60-day returns. Items must be unused with tags attached. Refunds to the original payment method.',
},
]
export function getProduct(pathname: string): ProductSource | null {
return PRODUCTS[pathname] ?? null // null = "not a product page" — the gateway moves on
}
export function getPolicies(pathname: string): PolicySource[] | null {
return pathname.startsWith('/policies') ? POLICIES : null
}Returning null means "this URL isn’t mine": the gateway tries the next source, and if nothing matches, the request passes through to your HTML. Money amounts are integer minor units (cents): 14800 renders as $148.00, and a non-integer amount throws rather than silently rounding. The full field reference is on the Sources page.
3. Create the gateway config
// lib/gateway-config.ts — wire the gateway to your source of truth
import type { GatewayConfig } from '@rebilder/gateway'
import { getProduct, getPolicies, getCollection } from './catalog' // your code
import { getDocument, getDocumentIndex } from './content' // your code
export const gatewayConfig: GatewayConfig = {
storeId: 'store_123',
sources: {
// Commerce shaped. Wire the ones you have; a site with no catalog wires none.
product: (url) => getProduct(url.pathname), // null when not a PDP
policies: (url) => getPolicies(url.pathname),
catalog: (url) => getCollection(url.pathname),
// Universal: any other page, and any index of them. Guides, services,
// locations, articles. Most of a site usually lives here.
document: (url) => getDocument(url.pathname),
collection: (url) => getDocumentIndex(url.pathname),
},
onEvent: (event) => { /* queue to your analytics sink; fire-and-forget */ },
}storeIdis stamped on every emitted event, so use the id from your Console once you have one.onEventis where the gateway hands you every request it handled, pass-throughs included. The next step wires it to the sink; the placeholder above is a config that compiles and reports nothing.- Emission is fire-and-forget: the gateway never awaits
onEvent, and it swallows both sync throws and async rejections, so a broken or slow sink can neither block nor break a response. maxBytes(not shown) caps the markdown size; default 5120 bytes.
4. Wire the event sink
Serving markdown and reporting what you served are two different things, and only one of them happens by itself. onEvent is the only route anything takes from your gateway to us: with it unwired, the install works perfectly and your Console stays empty forever, which also means no Agent Miss Report, no coverage numbers, and no answer rate. This is the step people skip.
npm install @rebilder/events # pnpm add / yarn add// lib/gateway-config.ts — the same config, with the event sink wired
import type { GatewayConfig } from '@rebilder/gateway'
import { createHttpEventSink } from '@rebilder/events'
import { getProduct, getPolicies, getCollection } from './catalog' // your code
import { getDocument, getDocumentIndex } from './content' // your code
// Module scope, not per request: the sink batches (20 events, or every 2s),
// so a sink created inside a handler is discarded before it ever flushes.
const sink = createHttpEventSink({
url: 'https://api.rebilder.com',
apiKey: process.env.REBILDER_API_KEY!, // set the env var; never a literal here
})
export const gatewayConfig: GatewayConfig = {
storeId: 'store_123',
sources: {
// Commerce shaped. Wire the ones you have; a site with no catalog wires none.
product: (url) => getProduct(url.pathname), // null when not a PDP
policies: (url) => getPolicies(url.pathname),
catalog: (url) => getCollection(url.pathname),
// Universal: any other page, and any index of them. Guides, services,
// locations, articles. Most of a site usually lives here.
document: (url) => getDocument(url.pathname),
collection: (url) => getDocumentIndex(url.pathname),
},
onEvent: (event) => sink.emit(event),
}- Your real
storeIdand the page that issues an API key are both on Console → Install. The key is read from the environment and never written into this file: it is store-scoped, and the store id on the wire is always resolved from the key rather than from the payload. - Create the sink at module scope, as above. It batches, flushing at 20 events or every 2 seconds, so a sink constructed inside a request handler is discarded before it ever flushes and every event it accepted is lost.
- The queue is capped at 1000 events, and it lives in memory. A runtime that discards the isolate loses whatever was still queued, and nothing retries a lost batch. That is deliberate: the alternative is durable state on the hot path.
- No credentials yet?
createConsoleEventSink()prints the same events as one structured log line each, which a log drain can pick up. The event shape is identical, so swapping in the HTTP sink later changes nothing else. See Events.
5. Add the proxy
// proxy.ts (Next 16) — middleware.ts on Next ≤15 is identical
import { NextResponse } from 'next/server'
import { createGatewayProxy } from '@rebilder/gateway/next'
import { gatewayConfig } from './lib/gateway-config'
// Pass your fallthrough. The proxy then returns a Response for every request,
// and the HTML half of each negotiated URL gets `Vary: Accept` — see below for
// why that matters more than it looks.
const gateway = createGatewayProxy(gatewayConfig, () => NextResponse.next())
export default gateway
export const config = { matcher: ['/products/:path*', '/policies/:path*', '/collections/:path*'] }That’s the whole integration: a Response short-circuits with markdown, null continues to your HTML pipeline unchanged. Scope the matcher to the paths your sources can answer. If you already have a middleware.ts (auth, session refresh), compose with the same ?? pattern: gateway first, your middleware as the fallthrough. The first live integration (trymumm.com) does exactly that.
6. Verify with curl
Prove both audiences get the right thing from the same URL. These are the two commands from the captured demo; against your own deployment, point them at one of your product URLs instead:
# 1. What an agent gets — clean markdown (note x-rebilder-path: markdown)
curl -si -H 'Accept: text/markdown' https://trymumm.com/trust/refunds
# 2. What a browser (or Googlebot) gets — the canonical HTML, unchanged
curl -si https://trymumm.com/trust/refundsWhat you should see
- Command 1:
200withcontent-type: text/markdown; charset=utf-8,vary: Accept, andx-rebilder-path: markdown, followed by front-loaded markdown. - Command 2: your normal HTML, byte-for-byte what you served before installing the gateway.
The markdown body leads with the buying facts (title, price, availability, shipping, returns) in the first screenful. From the captured demo response:
# [Alpine Trail Pack 28L](https://basecamp-supply.example/products/alpine-trail-pack-28l)
- **Brand:** Basecamp Supply Co
- **Price:** $148.00
- **Availability:** In stock
- **Shipping:** Free US shipping on orders over $75; standard shipping $6.95.7. Check every URL you wired, not the first one
Step 6 proves the install works. It does not prove your source map is complete, and an incomplete map is the most common install defect, because it is invisible until an agent hits the page you missed.
npx rebilder diff https://your-store.example/products/some-product
npx rebilder check https://your-store.example/products/some-productdifffetches the URL twice (once as a browser, once as an agent) and shows you both views side by side.checkgrades the page and names what is still missing.- Run them against one URL of each kind you wired (a product, a policy, a collection, your homepage), not just the one you were working on.
- Both run on your machine, upload nothing, and need no account.
Next: the Events reference for the full field list and the sink options you did not need in step 4, and an llms.txt while you’re at it.