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.jsYour 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:
- API endpoints --
POST /sdk/v1/logs,POST /sdk/v1/logs/batch,POST /sdk/v1/identify - Tracking script (optional) -- the
lite.jsfile loaded via script tag
SDK Configuration
Point the SDK at your proxy by setting the baseUrl option.
NPM Package
import { init } from '@kitbase/analytics';
const kitbase = init({
sdkKey: 'YOUR_SDK_KEY',
baseUrl: 'https://your-domain.com/kb',
});Script Tag
<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:
{
"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:
[[redirects]]
from = "/kb/*"
to = "https://ingest.kitbase.dev/:splat"
status = 200
force = trueThen 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:
// 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:
/** @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):
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:
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:
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:
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:
pnpm add http-proxy-middlewareconst 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
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
// 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
// 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
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:
- Add an origin pointing to
ingest.kitbase.dev(HTTPS only). - 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
- Cache policy: CachingDisabled (analytics requests are all
- Attach a CloudFront Function (viewer request) to strip the
/kbprefix, since CloudFront forwards the full path:
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.com→ingest.kitbase.dev— and setbaseUrltohttps://kb.your-domain.com. Because that subdomain is served from your own zone (not aCNAMEpointed atkitbase.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
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;
}// next.config.js
{
source: '/lite.js',
destination: 'https://kitbase.dev/lite.js',
}Then load the script from your domain:
<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
- Tracking Script — the script the proxy serves from your domain.
- Web Analytics overview — how tracking works end to end.