Zuplo

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

Create a new policy file in your project:

TypeScriptCode
// 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; // Route based on stage if (isCanary) { context.route.url = 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.route.url = 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(), request); } return request; };

Advanced: Multiple Backend Support

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

TypeScriptCode
// 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(), request); } return request; };

Percentage-Based Canary Routing

Roll out canary deployments gradually with percentage-based routing:

TypeScriptCode
// policies/percentage-canary-routing.ts import { InboundPolicyHandler, ZuploRequest, 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.route.url = 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 const sessionId = request.headers.get("x-session-id") || request.ip; 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.route.url = environment.API_URL_CANARY; request.headers.set("X-Canary-Route", "percentage"); } else { context.route.url = environment.API_URL_PRODUCTION; } } else { context.route.url = environment.API_URL_PRODUCTION; } return request; };

Configuration

Environment Variables

Configure your environment variables in the Zuplo dashboard:

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:

JSONCode
{ "paths": { "/api/v1/*": { "get": { "x-zuplo-route": { "corsPolicy": "anything-goes", "handler": { "export": "urlRewriteHandler", "module": "$import(@zuplo/runtime)", "options": { "rewritePattern": "https://api.company.com$1" } }, "policies": { "inbound": ["canary-routing", "auth-policy"] } } } } } }

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:

TypeScriptCode
export const canaryRoutingPolicy: InboundPolicyHandler = async ( request, context, ) => { const startTime = Date.now(); // Check stage conditions const url = new URL(request.url); const stageParam = url.searchParams.get("stage"); const stageHeader = request.headers.get("x-stage"); const canaryUser = /* ... check if user is in canary list ... */; const isCanary = stageParam === "canary" || stageHeader === "canary" || canaryUser; if (isCanary) { context.route.url = environment.API_URL_CANARY; // 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 response header for debugging context.response.headers.set("X-Backend-Type", "canary"); } else { context.route.url = environment.API_URL_PRODUCTION; context.response.headers.set("X-Backend-Type", "release"); } 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:

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

3. Fallback Handling

Implement fallback logic for canary backend failures:

TypeScriptCode
// In your error handling policy if (context.response.status >= 500 && context.custom.isCanary) { // Retry with production backend context.route.url = environment.API_URL_PRODUCTION; return context.retry(); }

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

Last modified on