AdvancedProduction Ready

Proxy Pattern

Scale to millions of frontend users with a single SSE connection per server replica. Keep your API key secure and get real-time updates without per-user connections.

Proxy Architecture: Browser connects to your backend proxy, which maintains a single SSE connection to Rollgate API

Why Use This Pattern?

🛡 Security

Server API key never exposed to clients. Frontend uses your proxy, not Rollgate directly.

📈 Scalability

3 replicas = 3 SSE connections. 100K users = still only 3 connections.

💾 Resilience

Built-in caching means your app works even if Rollgate is temporarily unavailable.

💰 Cost Effective

A ~6/month server handles 10K+ requests/second with this pattern.

Real-World Example: SaaS Platform with 2M Users

Scenario

A B2B SaaS platform serves 2 million monthly active users across 500+ enterprise clients. They use feature flags for gradual rollouts, A/B testing, and per-client feature customization.

Problem

Direct SDK connections from 2M users would require 2M SSE connections or massive polling load. This would cost $$$$ with most feature flag providers (MTU-based pricing) and strain their infrastructure.

Solution

Deploy the proxy pattern with 6 backend replicas behind a load balancer. Each replica maintains one SSE connection to Rollgate. Total: 6 connections serve 2M users.

Scalability: 2M users connect to Load Balancer, which distributes to 6 replicas, each with 1 SSE connection to Rollgate

Result

  • Cost: No per-user pricing. Pay for what you use, not how many users you have.
  • Latency: Sub-millisecond flag checks (in-memory cache)
  • Reliability: Circuit breaker ensures 100% uptime even during Rollgate maintenance
  • Security: Server API key never exposed to clients

Tested Performance

MetricSSE DirectPolling
Concurrent connections70,000+10,000 VUs
Error rate0%0%
API Memory~470 MiB~658 MiB
ThroughputReal-time push4,629 req/s

Implementation

1. Create the Proxy Server

server.ts
import express from 'express';
import cors from 'cors';
import { RollgateClient } from '@rollgate/sdk-node';

const PORT = process.env.PORT || 3001;

// Single Rollgate connection for all users
const rollgate = new RollgateClient({
  apiKey: process.env.ROLLGATE_API_KEY!,
  baseUrl: process.env.ROLLGATE_BASE_URL || 'https://api.rollgate.io',
  enableStreaming: true, // Use SSE for real-time
  refreshInterval: 30000, // Fallback polling
});

const app = express();
app.use(cors());
app.use(express.json());

// Get all flags (cached from SSE)
app.get('/api/flags', (req, res) => {
  res.json({
    flags: rollgate.getAllFlags(),
    _meta: { cachedAt: new Date().toISOString() },
  });
});

// Check single flag
app.get('/api/flags/:key', (req, res) => {
  const enabled = rollgate.isEnabled(req.params.key, false);
  res.json({ key: req.params.key, enabled });
});

// SSE endpoint for real-time updates to clients
const sseClients = new Set();
app.get('/api/stream', (req, res) => {
  res.set({
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });
  res.flushHeaders();

  // Send initial flags
  res.write(`event: flags\ndata: ${JSON.stringify(rollgate.getAllFlags())}\n\n`);
  sseClients.add(res);

  req.on('close', () => sseClients.delete(res));
});

// Broadcast updates when flags change
rollgate.on('flags-updated', (flags) => {
  const data = `event: flags\ndata: ${JSON.stringify(flags)}\n\n`;
  sseClients.forEach(client => client.write(data));
});

// Health check with circuit breaker status
app.get('/api/health', (req, res) => {
  const circuitState = rollgate.getCircuitState();
  res.json({
    status: circuitState === 'open' ? 'degraded' : 'healthy',
    circuit: circuitState,
    cache: rollgate.getCacheStats(),
  });
});

// Prometheus metrics
app.get('/api/metrics', (req, res) => {
  res.set('Content-Type', 'text/plain');
  res.send(rollgate.getPrometheusMetrics('rollgate_proxy'));
});

// Start server
async function start() {
  await rollgate.init();

  rollgate.on('flags-updated', (flags) => {
    console.log('[Proxy] Flags updated:', Object.keys(flags).length);
  });

  app.listen(PORT, () => {
    console.log(`[Proxy] Running on port ${PORT}`);
  });
}

start();

2. Use from Frontend

Choose between polling (simple) or SSE (real-time) for your frontend:

Option A: Polling (Simple)

React Hook - Polling
function useFlags() {
  const [flags, setFlags] = useState({});

  useEffect(() => {
    fetch('/api/flags')
      .then(res => res.json())
      .then(data => setFlags(data.flags));
  }, []);

  return flags;
}

Option B: SSE (Real-time updates)

React Hook - SSE
function useFlags() {
  const [flags, setFlags] = useState({});

  useEffect(() => {
    const eventSource = new EventSource('/api/stream');

    eventSource.addEventListener('flags', (e) => {
      setFlags(JSON.parse(e.data));
    });

    return () => eventSource.close();
  }, []);

  return flags;
}
Usage
function App() {
  const flags = useFlags();

  if (flags['new-checkout']) {
    return <NewCheckout />;
  }
  return <OldCheckout />;
}

3. Docker Deployment

docker-compose.yml
version: "3.8"

services:
  rollgate-proxy:
    build: .
    ports:
      - "3001:3001"
    environment:
      - ROLLGATE_API_KEY=${ROLLGATE_API_KEY}
      - ROLLGATE_BASE_URL=https://api.rollgate.io
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
      interval: 30s
      timeout: 3s