# [Quickstart](https://rebilder.com/docs/quickstart)

> Install the gateway in Next.js, connect your source data and request reporting, then verify browser and agent responses.

- **Updated:** 2026-08-12
- **Author:** Rebilder
- **Section:** Getting started
- **Description:** Install the gateway in Next.js, connect your source data and request reporting, then verify browser and agent responses.
- **Publisher:** Rebilder

## 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](/docs/adapters/shopify), [Node (Express/Fastify)](/docs/adapters/node), and [Cloudflare/edge](/docs/adapters/cloudflare) 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.

give this to your agent

```
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.

IMPLEMENTATION REQUIREMENTS
- 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,
  leave that field unresolved, continue wiring supported fields, and report the missing source. Ask only when that missing information blocks the requested behavior. 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.
- Prefer the existing build and runtime. Add a build step, service or dependency when it solves a concrete integration need; explain the tradeoff and verify compatibility, bundle impact and self-hosting.

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

Check source coverage across the URL patterns you intend to serve. Both commands run locally, upload
nothing, and need no account.

If you cannot answer "which URLs now serve markdown", the install is not done.
```

> **Connect real source data** The install prompt asks your coding agent to locate existing data sources and wire supported fields. If a source is missing, it should report the gap and continue independent work rather than invent a value. Review the integration diff before merging.

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](/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

terminal

```
npm install @rebilder/gateway        # pnpm add / yarn add
```

One 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](/docs/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

```
// 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](/docs/sources) page.

## 3. Create the gateway config

lib/gateway-config.ts

```
// 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 */ },
}
```

- `storeId` is stamped on every emitted event, so use the id from your [Console](/console) once you have one.
- `onEvent` is 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: connect it to populate your [Console](/console), Agent Miss Report, coverage numbers and answer rate.

terminal

```
npm install @rebilder/events        # pnpm add / yarn add
```

lib/gateway-config.ts

```
// 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 `storeId` and the page that issues an API key are both on [Console → Install](/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 in-memory queue holds up to 1000 events. Connect `onError` and use platform-appropriate flushing to monitor delivery; events still queued when a runtime ends can be lost.
- 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](/docs/events).

> **Reporting runs separately from serving** Event delivery runs independently of page serving. `emit()` never throws, `flush()` and `close()` never reject, and failures are reported to `onError` and then dropped. If our ingest is down, your pages serve exactly as before and the events for that window are simply gone.

## 5. Add the proxy

proxy.ts

```
// 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:

terminal

```
# 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/refunds
```

## What you should see

- Command 1: `200` with `content-type: text/markdown; charset=utf-8`, `vary: Accept`, and `x-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:

response body (excerpt)

```
# [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.
```

> **Getting HTML where you expected markdown?** Work through [Troubleshooting: an agent gets HTML](/docs/troubleshooting#agent-gets-html). The three usual causes: the matcher doesn’t cover the path, the source returned `null` for that exact pathname, or the requester classified as a crawler, which is correct behavior, never a bug.

## 7. Check every URL you wired, not the first one

Verify each source type and important route. A successful check on one URL confirms that route; a broader check finds missing source mappings.

> **Check your homepage too** Include your homepage as well as product, policy and collection pages in verification. A successful response on one route confirms that route; checking each source type gives you broader coverage.

terminal

```
npx rebilder diff https://your-store.example/products/some-product
npx rebilder check https://your-store.example/products/some-product
```

- `diff` fetches the URL twice (once as a browser, once as an agent) and shows you both views side by side.
- `check` grades 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](/docs/events) reference for the full field list and the sink options you did not need in step 4, and an [llms.txt](/docs/llms-txt) while you’re at it.