Documentation menu

Quickstart

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

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–3 is identical everywhere; only the mounting changes. See the Shopify, Node (Express/Fastify), and Cloudflare/edge adapter pages.

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 gets installed.

2. Wire your source of truth

The gateway renders from your data — it 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 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 — use the id from your Console once you have one.
  • onEvent is optional and fire-and-forget: the gateway never awaits it, and a broken sink can neither block nor break a response. Wire it to the HTTP sink later — see Events.
  • maxBytes (not shown) caps the markdown size; default 5120 bytes.

4. 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'

const gateway = createGatewayProxy(gatewayConfig)

export default async function proxy(req: Request) {
  return (await gateway(req)) ?? NextResponse.next()
}

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.

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

Next: wire events so visits show up in your Console, and serve an llms.txt while you’re at it.