Astro Proxy Setup
Configure Astro middleware to proxy Rybbit tracking requests
Astro's middleware feature makes it simple to proxy Rybbit tracking. This guide shows how to set up server-side request proxying in your Astro application.
Two settings make or break a first-party proxy:
- Enable First-Party Proxy in your Rybbit Site Settings → Privacy & Security, so the visitor IP your proxy forwards in
X-Forwarded-Foris trusted directly. - Forward all of the browser's request headers, not a hand-picked few. Rybbit's bot detection expects the headers every real browser sends (
Accept,Accept-Language,Accept-Encoding,Sec-Fetch-*) — proxies that forward onlyUser-Agentget their events accepted with200 OKbut classified as bot traffic. UseX-Forwarded-Forfor the visitor IP;X-Real-IPdoes not survive Rybbit Cloud's edge.
Overview
Astro middleware allows you to intercept and modify requests before they're handled, making it perfect for creating a transparent analytics proxy.
What you'll achieve:
- Proxy Rybbit endpoints through your Astro site
- Forward necessary headers for accurate tracking
- Simple configuration in
astro.config.mjs - Support all Rybbit features
Prerequisites
- Astro 5 or later, with a server adapter (examples below use
@astrojs/node) - Your Rybbit instance URL:
- Cloud hosted:
https://app.rybbit.io - Self-hosted: Your instance URL
- Cloud hosted:
- Your Rybbit site ID
This guide requires on-demand (server) rendering with an adapter. Static sites cannot proxy requests server-side. For static sites on platforms like Vercel or Netlify, use their platform-specific proxy configurations.
Implementation
Enable Server Rendering in Astro
Update astro.config.mjs to render on demand:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({
mode: 'standalone',
}),
});Astro 5 removed output: 'hybrid'. Use output: 'server' so the middleware runs on every request, including /analytics/* paths that have no page of their own.
Install the Node adapter if not already installed:
npm install @astrojs/nodeConfigure Environment Variables
Create or update .env:
# .env
RYBBIT_HOST=https://app.rybbit.io
# For self-hosted: RYBBIT_HOST=https://analytics.yourcompany.comCreate Middleware
Create src/middleware.ts (or .js for JavaScript):
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
const RYBBIT_HOST = import.meta.env.RYBBIT_HOST || 'https://app.rybbit.io';
// Proxies every /analytics/* route (script.js, replay.js, track, identify,
// site/tracking-config/:siteId, site/:siteId/feature-flags/evaluate,
// session-replay/record/:siteId) to Rybbit's /api/*.
export const onRequest = defineMiddleware(async (context, next) => {
const url = new URL(context.request.url);
if (url.pathname.startsWith('/analytics/')) {
// clientAddress is the visitor IP as your adapter reports it
return proxyToRybbit(context.request, url, context.clientAddress);
}
return next();
});
async function proxyToRybbit(request: Request, url: URL, clientIp: string): Promise<Response> {
try {
// Map paths: /analytics/* → /api/*, keeping the query string
const rybbitUrl = `${RYBBIT_HOST}${url.pathname.replace('/analytics/', '/api/')}${url.search}`;
// Forward the browser's headers and set the visitor IP
const proxyHeaders = new Headers(request.headers);
proxyHeaders.delete('host');
proxyHeaders.set('X-Forwarded-For', clientIp);
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
const response = await fetch(rybbitUrl, {
method: request.method,
headers: proxyHeaders,
body: hasBody ? await request.text() : undefined,
});
// fetch() already decoded the body, so drop the upstream encoding headers
const headers = new Headers(response.headers);
headers.delete('content-encoding');
headers.delete('content-length');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
} catch (error) {
console.error('Rybbit proxy error:', error);
return new Response('Analytics proxy error', { status: 500 });
}
}For JavaScript projects, use the same code in src/middleware.js without the type annotations.
Don't fall back to a fixed address such as 127.0.0.1 when you can't find the visitor IP. With First-Party Proxy enabled, Rybbit trusts the X-Forwarded-For you send, so a constant fallback makes every visitor share one IP: one location and visitors merged together. context.clientAddress comes from your adapter; if your Astro server sits behind another reverse proxy, configure the adapter or that proxy so clientAddress is the visitor's address, not the proxy's.
If the feature flag route isn't proxied, rybbit.flag() silently returns your fallback; the /analytics/ prefix match above already covers it. Web vitals collection is bundled into script.js; replay.js is only loaded when session replay is enabled for the site.
Update Your Layout
Add the tracking script to your layout:
---
// src/layouts/Layout.astro
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<!-- Rybbit Analytics -->
<script is:inline src="/analytics/script.js?siteId=YOUR_SITE_ID" defer></script>
</head>
<body>
<slot />
</body>
</html>is:inline stops Astro from bundling the tag, so it stays a plain external script. Older snippets that pass the ID as data-site-id still work.
Keep the proxied file named script.js. The script cuts its own URL at /script.js to find where to send events, so a renamed file (for example /analytics/a.js) sends every request to the wrong place.
Start Development Server
npm run devVerify the Setup
- Open your site in a browser with Developer Tools
- Check Network tab: Requests should go to
/analytics/* - Verify in Rybbit dashboard: Data should appear
How It Works
Astro middleware intercepts requests before they're processed:
- Request to
/analytics/script.jsenters middleware - Middleware proxies to
https://app.rybbit.io/api/script.js - The visitor IP from
context.clientAddressis sent inX-Forwarded-For - Response is returned to browser
Advanced Configuration
Caching with Astro
Add caching for scripts:
// src/middleware.ts
const cache = new Map<string, { response: Response; timestamp: number }>();
const CACHE_TTL = 3600000; // 1 hour in milliseconds
async function proxyToRybbit(request: Request, url: URL, clientIp: string): Promise<Response> {
const pathname = url.pathname;
// Cache GET requests for scripts
if (request.method === 'GET' && pathname.endsWith('.js')) {
const cached = cache.get(pathname);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.response.clone();
}
}
// ... proxy logic
// Cache the response
if (request.method === 'GET' && pathname.endsWith('.js')) {
cache.set(pathname, {
response: response.clone(),
timestamp: Date.now(),
});
}
return response;
}Conditional Proxying
Only proxy in production:
export const onRequest = defineMiddleware(async (context, next) => {
const url = new URL(context.request.url);
// Only proxy in production
if (import.meta.env.PROD && url.pathname.startsWith('/analytics/')) {
return proxyToRybbit(context.request, url, context.clientAddress);
}
return next();
});Error Logging
Add detailed error logging:
async function proxyToRybbit(request: Request, url: URL, clientIp: string): Promise<Response> {
try {
// ... proxy logic
} catch (error) {
const errorDetails = {
message: error instanceof Error ? error.message : 'Unknown error',
pathname: url.pathname,
method: request.method,
timestamp: new Date().toISOString(),
};
console.error('Rybbit proxy error:', errorDetails);
// Return error response
return new Response(JSON.stringify({ error: 'Analytics proxy error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}Troubleshooting
Middleware not running
Problem: Requests bypass middleware.
Solution:
- Ensure
output: 'server'and an adapter are set inastro.config.mjs - Verify middleware file is in
src/middleware.ts(not in subdirectory) - Check adapter is installed:
npm install @astrojs/node
Static build issues
Problem: Build fails or proxy doesn't work in production.
Solution: Middleware requires server-side rendering. Ensure:
// astro.config.mjs
export default defineConfig({
output: 'server', // Required for middleware
adapter: node({
mode: 'standalone',
}),
});Environment variables not loading
Problem: RYBBIT_HOST is undefined.
Solution:
- Check
.envfile is in project root - Use
import.meta.env.RYBBIT_HOST(notprocess.env) - For production, set environment variables in your hosting platform
Deployment
Vercel
Deploy with Vercel adapter:
npm install @astrojs/vercel// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
});Netlify
Deploy with Netlify adapter:
npm install @astrojs/netlify// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify(),
});Node.js Server
For self-hosting:
npm run build
node ./dist/server/entry.mjsRelated Resources
- Astro Integration Guide - General Astro integration
- Tracking Script Documentation - Script configuration
- Astro Middleware - Official Astro docs