Building blocks

Frameworks

1DB works with whatever your site is built with, because every connection ends in the same HTTP request. What changes is where that request is made: the page, your server, or 1DB’s hosted form.

Pick your stack

Find how your site is built, then use the way in the third column. Sending from a server keeps the key out of the page and lets you pass the visitor’s real IP on.

Your siteSend leads withKey in the pageRead

Static sites

Plain HTML, Astro, Hugo, Eleventy, Jekyll

The hosted form script, a fetch from the page, or a function on your hostOnly without a function

Next.js

App Router, Pages Router

A server action or route handlerNo

React apps

Vite, Create React App, any single-page app

Your own API route or serverless function; the page posts to itNo

Full-stack frameworks

TanStack Start, React Router, Nuxt, SvelteKit, Astro with an adapter

A server function or endpoint, the same shape as Next.jsNo

Cloudflare

Workers, Pages Functions

A Worker or Pages Function that adds the key and forwards the requestNo

Any backend

Rails, Django, Laravel, Express, Go, PHP

One HTTP POST from your form handlerNo

Site builders

Builders that let you add a script tag

The hosted form script, popup, slide-in or inlineYes, and that’s safe

The request every framework sends

However the site is built, this is what reaches 1DB. Put the three values in environment variables; the website’s Integrate tab and npx @1db/cli init both give you them under these names. Set VISITOR_IP_HEADER yourself, from the host table below: the route refuses to start without it.

app/api/lead/route.ts
// The one header YOUR host sets to the visitor's IP (table below): x-real-ip on Vercel,
// cf-connecting-ip on Cloudflare, x-nf-client-connection-ip on Netlify. Never fall back to
// another: a visitor can send any host's header.
const IP_HEADER = process.env.VISITOR_IP_HEADER;
if (!IP_HEADER) throw new Error("Set VISITOR_IP_HEADER to your host's visitor IP header");

// A server route the page posts its form to, as JSON
export async function POST(request: Request) {
  // The page makes submission_id once, when the form loads (crypto.randomUUID())
  const body: unknown = await request.json().catch(() => null);
  const fields = ["submission_id", "email", "message"] as const;
  if (!body || typeof body !== "object" || fields.some((f) => typeof (body as Record<string, unknown>)[f] !== "string")) {
    return Response.json({ error: "Send submission_id, email and message as JSON strings" }, { status: 400 });
  }
  const { submission_id, email, message } = body as Record<(typeof fields)[number], string>;

  const visitorIp = request.headers.get(IP_HEADER);
  if (!visitorIp) console.warn(`No ${IP_HEADER} header: is VISITOR_IP_HEADER right for this host?`);
  const visitorUserAgent = request.headers.get("user-agent");

  let res: Response;
  try {
    res = await fetch(`${process.env.DB_API_ENDPOINT}/v1/leads`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": process.env.DB_API_KEY!,
        "X-1DB-Contract": "2",
        // Sent only when known: without the IP, 1DB records your server's address and that
        // lead shares your server's rate limit
        ...(visitorIp ? { "X-1DB-Client-IP": visitorIp } : {}),
        ...(visitorUserAgent ? { "X-1DB-Client-UA": visitorUserAgent } : {}),
      },
      body: JSON.stringify({ formSlug: process.env.DB_FORM_SLUG, submission_id, email, message }),
      signal: AbortSignal.timeout(10_000),
    });
  } catch {
    // 1DB wasn't reached in 10 seconds. The page keeps its submission_id, so resending can't store the lead twice.
    return Response.json({ error: "Couldn't send the form. Try again." }, { status: 503 });
  }

  // 200: stored. 400 with `fields`: the visitor can fix those fields (`suggestions` may offer
  // fixes), so pass them back. Anything else (a wrong key or form slug, 1DB down) is yours to
  // fix: log it, and the page gets a plain error.
  const answer = await res.json().catch(() => null);
  if (res.ok) return Response.json({ ok: true });
  if (res.status === 400 && answer?.fields) {
    const error = typeof answer.error === "string" ? answer.error : "Check the highlighted fields.";
    return Response.json({ error, fields: answer.fields, suggestions: answer.suggestions }, { status: 400 });
  }
  console.error("1DB answered", res.status, answer);
  return Response.json({ error: "Couldn't send the form. Try again." }, { status: 502 });
}

Passing the visitor on

A request from your server carries your server’s address. Pass the visitor’s IP and browser in the two X-1DB-Client-* headers, or every lead looks like it came from the same data center and shares one rate limit.

Read the header your host sets itself, named in VISITOR_IP_HEADER so each deployment picks its own. Don’t take the first entry of x-forwarded-for on a host that appends to it rather than replacing it: a visitor can put any address there. Behind your own proxy, use the last entry, the one your proxy added.

HostVisitor IP header
Cloudflare Workers and Pagescf-connecting-ip
Vercelx-real-ip
Netlifyx-nf-client-connection-ip

Is the key safe in the page?

Yes. An ingest key can only send leads to its own website: it can’t read, change or delete anything, and requests are rate-limited per visitor. The worst anyone can do with it is send you junk, the same as filling in the form by hand. Revoke it in the dashboard if that happens.

Building a new site

When you add a site that isn’t live yet, 1DB asks which framework and host you’ll use and suggests pairings that work well together, like Next.js on Vercel or Astro on Cloudflare. That choice goes into the build prompt 1DB writes for your coding agent, so the site is built with its form already wired to 1DB.