ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop on the web portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
    CORSEnvironment VariablesBranch-Based DeploymentsTestingTroubleshootingGitOps vs TerraformCustom Code
    Local Development
    Guides
      Advanced Path MatchingAPI VersioningOpenAPI Server URLsConvert URLs to OpenAPIOpenAPI Extension DataFormat Validation WarningsPath Modification ScriptsOpenAPI OverlaysCanary Routing for EmployeesGeolocation Backend RoutingUser-Based Backend RoutingTransform Route ParametersBypass a PolicyTesting GraphQL QueriesHealth ChecksPerformance TestingTroubleshooting Slow ResponsesNon-Standard PortsHandling FormDataS3 Signed URL UploadsCheck IP AddressLazy Load ConfigurationSharing Code Across ProjectsBackstage IntegrationGitHub Action Automation
Policies
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Guides

Route Employees to Canary or Staging Backends

This guide explains how to create a Zuplo policy that routes employee requests to canary or staging backends for testing and dogfooding purposes.

Overview

When releasing new API versions, it's common to route internal employees or beta testers to a canary or staging environment before rolling out to all users. This allows teams to test new features and catch issues early without affecting production traffic.

How It Works

The policy checks for staging environment indicators in this order:

  1. Query parameter: ?stage=canary (defaults to release)
  2. Request header: x-stage
  3. User identity: Employee email/ID in allow list

If any condition is met, the request routes to canary backends.

Creating a Canary Routing Policy

The policy doesn't set the backend URL directly. Instead, it stores the chosen backend on context.custom, and the route's handler reads that value to forward the request (see "Adding the Policy to Routes" below).

Create a new policy file in your project:

Code
// policies/canary-routing.ts import { InboundPolicyHandler, ZuploRequest, environment, } from "@zuplo/runtime"; export const canaryRoutingPolicy: InboundPolicyHandler = async ( request, context, ) => { // Get canary users from environment variable const CANARY_USERS = environment.CANARY_USERS ? environment.CANARY_USERS.split(",").map((user) => user.trim()) : []; // Check for staging indicators const url = new URL(request.url); const stageParam = url.searchParams.get("stage"); const stageHeader = request.headers.get("x-stage"); const canaryUser = request.user?.sub && CANARY_USERS.includes(request.user.sub); // Determine if we should route to canary const isCanary = stageParam === "canary" || stageHeader === "canary" || canaryUser; // Store the backend URL for the route's handler to use if (isCanary) { context.custom.backendUrl = environment.API_URL_CANARY; // Log canary routing for debugging context.log.info("Routing to canary backend", { reason: stageHeader ? "header" : canaryUser ? "user" : "query", user: request.user?.sub, stage: stageParam || stageHeader || "canary", }); } else { context.custom.backendUrl = environment.API_URL_PRODUCTION; } // Remove stage query parameter to avoid passing it to backend if (stageParam) { url.searchParams.delete("stage"); return new ZuploRequest(url.toString(), { method: request.method, headers: request.headers, body: request.body, user: request.user, params: request.params, }); } return request; };

Advanced: Multiple Backend Support

For applications with multiple backend services, extend the policy to route each service appropriately:

Code
// policies/multi-service-canary-routing.ts import { InboundPolicyHandler, ZuploRequest, environment, } from "@zuplo/runtime"; export const multiServiceCanaryRoutingPolicy: InboundPolicyHandler = async ( request, context, ) => { const CANARY_USERS = environment.CANARY_USERS ? environment.CANARY_USERS.split(",").map((user) => user.trim()) : []; // Check staging conditions const url = new URL(request.url); const stageParam = url.searchParams.get("stage"); const stageHeader = request.headers.get("x-stage"); const canaryUser = request.user?.sub && CANARY_USERS.includes(request.user.sub); // Determine if we should route to canary const isCanary = stageParam === "canary" || stageHeader === "canary" || canaryUser; // Route multiple services based on stage if (isCanary) { // Store multiple canary URLs in context for different services context.custom.userApiUrl = environment.USER_API_CANARY; context.custom.orderApiUrl = environment.ORDER_API_CANARY; context.custom.inventoryApiUrl = environment.INVENTORY_API_CANARY; // Add stage indicator header for downstream services request.headers.set("X-Stage", "canary"); } else { // Production URLs (release stage) context.custom.userApiUrl = environment.USER_API_PRODUCTION; context.custom.orderApiUrl = environment.ORDER_API_PRODUCTION; context.custom.inventoryApiUrl = environment.INVENTORY_API_PRODUCTION; request.headers.set("X-Stage", "release"); } // Clean up query parameter if (stageParam) { url.searchParams.delete("stage"); return new ZuploRequest(url.toString(), { method: request.method, headers: request.headers, body: request.body, user: request.user, params: request.params, }); } return request; };

