Proxy Guide

Astro Proxy Setup

Configure Astro middleware to proxy Fluxner tracking requests

Astro's middleware feature makes it simple to proxy Fluxner 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:

  1. Enable First-Party Proxy in your Fluxner Site Settings → Privacy & Security, so the visitor IP your proxy forwards in X-Forwarded-For is trusted directly.
  2. Forward all of the browser's request headers, not a hand-picked few. Fluxner's bot detection expects the headers every real browser sends (Accept, Accept-Language, Accept-Encoding, Sec-Fetch-*) — proxies that forward only User-Agent get their events accepted with 200 OK but classified as bot traffic. Use X-Forwarded-For for the visitor IP; X-Real-IP does not survive Fluxner 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 Fluxner endpoints through your Astro site
  • Forward necessary headers for accurate tracking
  • Simple configuration in astro.config.mjs
  • Support all Fluxner features

Prerequisites

  • Astro 2.0 or later (with SSR or hybrid rendering enabled)
  • Your Fluxner instance URL:
    • Cloud hosted: https://fluxner.com
    • Self-hosted: Your instance URL
  • Your Fluxner site ID

This guide requires SSR (Server-Side Rendering) or hybrid mode. Static sites cannot proxy requests server-side. For static sites on platforms like Vercel or Netlify, use their platform-specific proxy configurations.

Implementation

Enable SSR in Astro

Update astro.config.mjs to enable SSR:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server', // or 'hybrid'
  adapter: node({
    mode: 'standalone',
  }),
});

Install the Node adapter if not already installed:

npm install @astrojs/node

Configure Environment Variables

Create or update .env:

# .env
FLUXNER_HOST=https://fluxner.com
# For self-hosted: FLUXNER_HOST=https://analytics.yourcompany.com

Create Middleware

Create src/middleware.ts (or .js for JavaScript):

// src/middleware.ts
import type { MiddlewareHandler } from 'astro';

const FLUXNER_HOST = import.meta.env.FLUXNER_HOST || 'https://fluxner.com';

export const onRequest: MiddlewareHandler = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  // Check if this is an analytics request
  if (url.pathname.startsWith('/analytics/')) {
    return proxyToFluxner(request, url.pathname);
  }

  // Continue with normal request handling
  return next();
};

async function proxyToFluxner(request: Request, pathname: string): Promise<Response> {
  try {
    // Map paths: /analytics/* → /api/*
    let fluxnerPath = pathname.replace('/analytics/', '/api/');

    const fluxnerUrl = `${FLUXNER_HOST}${fluxnerPath}`;

    // Get client IP from headers
    const forwardedFor = request.headers.get('x-forwarded-for');
    const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';

    // Forward request
    const proxyHeaders = new Headers(request.headers);
    proxyHeaders.set('X-Real-IP', clientIp);
    proxyHeaders.set('X-Forwarded-For', clientIp);

    const response = await fetch(fluxnerUrl, {
      method: request.method,
      headers: proxyHeaders,
      body: request.method !== 'GET' ? await request.text() : undefined,
    });

    // Return proxied response
    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: response.headers,
    });
  } catch (error) {
    console.error('Fluxner proxy error:', error);
    return new Response('Analytics proxy error', { status: 500 });
  }
}

For JavaScript projects:

// src/middleware.js
const FLUXNER_HOST = import.meta.env.FLUXNER_HOST || 'https://fluxner.com';

export const onRequest = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  if (url.pathname.startsWith('/analytics/')) {
    return proxyToFluxner(request, url.pathname);
  }

  return next();
};

async function proxyToFluxner(request, pathname) {
  try {
    // Map paths: /analytics/* → /api/*
    let fluxnerPath = pathname.replace('/analytics/', '/api/');

    const fluxnerUrl = `${FLUXNER_HOST}${fluxnerPath}`;
    const forwardedFor = request.headers.get('x-forwarded-for');
    const clientIp = forwardedFor ? forwardedFor.split(',')[0].trim() : '127.0.0.1';

    const proxyHeaders = new Headers(request.headers);
    proxyHeaders.set('X-Real-IP', clientIp);
    proxyHeaders.set('X-Forwarded-For', clientIp);

    const response = await fetch(fluxnerUrl, {
      method: request.method,
      headers: proxyHeaders,
      body: request.method !== 'GET' ? await request.text() : undefined,
    });

    return new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: response.headers,
    });
  } catch (error) {
    console.error('Fluxner proxy error:', error);
    return new Response('Analytics proxy error', { status: 500 });
  }
}

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>

    <!-- Fluxner Analytics -->
    <script src="/analytics/script.js" async data-site-id="YOUR_SITE_ID"></script>
  </head>
  <body>
    <slot />
  </body>
</html>

Start Development Server

npm run dev

Verify the Setup

  1. Open your site in a browser with Developer Tools
  2. Check Network tab: Requests should go to /analytics/*
  3. Verify in Fluxner dashboard: Data should appear

How It Works

Astro middleware intercepts requests before they're processed:

  1. Request to /analytics/script.js enters middleware
  2. Middleware proxies to https://fluxner.com/api/script.js
  3. Client IP headers are preserved
  4. 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 proxyToFluxner(request: Request, pathname: string): Promise<Response> {
  // 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: MiddlewareHandler = async (context, next) => {
  const { request } = context;
  const url = new URL(request.url);

  // Only proxy in production
  if (import.meta.env.PROD && url.pathname.startsWith('/analytics/')) {
    return proxyToFluxner(request, url.pathname);
  }

  return next();
};

Error Logging

Add detailed error logging:

async function proxyToFluxner(request: Request, pathname: string): Promise<Response> {
  try {
    // ... proxy logic
  } catch (error) {
    const errorDetails = {
      message: error instanceof Error ? error.message : 'Unknown error',
      pathname,
      method: request.method,
      timestamp: new Date().toISOString(),
    };

    console.error('Fluxner 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:

  1. Ensure SSR or hybrid mode is enabled in astro.config.mjs
  2. Verify middleware file is in src/middleware.ts (not in subdirectory)
  3. 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: FLUXNER_HOST is undefined.

Solution:

  1. Check .env file is in project root
  2. Use import.meta.env.FLUXNER_HOST (not process.env)
  3. 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/serverless';

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/functions';

export default defineConfig({
  output: 'server',
  adapter: netlify(),
});

Node.js Server

For self-hosting:

npm run build
node ./dist/server/entry.mjs

On this page