Migration (manual)
This guide adds shadow-canary to an existing Next.js 13.4+ App Router project. It covers branch setup, middleware merge, workflow setup, and common collision scenarios.
Scenarios
- New project,
mainbranch — renamemaintomaster, createproductionfrommaster. - Existing project,
mainbranch — same rename, but you need to update branch protection rules and any existing CI that referencesmain. - Existing project with middleware — merge the shadow-canary routing into your existing middleware.
-
Rename branches
Terminal window # Rename main → master locallygit branch -m main mastergit push origin mastergit push origin --delete main# Update the default branch in GitHub:# Settings > Branches > Default branch → masterThen create the
productionbranch:Terminal window git checkout -b productiongit push origin productionIn Vercel Project Settings > Git, set Production Branch to
production. -
Configure Vercel project settings
In the Vercel dashboard, make these one-time changes:
Setting Value Why Production Branch productionOnly pushes to productiontrigger prod deploysAuto-assign Custom Production Domains OFF deploy-prod.ymlcallsvercel promotemanuallyDeployment Protection Disabled or Bypass enabled Cross-deploy rewrites need to reach shadow/previous without 401 Skew Protection ON, 7 days Prevents JS chunk mismatches during canary -
Create and link Edge Config
In Vercel Storage, create an Edge Config store. Link it to your project (store Settings > Connected Projects).
Install the SDK if not already present:
Terminal window npm install @vercel/edge-configAdd the
EDGE_CONFIGenvironment variable — Vercel injects it automatically when the store is linked to the project. Verify in Project Settings > Environment Variables thatEDGE_CONFIGappears. -
Install or merge middleware
If you have no existing
middleware.ts, copy the file from the template:Terminal window cp node_modules/@dotworld/shadow-canary-core/dist/middleware.ts middleware.ts# or copy from examples/greenfield/middleware.ts in the monorepoIf you already have a middleware, compose
shadowCanaryMiddlewareat the top of your existing handler. It returns aNextResponsewhen routing is needed, ornullwhen the request should pass through to your own logic:import { NextRequest, NextResponse } from 'next/server';import { shadowCanaryMiddleware } from '@dotworld/shadow-canary-core/edge';export async function middleware(req: NextRequest) {// Run shadow/canary routing first. Returns a rewrite/cookie response when// action is needed, or null when the request should fall through.const canaryRes = await shadowCanaryMiddleware(req);if (canaryRes) return canaryRes;// ... your existing logic (auth, i18n, etc.) ...return NextResponse.next();}export const config = {matcher: ['/((?!api|_next/static|_next/image|favicon.ico|admin|.*\\..*).*)',],};shadowCanaryMiddlewarebails out (returnsnull) when the request should bypass routing — already-rewritten requests, non-production deploys, bot UAs, or when the Edge Config payload is missing. That’s when control falls through to your existing logic.Next.js 16:
proxy.tsNext.js 16 (Oct 2025) renamed the file convention from
middleware.ts→proxy.tsand the exported function frommiddleware()→proxy(). Both names still work on v16 —middleware.tsis deprecated but is the only path for Edge runtime;proxy.tsruns on the Node.js runtime.The wire-level API (
NextRequest,NextResponse,config.matcher) is unchanged, soshadowCanaryMiddlewareworks identically across both. We exposeshadowCanaryProxyas an alias matching the v16 naming:// proxy.ts (Next.js 16, Node runtime)import { NextRequest, NextResponse } from 'next/server';import { shadowCanaryProxy } from '@dotworld/shadow-canary-core';export async function proxy(req: NextRequest) {const res = await shadowCanaryProxy(req);return res ?? NextResponse.next();}export const config = {matcher: ['/((?!api|_next/static|_next/image|favicon.ico|admin|.*\\..*).*)',],}; -
Add the three workflows
Copy
.github/workflows/from the template or monorepo into your project:deploy-shadow.yml— triggers on push tomasterdeploy-prod.yml— triggers on push toproductioncanary-ramp.yml— cron*/15 * * * *
These files do not need modification for most projects. Customisable parameters are at the top of each file (see Workflows reference).
-
Add the pages
Copy these from the template (adjust to your app directory structure):
app/debug/page.tsx— shows routing state, cookie, IP, deploy ID; three bucket-force buttonsapp/admin/— login page, dashboard, API routes for Pause/Resume/Cancel/Rollbackapp/api/slo/route.ts— stub that returns 200; replace with a real check (see SLO integration)
-
Set secrets and env vars
GitHub secrets (Settings > Secrets and variables > Actions):
Secret Source VERCEL_TOKENVercel Account Settings > Tokens VERCEL_ORG_ID.vercel/project.jsonVERCEL_PROJECT_ID.vercel/project.jsonVERCEL_EDGE_CONFIG_IDEdge Config store ID ( ecfg_xxxx)SLACK_WEBHOOK_URLSlack app > Incoming Webhooks (optional) Vercel env vars (Project Settings > Environment Variables, Production + Preview):
Variable Value VERCEL_API_TOKENSame as VERCEL_TOKENVERCEL_ORG_IDSame as GitHub secret VERCEL_EDGE_CONFIG_IDSame as GitHub secret ADMIN_USERAdmin username ADMIN_PASSAdmin password ADMIN_SESSION_SECRETRandom string: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Collision scenarios
/admin route conflict
If your app already has a route at /admin, rename the shadow-canary admin to something like /sc-admin and update:
- The middleware matcher exclusion (
admin→sc-admin) - The
app/admin/directory name - The
NEXT_PUBLIC_ADMIN_PATHenv var if the template uses one
/api/slo route conflict
Rename the SLO endpoint to /api/canary-slo and update the canary-ramp.yml workflow:
# In canary-ramp.yml, step "Run 2 SLO checks"URL='${{ steps.state.outputs.prod_url }}/api/canary-slo'Cookie name conflict
If shadow-bucket conflicts with another cookie in your app, change the cookie name in middleware.ts (search for shadow-bucket) and keep it consistent across the middleware, admin API, and debug page.
Existing middleware matcher
If your existing middleware matcher is more restrictive than the shadow-canary one, ensure it still covers the paths you want routed. The shadow-canary matcher uses:
/((?!api|_next/static|_next/image|favicon.ico|admin|.*\\..*).*Adjust as needed — the key exclusions are _next/static, _next/image (to avoid routing static assets), and admin.
Related:
- Quickstart — full walkthrough for greenfield projects
- Prerequisites — Vercel plan requirements
- Routing — how the middleware decision tree works