Skip to content

Proxy / Custom Domain

Ad blockers and privacy extensions block requests to known analytics domains like ingest.kitbase.dev. You can avoid this by proxying analytics requests through your own domain so they appear as first-party traffic.

Why proxy?

  • Ad blockers -- requests to your own domain are not blocked
  • Content Security Policy -- no need to allowlist a third-party domain
  • Brand consistency -- all traffic stays on your domain
  • Privacy -- analytics data never visibly leaves your infrastructure

How It Works

Browser ──► your-domain.com/kb/* ──► ingest.kitbase.dev/*
Browser ──► your-domain.com/lite.js ──► kitbase.dev/lite.js

Your proxy sits between the browser and the Kitbase API. The SDK sends requests to your proxy URL, and the proxy forwards them to Kitbase. Two things need proxying:

  1. API endpoints -- POST /sdk/v1/logs, POST /sdk/v1/logs/batch, POST /sdk/v1/identify
  2. Tracking script (optional) -- the lite.js file loaded via script tag

SDK Configuration

Point the SDK at your proxy by setting the baseUrl option.

NPM Package

typescript
import { init } from '@kitbase/analytics';

const kitbase = init({
  sdkKey: 'YOUR_SDK_KEY',
  baseUrl: 'https://your-domain.com/kb',
});

Script Tag

html
<script>
  window.KITBASE_CONFIG = {
    sdkKey: 'YOUR_SDK_KEY',
    baseUrl: 'https://your-domain.com/kb',
  };
</script>
<script defer src="https://your-domain.com/lite.js"></script>

The SDK sends all API requests to baseUrl instead of https://ingest.kitbase.dev. The x-sdk-key header is included automatically -- no changes needed on the auth side.

Proxy Setup Examples

Every example below does the same two things: forward your-domain.com/kb/* to ingest.kitbase.dev/*, then point the SDK's baseUrl at /kb. Pick the one that matches your stack — hosting platforms first, then JavaScript frameworks, web servers, PHP, and hosted CMSs. Whichever you choose, follow the header and caching rules in Important Notes.

Vercel

For any project on Vercel (on Next.js, prefer the next.config.js rewrite below). Add vercel.json at the project root:

json
{
  "rewrites": [
    { "source": "/kb/:path*", "destination": "https://ingest.kitbase.dev/:path*" }
  ]
}

Then set baseUrl: '/kb'.

Netlify

Add a redirect to netlify.toml (or public/_redirects). Status 200 makes Netlify proxy the request instead of redirecting:

toml
[[redirects]]
  from = "/kb/*"
  to = "https://ingest.kitbase.dev/:splat"
  status = 200
  force = true

Then set baseUrl: '/kb'. Proxied bytes count toward your Netlify bandwidth.

Cloudflare Pages

A Pages _redirects file can only rewrite to internal assets, so external proxying needs a Pages Function. Create functions/kb/[[path]].js:

js
// functions/kb/[[path]].js
export async function onRequest({ request, params }) {
  const path = (params.path || []).join('/');
  const { search } = new URL(request.url);

  const headers = new Headers(request.headers);
  headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP') || '');

  return fetch(`https://ingest.kitbase.dev/${path}${search}`, {
    method: request.method,
    headers,
    body: request.body,
  });
}

Then set baseUrl: '/kb'.

Next.js (Rewrites)

Next.js rewrites require zero extra dependencies. Add this to your next.config.js:

js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/kb/:path*',
        destination: 'https://ingest.kitbase.dev/:path*',
      },
    ];
  },
};

module.exports = nextConfig;

Then set baseUrl to /kb (relative URL):

typescript
const kitbase = init({
  sdkKey: 'YOUR_SDK_KEY',
  baseUrl: '/kb',
});

Nuxt

Nuxt 3 (Nitro) has built-in route-rule proxying — no extra dependencies. In nuxt.config.ts:

ts
export default defineNuxtConfig({
  routeRules: {
    '/kb/**': { proxy: 'https://ingest.kitbase.dev/**' },
  },
});

Then set baseUrl: '/kb'.

SvelteKit

Add a catch-all server route at src/routes/kb/[...path]/+server.ts:

ts
import type { RequestHandler } from './$types';

const UPSTREAM = 'https://ingest.kitbase.dev';

const proxy: RequestHandler = async ({ request, params, url, getClientAddress }) => {
  const headers = new Headers(request.headers);
  headers.delete('host');           // let fetch set the upstream Host
  headers.delete('content-length'); // recomputed from the forwarded body
  headers.set('X-Forwarded-For', getClientAddress());

  const method = request.method;
  return fetch(`${UPSTREAM}/${params.path}${url.search}`, {
    method,
    headers,
    body: method === 'GET' || method === 'HEAD' ? undefined : await request.text(),
  });
};

export const GET = proxy;
export const POST = proxy;

Then set baseUrl: '/kb'.

Astro

Astro needs an SSR adapter (@astrojs/node, Vercel, Netlify, …). Add a server endpoint at src/pages/kb/[...path].ts:

ts
import type { APIRoute } from 'astro';

export const prerender = false;

const UPSTREAM = 'https://ingest.kitbase.dev';

export const ALL: APIRoute = async ({ request, params, url }) => {
  const headers = new Headers(request.headers);
  headers.delete('host');
  headers.delete('content-length');

  const method = request.method;
  return fetch(`${UPSTREAM}/${params.path}${url.search}`, {
    method,
    headers,
    body: method === 'GET' || method === 'HEAD' ? undefined : await request.text(),
  });
};

Then set baseUrl: '/kb'.

Node.js (Express)

Use http-proxy-middleware to forward requests:

bash
pnpm add http-proxy-middleware
js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

app.use(
  '/kb',
  createProxyMiddleware({
    target: 'https://ingest.kitbase.dev',
    changeOrigin: true,
    pathRewrite: { '^/kb': '' },
  })
);

app.listen(3000);

Nginx

nginx
location /kb/ {
    proxy_pass https://ingest.kitbase.dev/;
    proxy_set_header Host ingest.kitbase.dev;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_ssl_server_name on;
}

Apache

With access to the server config or a <VirtualHost> (requires mod_proxy + mod_proxy_http):

SSLProxyEngine On
ProxyPreserveHost Off

ProxyPass        /kb/  https://ingest.kitbase.dev/
ProxyPassReverse /kb/  https://ingest.kitbase.dev/

ProxyPreserveHost Off sends ingest.kitbase.dev as the upstream Host, which its TLS termination requires.

On shared hosting where you can only edit .htaccess, use the mod_rewrite proxy flag instead — note SSLProxyEngine On must already be set by your host, since it can't live in .htaccess:

RewriteEngine On
RewriteRule ^kb/(.*)$ https://ingest.kitbase.dev/$1 [P,L]

Then set baseUrl: '/kb'.

Caddy

In your Caddyfile:

your-domain.com {
    handle /kb/* {
        uri strip_prefix /kb
        reverse_proxy https://ingest.kitbase.dev {
            header_up Host ingest.kitbase.dev
        }
    }

    # ... the rest of your site
}

Caddy adds X-Forwarded-For automatically. Then set baseUrl: '/kb'.

PHP

A minimal curl-based proxy script:

php
<?php
// proxy.php — place at /kb/proxy.php and rewrite /kb/* to this file
$path = ltrim($_SERVER['PATH_INFO'] ?? '', '/');
$url  = 'https://ingest.kitbase.dev/' . $path;

$headers = [];
foreach (getallheaders() as $key => $value) {
    // Forward relevant headers
    $lower = strtolower($key);
    if (in_array($lower, ['content-type', 'x-sdk-key', 'user-agent'])) {
        $headers[] = "$key: $value";
    }
}

// Forward the client IP
$headers[] = 'X-Forwarded-For: ' . $_SERVER['REMOTE_ADDR'];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input'));
}

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
curl_close($ch);

http_response_code($httpCode);
header('Content-Type: ' . ($contentType ?: 'application/json'));
echo $response;

WordPress

If you control the web server, use the Nginx or Apache config above — it's the most efficient option. On a managed host where you can only add code, drop a must-use plugin at wp-content/mu-plugins/kitbase-proxy.php that streams /kb/* to the API:

php
<?php
// wp-content/mu-plugins/kitbase-proxy.php
add_action('muplugins_loaded', function () {
    $prefix = '/kb/';
    $uri = $_SERVER['REQUEST_URI'] ?? '';
    if (strncmp($uri, $prefix, strlen($prefix)) !== 0) {
        return; // not an analytics request — let WordPress handle it
    }

    $path   = substr(parse_url($uri, PHP_URL_PATH), strlen($prefix));
    $query  = $_SERVER['QUERY_STRING'] ?? '';
    $target = 'https://ingest.kitbase.dev/' . $path . ($query ? "?$query" : '');

    $headers = ['X-Forwarded-For: ' . ($_SERVER['REMOTE_ADDR'] ?? '')];
    if (!empty($_SERVER['CONTENT_TYPE']))    $headers[] = 'Content-Type: ' . $_SERVER['CONTENT_TYPE'];
    if (!empty($_SERVER['HTTP_X_SDK_KEY']))  $headers[] = 'x-sdk-key: ' . $_SERVER['HTTP_X_SDK_KEY'];
    if (!empty($_SERVER['HTTP_USER_AGENT'])) $headers[] = 'User-Agent: ' . $_SERVER['HTTP_USER_AGENT'];

    $ch = curl_init($target);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $_SERVER['REQUEST_METHOD'],
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_POSTFIELDS     => file_get_contents('php://input'),
    ]);
    $body = curl_exec($ch);

    http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
    header('Content-Type: ' . (curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'application/json'));
    curl_close($ch);
    echo $body;
    exit;
});

Then set baseUrl: '/kb'. Because WordPress starts booting on every analytics request, prefer the server-level proxy whenever you can.

Cloudflare Workers

js
export default {
  async fetch(request) {
    const url = new URL(request.url);

    // Rewrite /kb/* → ingest.kitbase.dev/*
    url.hostname = 'ingest.kitbase.dev';
    url.pathname = url.pathname.replace(/^\/kb/, '');

    const headers = new Headers(request.headers);
    headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP'));

    return fetch(url.toString(), {
      method: request.method,
      headers,
      body: request.body,
    });
  },
};

AWS CloudFront

CloudFront needs three pieces of configuration:

  1. Add an origin pointing to ingest.kitbase.dev (HTTPS only).
  2. Add a cache behavior for path pattern /kb/* → that origin, with:
    • Cache policy: CachingDisabled (analytics requests are all POST)
    • Origin request policy: AllViewerExceptHostHeader (forwards the visitor's headers and query string, sends the origin's own Host)
    • Allowed methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
  3. Attach a CloudFront Function (viewer request) to strip the /kb prefix, since CloudFront forwards the full path:
js
function handler(event) {
  var request = event.request;
  request.uri = request.uri.replace(/^\/kb/, '');
  return request;
}

CloudFront adds X-Forwarded-For automatically. Then set baseUrl: '/kb'.

Shopify, Webflow, Wix & other hosted builders

Fully hosted site builders don't give you a reverse proxy or server config, so you can't make analytics requests first-party the way the platforms above do. Two options:

  • Use the standard third-party snippet (kitbase.dev/lite.js + ingest.kitbase.dev). It works everywhere; the only cost is that visitors running aggressive blockers aren't counted.
  • Proxy through a subdomain you control. If your site's DNS is on Cloudflare, run a Worker on a subdomain of your own domain — e.g. kb.your-domain.comingest.kitbase.dev — and set baseUrl to https://kb.your-domain.com. Because that subdomain is served from your own zone (not a CNAME pointed at kitbase.dev), it survives the CNAME-uncloaking that uBlock Origin and Firefox apply to bare-CNAME trackers, and it shares your site's registrable domain, so it reads as first-party. Serve the script from the same subdomain too.

Self-Hosting the Script File

To also proxy the tracking script (lite.js), serve it from your domain. This prevents the script tag itself from being blocked.

Option A: Static file -- download https://kitbase.dev/lite.js and serve it as a static asset. Re-download periodically to pick up SDK updates.

Option B: Rewrite rule -- proxy the request like the API:

nginx
# Nginx
location = /lite.js {
    proxy_pass https://kitbase.dev/lite.js;
    proxy_set_header Host kitbase.dev;
    proxy_ssl_server_name on;
    proxy_cache_valid 200 1h;
}
js
// next.config.js
{
  source: '/lite.js',
  destination: 'https://kitbase.dev/lite.js',
}

Then load the script from your domain:

html
<script defer src="https://your-domain.com/lite.js"></script>

Important Notes

Forward Client IP Headers

Kitbase uses client IP addresses for geolocation enrichment (country, region, city). Your proxy must forward the original client IP so geo data is accurate. The backend reads these headers in order:

  • CF-Connecting-IP (Cloudflare)
  • X-Forwarded-For (standard proxy header)
  • X-Real-IP (Nginx)
  • True-Client-IP (Akamai)

Most reverse proxies add X-Forwarded-For automatically. If yours doesn't, add it explicitly (see the examples above).

Keep the User-Agent Header

The backend parses User-Agent to extract device type, browser, and OS. Make sure your proxy forwards this header unchanged.

Don't Cache POST Responses

The SDK sends analytics data via POST requests. Caching POST responses will cause events to be silently dropped. Only cache GET requests (like lite.js).

Update Your Content Security Policy

If you use CSP headers, update connect-src to allow your proxy path instead of (or in addition to) ingest.kitbase.dev:

script-src 'self';
connect-src 'self';

Since both the script and API calls come from your own domain, no third-party domains need to be allowlisted.

Next steps

Released under the MIT License.