Each service's route then reads the matching value in its handler options. For example, the user API route would use a URL Forward handler with "baseUrl": "${context.custom.userApiUrl}".

Percentage-Based Canary Routing

Roll out canary deployments gradually with percentage-based routing:

Code
// policies/percentage-canary-routing.ts import { InboundPolicyHandler, environment } from "@zuplo/runtime"; export const percentageCanaryRoutingPolicy: InboundPolicyHandler = async ( request, context, ) => { // Always route configured users to canary const CANARY_USERS = environment.CANARY_USERS ? environment.CANARY_USERS.split(",").map((user) => user.trim()) : []; if (request.user?.sub && CANARY_USERS.includes(request.user.sub)) { context.custom.backendUrl = environment.API_URL_CANARY; return request; } // Route percentage of anonymous traffic to canary const CANARY_PERCENTAGE = parseInt(environment.CANARY_PERCENTAGE || "0", 10); if (CANARY_PERCENTAGE > 0) { // Use a consistent hash for sticky sessions. The client IP is // available on the true-client-ip header. const sessionId = request.headers.get("x-session-id") ?? request.headers.get("true-client-ip") ?? "unknown"; const hash = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(sessionId), ); const hashArray = Array.from(new Uint8Array(hash)); const hashValue = hashArray[0] / 255; // Value between 0 and 1 if (hashValue * 100 < CANARY_PERCENTAGE) { context.custom.backendUrl = environment.API_URL_CANARY; request.headers.set("X-Canary-Route", "percentage"); } else { context.custom.backendUrl = environment.API_URL_PRODUCTION; } } else { context.custom.backendUrl = environment.API_URL_PRODUCTION; } return request; };

Configuration

Environment Variables

Configure environment variables under the Environments tab of your project in the Zuplo Portal:

TerminalCode
# List of employee emails or user IDs CANARY_USERS=alice@company.com,bob@company.com,user-123 # Backend URLs API_URL_PRODUCTION=https://api.company.com API_URL_CANARY=https://api-canary.company.com # For percentage-based routing CANARY_PERCENTAGE=10 # Route 10% of traffic to canary

Adding the Policy to Routes

Add the policy to your route configuration. Use the built-in URL Forward handler and reference the value the policy stored on context.custom in the baseUrl option:

Code
{ "paths": { "/api/v1/*": { "get": { "x-zuplo-route": { "corsPolicy": "anything-goes", "handler": { "export": "urlForwardHandler", "module": "$import(@zuplo/runtime)", "options": { "baseUrl": "${context.custom.backendUrl}" } }, "policies": { "inbound": ["auth-policy", "canary-routing"] } } } } } }

The authentication policy runs first so that request.user is populated when the canary routing policy checks the allow list.

Testing Your Policy

1. Using Query Parameters

Test canary routing with a query parameter:

TerminalCode
# Route to canary backend curl https://your-api.zuplo.app/api/v1/users?stage=canary # Route to release backend (default) curl https://your-api.zuplo.app/api/v1/users?stage=release # Or omit the parameter entirely for release curl https://your-api.zuplo.app/api/v1/users

2. Using Headers

Test with a custom header:

TerminalCode
# Route to canary backend curl https://your-api.zuplo.app/api/v1/users \ -H "x-stage: canary" # Route to release backend curl https://your-api.zuplo.app/api/v1/users \ -H "x-stage: release"

