Oct 28, 20268 min read

How to Move a Node.js Backend from Vercel to AWS

Moving Node.js API routes, Vercel Cron, a Neon or Vercel Postgres database and Blob uploads onto Encore in your own AWS account, while the frontend stays where it is.

Many Node.js backends on Vercel started as a few files in api/ or app/api/ next to a Next.js frontend: a POST /api/orders function that writes to Postgres from the Neon integration, a GET /api/invoices/[id] that hands out a Blob URL, and a vercel.json with a crons entry that hits /api/cron/expire every hour. It starts to pinch when the functions need background work that outlives a request, when connection counts from many short-lived function instances hit the database limit, or when the backend has to run in an AWS account the company controls.

This guide moves the backend part onto Encore in your own AWS account and leaves the frontend on Vercel. The API becomes a long-running service with its own database, schedule and bucket, tested locally with encore run, and the frontend switches to calling its URL. The generic Vercel to AWS migration guide covers the hosting side, including moving the frontend.

Vercel to AWS mapping for a Node.js backend

VercelEncore on AWSManual AWS
Functions in api/Endpoints declared with api(), one long-running service on FargateLambda functions behind API Gateway, or an ECS service
crons in vercel.jsonCronJob calling an endpointEventBridge Scheduler
Postgres from the Neon integrationSQLDatabase, provisioned as RDSRDS instance and RDS Proxy
Vercel BlobBucket, provisioned as S3S3 bucket and presigned URLs
Environment variablessecret(), stored in Secrets ManagerSecrets Manager or Parameter Store
Preview deploymentsPreview environments per pull requestA separate stack per branch

Encore: the backend as one service with its infrastructure in the code

Encore

Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions what the code declares in your own AWS or GCP account. The functions become endpoints in a service that stays running, which holds a connection pool to Postgres and can publish work to a queue, and encore run starts the service with Postgres and a local bucket so the whole backend runs on a laptop without the Vercel CLI.

Encore declares infrastructure in application code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in application code, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

Install the CLI and create the Encore app in its own directory, such as backend/ next to the Next.js app:

~/shop/backend
$ brew install encoredev/tap/encore$ encore app create

API routes become endpoints

The Postgres connection that each function opened from POSTGRES_URL becomes a database declared once per service. The existing schema goes into numbered SQL files in shop/migrations:

// shop/db.ts import { SQLDatabase } from "encore.dev/storage/sqldb"; // Replaces the managed Postgres add-on: a local database under `encore run`, // RDS when deployed to AWS. export const db = new SQLDatabase("shop", { migrations: "./migrations" });

Each route handler becomes an endpoint with typed input and output. Validation errors and missing rows turn into 400 and 404 responses through APIError:

// shop/orders.ts import { api, APIError } from "encore.dev/api"; import { db } from "./db"; export interface Order { id: number; email: string; totalCents: number; status: string; } export const createOrder = api( { expose: true, method: "POST", path: "/orders" }, async (p: { email: string; totalCents: number }): Promise<Order> => { if (!p.email.includes("@") || p.totalCents < 0) { throw APIError.invalidArgument("a valid email and a non-negative total are required"); } const order = await db.queryRow<Order>` INSERT INTO orders (email, total_cents) VALUES (${p.email}, ${p.totalCents}) RETURNING id, email, total_cents AS "totalCents", status`; return order!; }, ); export const getOrder = api( { expose: true, method: "GET", path: "/orders/:id" }, async ({ id }: { id: number }): Promise<Order> => { const order = await db.queryRow<Order>` SELECT id, email, total_cents AS "totalCents", status FROM orders WHERE id = ${id}`; if (!order) throw APIError.notFound("order not found"); return order; }, );

Without the /api prefix from Vercel, either add it to each path or rewrite it on the frontend side. Endpoints are documented in the local development dashboard, and encore gen client writes a typed TypeScript client the Next.js app can import, as the client generation docs describe.

Vercel Cron becomes a cron job

A crons entry in vercel.json calls a route over HTTP, which means the route is public unless you check the CRON_SECRET header. An Encore cron job calls a private endpoint instead, which is not reachable from the internet:

// shop/jobs.ts import { api } from "encore.dev/api"; import { CronJob } from "encore.dev/cron"; import { db } from "./db"; // Only touches rows that are still pending, so a repeated run is harmless. export const expirePending = api( { expose: false, method: "POST", path: "/internal/expire-pending" }, async (): Promise<{ cancelled: number }> => { const rows = await db.queryAll` UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND created_at < now() - interval '24 hours' RETURNING id`; return { cancelled: rows.length }; }, ); // Replaces the scheduler entry on the old platform. const _ = new CronJob("expire-pending", { title: "Cancel orders left pending for a day", every: "1h", endpoint: expirePending, });

Cron jobs do not run under encore run, so call the endpoint directly while developing. Once deployed, remove the crons entry from vercel.json in the same release.

Blob becomes a bucket

Files the app stored in Vercel Blob move to an object storage bucket. The frontend asks the API for a short-lived signed URL and downloads from S3 directly:

