DEV Community

Cover image for How to Hide Your Backend VPS IP Behind Vercel Using Next.js Rewrites
Pradeep Kumar
Pradeep Kumar

Posted on Originally published at zyvop.com

How to Hide Your Backend VPS IP Behind Vercel Using Next.js Rewrites

If you host your frontend on Vercel and your backend on a custom VPS, your frontend usually makes API requests directly to the VPS's public IP address or domain. While this works, it exposes your backend server's true IP address to the world, making it vulnerable to direct DDoS attacks, port scanners, and malicious bots.

By using Vercel Rewrites, you can use Vercel's edge network as a shield. To the outside world, your API requests appear to be going to Vercel, while Vercel secretly fetches the data from your VPS behind the scenes.

Here is how you can set up a secure API proxy in Next.js (App Router) and lock down your backend to reject any request that tries to bypass Vercel.

The Architecture

sequenceDiagram
    actor User
    participant Vercel as Vercel (Next.js)
    participant VPS as VPS (NestJS Backend)

    User->>Vercel: Fetch /api-proxy/data
    Note over Vercel: Middleware injects secret header<br/>next.config.ts rewrites path
    Vercel->>VPS: Fetch /data (with Secret Header)

    alt Valid Secret
        VPS-->>Vercel: 200 OK (Data)
        Vercel-->>User: 200 OK (Data)
    else Missing/Invalid Secret
        VPS-->>Vercel: 403 Forbidden
    end

    User-xVPS: Direct Request (Blocked 403)

Enter fullscreen mode Exit fullscreen mode

Step 1: Configure Next.js Rewrites

First, we need to tell Next.js to intercept requests made to a specific proxy path (e.g., /api-proxy/*) and forward them to your VPS. We achieve this by adding a rewrite rule to next.config.ts.

To ensure we don't accidentally create proxy loops, we will use a specific environment variable BACKEND_URL solely for the rewrite destination.

// next.config.ts
export default {
  async rewrites() {
    // The true IP of your VPS, safely hidden in Vercel environment variables
    const apiUrl = process.env.BACKEND_URL || 'http://localhost:4000';

    return [
      { 
        source: '/api-proxy/:path*', 
        destination: `${apiUrl}/:path*` 
      },
    ];
  },
};

Enter fullscreen mode Exit fullscreen mode

Step 2: Inject a Secret Header at the Edge

Now that the request is proxying through Vercel, we need a way for our backend to know that the request actually came from Vercel. We do this by injecting a secret header.

In Next.js, we can use Edge Middleware to intercept the request and inject the header before the next.config.ts rewrite takes effect.

// middleware.ts (or proxy.ts if you use a custom setup)
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(req: NextRequest) {
  const requestHeaders = new Headers(req.headers);
  const proxySecret = process.env.VERCEL_PROXY_SECRET;

  // Inject the secret key into the headers
  if (proxySecret) {
    requestHeaders.set('x-vercel-proxy-secret', proxySecret);
  }

  // Pass the modified headers along to the Next.js rewrite engine
  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

export const config = {
  matcher: ['/api-proxy/:path*'],
};

Enter fullscreen mode Exit fullscreen mode

Step 3: Lock Down the Backend

If an attacker guesses your VPS IP, they could still bypass Vercel entirely. We need to configure the backend to reject any request that lacks the secret header.

Here is an example of how to enforce this using a global hook in a NestJS + Fastify backend:

// backend/src/main.ts
const fastifyInstance = app.getHttpAdapter().getInstance();

fastifyInstance.addHook('onRequest', (request, reply, done) => {
  const proxySecret = process.env.VERCEL_PROXY_SECRET;

  // Exclude localhost (127.0.0.1) so internal Docker health checks still work!
  const isLocal = request.ip === '127.0.0.1' || request.ip === '::1';

  if (proxySecret && !isLocal) {
    const incomingSecret = request.headers['x-vercel-proxy-secret'];
    if (incomingSecret !== proxySecret) {
      reply.status(403).send({ message: 'Forbidden: Invalid proxy secret' });
      return;
    }
  }
  done();
});

Enter fullscreen mode Exit fullscreen mode

[!TIP] Docker Health Checks: Notice the isLocal check. If you use Docker, your container's internal health check pinging localhost won't have the secret header. Bypassing the check for local IPs prevents your deployments from rolling back due to failed health checks!

Step 4: Fixing Server-Side Rendering (SSR/SSG)

There's one hidden "gotcha" with this setup. During the Next.js build process (next build), Next.js generates static pages and makes fetch requests directly to your backend API.

Because these requests originate from the build server and bypass your Next.js middleware, they will fail with a 403 Forbidden error because they lack the x-vercel-proxy-secret header.

To fix this, you must manually inject the secret into your internal API client or Apollo Client configuration:

// lib/apollo-server.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';

export const apolloServerClient = new ApolloClient({
  ssrMode: true,
  link: new HttpLink({ 
    uri: `${API_URL}/graphql`, 
    fetch, 
    headers: process.env.VERCEL_PROXY_SECRET 
      ? { 'x-vercel-proxy-secret': process.env.VERCEL_PROXY_SECRET } 
      : {}
  }),
  cache: new InMemoryCache(),
});

Enter fullscreen mode Exit fullscreen mode

Summary

By combining Next.js rewrites, Edge middleware, and a simple backend validation check, you can successfully shield your custom VPS behind Vercel's robust infrastructure. Your true IP address remains a secret, and your API is locked down against direct unauthorized access!


Originally published on ZyVOP

💡 For more articles like this, subscribe to the ZyVOP newsletter!

Top comments (0)