# Connect Next.js to Amazon Aurora PostgreSQL using Vercel Marketplace

**Author:** Michael Toth, Ismael Rumzan

---

Securing database connections is a critical part of building production applications. Traditional methods often involve managing long-lived database credentials, which increases the risk of credential leaks. Vercel's AWS integration simplifies this by using OIDC Federation and RDS IAM authentication, allowing your Next.js application to connect to Aurora PostgreSQL without hardcoded access tokens or passwords.

This guide shows you how to set up a secure connection, implement database queries, and fetch data in a Next.js Server Component.

## Overview

By the end of this guide, you will be able to:

- Connect a Next.js application to AWS Aurora PostgreSQL securely
  
- Use Vercel OIDC federation to authenticate with AWS
  
- Implement RDS IAM authentication for database access without passwords
  
- Fetch and display data from your database using Server Components
  

## How it works

Understanding the security flow helps you manage your database connections effectively.

### OIDC federation and RDS IAM authentication

Instead of using a static username and password, this approach uses short-lived tokens:

1. **OIDC federation**: Vercel provides an OIDC token to your serverless functions. This token identifies the Vercel project and environment.
   
2. **AWS role assumption**: Your AWS IAM role is configured to trust Vercel's OIDC provider. When your function runs, it exchanges the Vercel OIDC token for temporary AWS credentials.
   
3. **RDS IAM authentication**: Using these temporary credentials, your application generates an auth token specifically for Aurora PostgreSQL. This token acts as a temporary password to connect to the database.
   

This multi-layered approach ensures that no long-lived secrets are stored in your environment variables, significantly reducing your security surface area.

## Prerequisites

Before you begin, make sure you have:

- A **Vercel account**
  
- **Vercel CLI** installed (`npm i -g vercel`)
  
- **Node.js 18** or later installed locally
  

## 1\. Set up the Vercel AWS integration

Before connecting your application, you need a running Aurora PostgreSQL cluster.