3. Using Authentication

Test with an authenticated employee user:

TerminalCode
curl https://your-api.zuplo.app/api/v1/users \ -H "Authorization: Bearer <employee-token>"

Monitoring and Observability

Add comprehensive logging to track canary routing. Inbound policies run before a response exists, so set debug response headers from a response sending hook:

Code
export const canaryRoutingPolicy: InboundPolicyHandler = async ( request, context, ) => { const startTime = Date.now(); const CANARY_USERS = environment.CANARY_USERS ? environment.CANARY_USERS.split(",").map((user) => user.trim()) : []; // Check stage conditions const url = new URL(request.url); const stageParam = url.searchParams.get("stage"); const stageHeader = request.headers.get("x-stage"); const canaryUser = request.user?.sub && CANARY_USERS.includes(request.user.sub); const isCanary = stageParam === "canary" || stageHeader === "canary" || canaryUser; const backendType = isCanary ? "canary" : "release"; context.custom.backendUrl = isCanary ? environment.API_URL_CANARY : environment.API_URL_PRODUCTION; if (isCanary) { // Log canary routing metrics context.log.info("Canary route selected", { userId: request.user?.sub, method: request.method, path: url.pathname, stage: "canary", duration: Date.now() - startTime, }); } // Add a response header for debugging context.addResponseSendingHook((response) => { response.headers.set("X-Backend-Type", backendType); return response; }); return request; };

Best Practices

1. Gradual Rollout

Start with a small group of employees before expanding:

  1. Begin with volunteer beta testers
  2. Expand to engineering team
  3. Include all employees
  4. Optionally add percentage-based routing for external users

2. Feature Flags Integration

Combine with feature flags for fine-grained control:

Code
if (isCanaryUser) { context.custom.featureFlags = { newDashboard: true, betaFeatures: true, experimentalApi: true, }; }

3. Fallback Handling

To fall back to production when the canary backend fails, replace the URL Forward handler with a custom handler that retries against production on server errors:

Code
// modules/canary-fallback-handler.ts import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime"; export default async function handler( request: ZuploRequest, context: ZuploContext, ) { const url = new URL(request.url); const backendUrl = context.custom.backendUrl; const target = `${backendUrl}${url.pathname}${url.search}`; // Buffer the body so the request can be retried const body = request.body ? await request.arrayBuffer() : undefined; const response = await fetch(target, { method: request.method, headers: request.headers, body, }); // Retry against production if the canary backend returns a server error if (backendUrl === environment.API_URL_CANARY && response.status >= 500) { context.log.warn("Canary backend failed, falling back to production", { status: response.status, }); const fallbackBase = environment.API_URL_PRODUCTION; return fetch(`${fallbackBase}${url.pathname}${url.search}`, { method: request.method, headers: request.headers, body, }); } return response; }

Security Considerations

  • Authentication: Always verify user identity before canary routing
  • Query Parameter: Consider restricting stage parameter to authenticated users only
  • Access Control: Limit canary access to verified employees only
  • Audit Logging: Log all staging decisions for security audits
  • Stage Validation: Only accept valid stage values ("release" or "canary")

Next Steps

  • Learn about custom policies
  • Explore environment variables
  • Set up monitoring and analytics for canary deployments
Edit this page
Last modified on June 10, 2026
OpenAPI OverlaysGeolocation Backend Routing
On this page
  • Overview
  • How It Works
  • Creating a Canary Routing Policy
  • Advanced: Multiple Backend Support
  • Percentage-Based Canary Routing
  • Configuration
    • Environment Variables
    • Adding the Policy to Routes
  • Testing Your Policy
    • 1. Using Query Parameters
    • 2. Using Headers
    • 3. Using Authentication
  • Monitoring and Observability
  • Best Practices
    • 1. Gradual Rollout
    • 2. Feature Flags Integration
    • 3. Fallback Handling
  • Security Considerations
  • Next Steps
TypeScript
TypeScript
TypeScript
JSON
TypeScript
TypeScript
TypeScript