// shop/uploads.ts import { api } from "encore.dev/api"; import { Bucket } from "encore.dev/storage/objects"; // Replaces files written to a mounted volume or a hosted blob store: // a local bucket under `encore run`, S3 when deployed to AWS. export const invoices = new Bucket("invoices"); export const uploadInvoice = api( { expose: true, method: "POST", path: "/invoices/:id" }, async ({ id, contents }: { id: string; contents: string }): Promise<{ key: string }> => { const key = `invoice-${id}.txt`; await invoices.upload(key, Buffer.from(contents), { contentType: "text/plain" }); return { key }; }, ); export const invoiceLink = api( { expose: true, method: "GET", path: "/invoices/:id/link" }, async ({ id }: { id: string }): Promise<{ url: string }> => { const { url } = await invoices.signedDownloadUrl(`invoice-${id}.txt`, { ttl: 300 }); return { url }; }, );

For larger files, signedUploadUrl lets the browser upload straight to the bucket as well. Running it all locally:

~/shop/backend
$ encore runYour API is running at: http://127.0.0.1:4000$ curl -X POST localhost:4000/orders -d '{"email":"ada@example.com","totalCents":5500}'{"id":1,"status":"pending","email":"ada@example.com","totalCents":5500}$ curl localhost:4000/orders/1{"status":"pending","id":1,"totalCents":5500,"email":"ada@example.com"}$ curl -X POST localhost:4000/invoices/7 -d '{"contents":"hello"}'{"key":"invoice-7.txt"}$ curl "$(curl -s localhost:4000/invoices/7/link | jq -r .url)"hello

Pointing the frontend at the new API

Add the production frontend's domain to allow_origins_with_credentials under the global_cors key in encore.app if the browser sends cookies, as the CORS docs describe, and set the API URL as an environment variable in the Vercel project. Unauthenticated requests from any origin are allowed by default.

After you connect your AWS account and push, Encore provisions the RDS database, the S3 bucket, the schedule and the Fargate service in your own account, and provisions an IAM role for the service. Each pull request can get a preview environment, which pairs with Vercel's preview deployments of the frontend.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

A coding agent can carry a feature across both halves in one branch: the endpoint and its table in backend/, the page that calls it in the Next.js app, with encore run and next dev side by side to check the result.

The manual AWS path

Without Encore, the closest match to Vercel functions on AWS is Lambda:

  • Package each route as a Lambda function, or the whole Express app with an adapter, behind API Gateway or a Lambda function URL.
  • Create an RDS for PostgreSQL instance in a VPC and put RDS Proxy in front of it, because each concurrent Lambda instance opens its own connection.
  • Move Blob files to an S3 bucket, grant the functions s3:GetObject and s3:PutObject, and generate presigned URLs with the AWS SDK.
  • Replace the crons entries with EventBridge Scheduler schedules that invoke the functions.
  • Store environment variables in Secrets Manager and read them at cold start.

An ECS service on Fargate avoids the connection problem at the cost of a load balancer, a task definition and an image pipeline.

Moving the data

Postgres created through Vercel now runs on Neon, which accepts standard pg_dump connections. Pause writes on the frontend, dump in custom format and restore the data into the Encore database:

~/shop/backend
$ pg_dump -Fc --no-owner "$POSTGRES_URL_NON_POOLING" -f shop.dump$ pg_restore --data-only --no-owner \ --dbname "$(encore db conn-uri shop --env=prod --write)" shop.dump

Use the direct (non-pooling) connection string for the dump, since pg_dump does not work well through a transaction-mode pooler, and check the pg_dump docs if you need to exclude tables. Copy Blob files to the bucket with a one-off script that lists them and uploads each one with the same key.

How to choose

  • Encore if you want the backend as a long-running service with its database, schedule and bucket in TypeScript, running in your own AWS account, and the frontend left where it works well.
  • Manual AWS with Lambda and RDS Proxy if you want to keep the function-per-route model and already manage AWS with Terraform or CDK.

Vercel alternatives covers frontend and full-stack platforms, and How to deploy Node.js to AWS compares every AWS compute option.

Frequently asked questions

How do I move a Node.js backend from Vercel to AWS?

Move the API routes into a long-running backend and leave the frontend where it is. With Encore, each function in api/ becomes an endpoint declared with api(), Vercel Cron entries in vercel.json become CronJob declarations, Blob uploads move to a Bucket and the Postgres connection becomes an SQLDatabase. encore run starts all of it locally, and on deploy Encore provisions Fargate, RDS and S3 in your AWS account. The frontend then calls the new API URL.

Can I keep my Next.js frontend on Vercel and run the backend on AWS?

Yes. Deploy the backend to your AWS account, set its URL as an environment variable in the Vercel project, and allow the frontend's origin in the backend's CORS settings. If the browser sends cookies, the origin has to be listed explicitly rather than allowed with a wildcard.

What replaces Vercel Cron on AWS?

EventBridge Scheduler triggering a Lambda function or an ECS task on the same cron expressions vercel.json used. Vercel Cron schedules are in UTC, and so are EventBridge Scheduler schedules unless you set a time zone.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders