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

> Install the gateway in a Next.js app, wire it to a product catalog, connect the event sink, and verify with two curl commands. Target: under 30 minutes, no conversation required.

- **Updated:** 2026-08-12
- **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. 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 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 price 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 tells your agent to ask, not guess** Nobody outside your team knows where your prices live. An agent that writes a plausible-looking price into a resolver has produced the exact failure this package exists to prevent, so the prompt instructs it to stop and ask you instead. Read the diff before you merge it, the same as any other change.

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`, `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 your catalog. Here is a complete, realistic example (in a real store 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

export const gatewayConfig: GatewayConfig = {
  storeId: 'store_123',
  sources: {
    product:  (url) => getProduct(url.pathname),      // null when not a PDP
    policies: (url) => getPolicies(url.pathname),
    catalog:  (url) => getCollection(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: with it unwired, the install works perfectly and your [Console](/console) stays empty forever, which also means no Agent Miss Report, no coverage numbers, and no answer rate. This is the step people skip.

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

// 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: {
    product:  (url) => getProduct(url.pathname),      // null when not a PDP
    policies: (url) => getPolicies(url.pathname),
    catalog:  (url) => getCollection(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 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](/docs/events).

> **A dead sink cannot take your site down** This is the one guarantee that makes the step safe to do on a Friday. `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

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.

> **We shipped this exact bug** rebilder.com ran the gateway for months with its docs, help, guides and pricing all serving markdown correctly, and its **homepage** never wired at all. The most-linked URL on the site, from the company that sells the fix. Nobody caught it because the one URL anyone tested worked.

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.