1. Go to the [Vercel Marketplace AWS integration](https://vercel.com/marketplace/aws) and select install for the **Amazon Aurora PostgreSQL** product
   
2. Follow the setup wizard to provision a new Aurora PostgreSQL database
   
3. The integration automatically configures IAM authentication and creates the necessary roles
   

The Vercel Marketplace handles the complex AWS setup, including security groups, IAM roles, and the OIDC trust relationship.

## 2\. Connect your project

To develop locally with the same secure connection, link your project to Vercel and pull the environment variables.

### Link your project

`vercel link`

Select your team and the project connected to the AWS integration.

### Pull environment variables

`vercel env pull`

This creates a `.env.local` file with the required variables:

- `AWS_ROLE_ARN` - The IAM role your application assumes
  
- `AWS_REGION` - The AWS region of your Aurora cluster
  
- `PGHOST` - The Aurora cluster endpoint
  
- `PGPORT` - The database port (typically 5432)
  
- `PGUSER` - The database user
  
- `PGDATABASE` - The database name
  
- `VERCEL_OIDC_TOKEN` - The OIDC token for local development
  

## 3\. Install dependencies

Aurora Serverless is fully PostgreSQL compatible. Install the required packages:

`npm install pg @aws-sdk/rds-signer @vercel/functions dotenv npm install -D @types/pg tsx`

## 4\. Implement the database connection

Create a `lib/db/db.ts` file to handle the connection pool and IAM authentication:

``import { config } from "dotenv"; config({ path: ".env.local" }); import { awsCredentialsProvider } from "@vercel/functions/oidc"; import { attachDatabasePool } from "@vercel/functions"; import { Signer } from "@aws-sdk/rds-signer"; import { ClientBase, Pool } from "pg"; const signer = new Signer({ hostname: process.env.PGHOST, port: Number(process.env.PGPORT), username: process.env.PGUSER, region: process.env.AWS_REGION, credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN, clientConfig: { region: process.env.AWS_REGION }, }), }); const pool = new Pool({ host: process.env.PGHOST, user: process.env.PGUSER, database: process.env.PGDATABASE || "postgres", // The auth token value can be cached for up to 15 minutes (900 seconds) if desired. password: () => signer.getAuthToken(), port: Number(process.env.PGPORT), // Recommended to switch to `true` in production. // See <https://docs.aws.amazon.com/lambda/latest/dg/services-rds.html#rds-lambda-certificates> ssl: { rejectUnauthorized: false }, max: 20, }); attachDatabasePool(pool); // Single query transaction. export async function query(sql: string, args: unknown[]) { return pool.query(sql, args); } // Use it for multiple queries transaction. export async function withConnection<T>( fn: (client: ClientBase) => Promise<T>, ): Promise<T> { const client = await pool.connect(); try { return await fn(client); } finally { client.release(); } }`` The `dotenv` import ensures environment variables are loaded when running scripts locally. The `attachDatabasePool` utility optimizes connection management in serverless environments. ## 5\. Test the connection locally Create a test script to verify your database connection works before building features. Create `lib/db/test-connection.ts`: ``import { config } from "dotenv"; config({ path: ".env.local" }); import { awsCredentialsProvider } from "@vercel/functions/oidc"; import { Signer } from "@aws-sdk/rds-signer"; import { Pool } from "pg"; async function testConnection() { console.log("🔐 Creating RDS signer..."); console.log(` Host: ${process.env.PGHOST}`); console.log(` User: ${process.env.PGUSER}`); console.log(` Region: ${process.env.AWS_REGION}`); const signer = new Signer({ hostname: process.env.PGHOST, port: Number(process.env.PGPORT), username: process.env.PGUSER, region: process.env.AWS_REGION, credentials: awsCredentialsProvider({ roleArn: process.env.AWS_ROLE_ARN, clientConfig: { region: process.env.AWS_REGION }, }), }); console.log("\\n🔑 Generating auth token..."); const token = await signer.getAuthToken(); console.log(` Token generated (length: ${token.length})`); console.log("\\n🔌 Connecting to database..."); const pool = new Pool({ host: process.env.PGHOST, user: process.env.PGUSER, database: process.env.PGDATABASE || "postgres", password: token, port: Number(process.env.PGPORT), ssl: { rejectUnauthorized: false }, }); const result = await pool.query("SELECT NOW() as time, current_database() as db"); console.log(`\\n✅ Connected successfully!`); console.log(` Database: ${result.rows[0].db}`); console.log(` Server time: ${result.rows[0].time}`); await pool.end(); } testConnection().catch((err) => { console.error("\\n❌ Connection failed:", err.message); process.exit(1); });`` Add the script to `package.json`: `{ "scripts": { "db:test": "tsx lib/db/test-connection.ts" } }` Run the test: `npm run db:test` You should see output confirming a successful connection to your Aurora database. ## 6\. Create tables and seed data Create a setup script to initialize your database schema and seed data. Create `lib/db/setup.ts`: ``import { config } from "dotenv"; config({ path: ".env.local" }); import { withConnection } from "./db"; async function setup() { console.log("🔌 Connecting to Aurora PostgreSQL..."); await withConnection(async (client) => { console.log("📦 Creating movies table..."); await client.query(` CREATE TABLE IF NOT EXISTS movies ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, score INTEGER NOT NULL DEFAULT 0 ) `); const { rows } = await client.query("SELECT COUNT(*) as count FROM movies"); const count = parseInt(rows[0].count); if (count === 0) { console.log("🌱 Seeding movies..."); await client.query(` INSERT INTO movies (title, score) VALUES ('The Shawshank Redemption', 93), ('The Godfather', 92), ('The Dark Knight', 90), ('Pulp Fiction', 89), ('Forrest Gump', 88), ('Inception', 87), ('The Matrix', 86), ('Goodfellas', 85), ('Interstellar', 84), ('Fight Club', 83) `); console.log("✅ Seeded 10 movies!"); } else { console.log(`ℹ️ Table already has ${count} movies, skipping seed.`); } }); console.log("✅ Setup complete!"); } setup().catch((err) => { console.error("❌ Setup failed:", err); process.exit(1); });`` Add the script to `package.json`: `{ "scripts": { "db:test": "tsx lib/db/test-connection.ts", "db:setup": "tsx lib/db/setup.ts" } }` Run the setup: `npm run db:setup` ## 7\. Fetch data in a server component Now you can use the `query` function to fetch data directly in your Next.js Server Components. ### Create a query function In `lib/db/queries.ts`, implement a function to fetch movies: `import { query } from "./db"; export interface Movie { id: number; title: string; score: number; } export async function getMovies() { const sql = "SELECT id, title, score FROM movies ORDER BY score DESC LIMIT 10"; const result = await query(sql, []); return result.rows as Movie[]; }` ### Display data in the page In `app/page.tsx`, fetch and render the movies: `import { getMovies } from "@/lib/db/queries"; // Force dynamic rendering since we're fetching from a database export const dynamic = "force-dynamic"; export default async function HomePage() { const movies = await getMovies(); return ( <main className="p-8"> <h1 className="text-2xl font-bold mb-4">Top Movies</h1> <ul className="space-y-2"> {movies.map((movie) => ( <li key={movie.id} className="p-4 border rounded shadow-sm"> <span className="font-semibold">{movie.title}</span> — {movie.score} </li> ))} </ul> </main> ); }` The `dynamic = "force-dynamic"` export prevents Next.js from attempting to statically generate the page at build time, which would fail without database access. ## 8\. Run the application Start the development server: `npm run dev` Visit [http://localhost:3000](http://localhost:3000) to see your movies displayed from the Aurora PostgreSQL database.

## Best practices

To ensure your application remains secure and performant:

- **Use** `attachDatabasePool`: This utility from `@vercel/functions` helps manage database connections efficiently across multiple serverless function invocations, preventing connection exhaustion.
  
- **Use** `withConnection` **for transactions**: When running multiple queries that need to be atomic, use the `withConnection` helper to ensure they run on the same client.
  
- **Set Proper SSL**: Always use `ssl: { rejectUnauthorized: false }` (or provide the RDS CA certificate) when connecting to AWS RDS to ensure the connection is encrypted.
  
- **Principle of Least Privilege**: Ensure the IAM role assumed by your Vercel project only has the `rds-db:connect` permission for the specific database user and resource.
  

## Conclusion

You have successfully connected a Next.js application to AWS Aurora PostgreSQL using Vercel's secure OIDC Federation and RDS IAM authentication. This setup eliminates the need for long-lived database passwords, providing a more secure and maintainable architecture. By using Server Components and Vercel's database utilities, you've built a performant data-fetching layer that scales automatically with your application.

### Next steps

- **Explore the demo**: View the [live demo](https://aws-aurora-postgresql-demo.labs.vercel.dev/) or explore the [source code](https://github.com/vercel/aws-aurora-postgresql-demo)
  
- **Deep dive into OIDC**: Learn more about [OIDC Federation on Vercel](https://vercel.com/docs/security/secure-backend-access/oidc)
  
- **Database management**: Set up migrations with [Drizzle](https://orm.drizzle.team/docs/get-started-postgresql) or [Prisma](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases-typescript-postgresql)
  
- **Performance monitoring**: Use [Vercel Monitoring](https://vercel.com/docs/monitoring) to track database query performance in production

---

[View full KB sitemap](/kb/sitemap.md)
