Skip to content

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, main branch — rename main to master, create production from master.
  • Existing project, main branch — same rename, but you need to update branch protection rules and any existing CI that references main.
  • Existing project with middleware — merge the shadow-canary routing into your existing middleware.

  1. Rename branches

    Terminal window
    # Rename main → master locally
    git branch -m main master
    git push origin master
    git push origin --delete main
    # Update the default branch in GitHub:
    # Settings > Branches > Default branch → master

    Then create the production branch:

    Terminal window
    git checkout -b production
    git push origin production

    In Vercel Project Settings > Git, set Production Branch to production.

  2. Configure Vercel project settings

    In the Vercel dashboard, make these one-time changes:

    SettingValueWhy
    Production BranchproductionOnly pushes to production trigger prod deploys
    Auto-assign Custom Production DomainsOFFdeploy-prod.yml calls vercel promote manually
    Deployment ProtectionDisabled or Bypass enabledCross-deploy rewrites need to reach shadow/previous without 401
    Skew ProtectionON, 7 daysPrevents JS chunk mismatches during canary
  3. 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-config

    Add the EDGE_CONFIG environment variable — Vercel injects it automatically when the store is linked to the project. Verify in Project Settings > Environment Variables that EDGE_CONFIG appears.

  4. 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 monorepo

    If you already have a middleware, compose shadowCanaryMiddleware at the top of your existing handler. It returns a NextResponse when routing is needed, or null when 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|.*\\..*).*)',
    ],
    };

    shadowCanaryMiddleware bails out (returns null) 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.ts

    Next.js 16 (Oct 2025) renamed the file convention from middleware.tsproxy.ts and the exported function from middleware()proxy(). Both names still work on v16 — middleware.ts is deprecated but is the only path for Edge runtime; proxy.ts runs on the Node.js runtime.

    The wire-level API (NextRequest, NextResponse, config.matcher) is unchanged, so shadowCanaryMiddleware works identically across both. We expose shadowCanaryProxy as 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|.*\\..*).*)',
    ],
    };
  5. Add the three workflows

    Copy .github/workflows/ from the template or monorepo into your project:

    • deploy-shadow.yml — triggers on push to master
    • deploy-prod.yml — triggers on push to production
    • canary-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).

  6. 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 buttons
    • app/admin/ — login page, dashboard, API routes for Pause/Resume/Cancel/Rollback
    • app/api/slo/route.ts — stub that returns 200; replace with a real check (see SLO integration)
  7. Set secrets and env vars

    GitHub secrets (Settings > Secrets and variables > Actions):

    SecretSource
    VERCEL_TOKENVercel Account Settings > Tokens
    VERCEL_ORG_ID.vercel/project.json
    VERCEL_PROJECT_ID.vercel/project.json
    VERCEL_EDGE_CONFIG_IDEdge Config store ID (ecfg_xxxx)
    SLACK_WEBHOOK_URLSlack app > Incoming Webhooks (optional)

    Vercel env vars (Project Settings > Environment Variables, Production + Preview):

    VariableValue
    VERCEL_API_TOKENSame as VERCEL_TOKEN
    VERCEL_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 (adminsc-admin)
  • The app/admin/ directory name
  • The NEXT_PUBLIC_ADMIN_PATH env 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'

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