This guide shows you how to set up PostgreSQL using Docker for local development and Supabase for production. We'll use Drizzle ORM to interact with both databases using the same code.
Before we begin, ensure you have the following installed:
Create a new Next.js project and install dependencies:
// Create project
pnpm create next-app@latest your_app_name --typescript
cd your_app_name
// Install dependencies
pnpm add drizzle-orm postgres @neondatabase/serverless
pnpm add -D drizzle-kit
Organize your project files like this:
.
├── app/
│ └── api/
│ └── hello/
│ └── route.ts // API endpoints
├── drizzle/
│ ├── schema.ts // Database tables
│ └── index.ts // Database client
├── .env // Environment variables
├── docker-compose.yml // Local database
├── drizzle.config.ts // Drizzle configuration
└── package.json // Project dependencies
drizzle.config.ts
:import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./drizzle/schema.ts",
out: "./drizzle/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
package.json
:{
"scripts": {
// Next.js commands
+ "dev": "next dev --turbo",
+ "build": "next build",
+ "start": "next start",
// Database commands
+ "db:generate": "pnpm drizzle-kit generate",
+ "db:push": "dpnpm drizzle-kit push",
+ "db:studio": "pnpm drizzle-kit studio",
// Docker commands
+ "docker:up": "docker-compose up -d",
+ "docker:down": "docker-compose down",
+ "docker:logs": "docker-compose logs -f",
// Combined commands
+ "dev:db": "pnpm docker:up && pnpm dev",
+ "setup:local": "pnpm docker:up && pnpm db:push"
}
}
Create docker-compose.yml
for local development:
version: "3.9"
services:
postgres:
image: postgres:latest
container_name: your_container_name
ports:
- "5432:5432"
environment:
POSTGRES_USER: your_username
POSTGRES_PASSWORD: your_password
POSTGRES_DB: your_database_name
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U your_username -d your_database_name"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
your_project_name
postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase co:5432/postgres
Create .env
file with your database URLs:
DATABASE_URL="postgresql://your_username:your_password@localhost:5432/your_database_name"
SUPABASE_DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
Create drizzle/schema.ts
:
import { pgTable, serial, text } from "drizzle-orm/pg-core";
// Simple message table
export const messages = pgTable("messages", {
id: serial("id").primaryKey(),
text: text("text").notNull(),
});
Create drizzle/index.ts
:
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
// Get database URL based on environment
const databaseUrl =
process.env.NODE_ENV === "production"
? process.env.SUPABASE_DATABASE_URL
: process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("No database URL provided");
}
// Configure database client
const client = postgres(databaseUrl, {
ssl: process.env.NODE_ENV === "production",
max: 1,
});
// Create database instance
export const db = drizzle(client);
Create app/api/hello/route.ts
:
import { db } from "@/drizzle";
import { messages } from "@/drizzle/schema";
import { NextResponse } from "next/server";
// Get the latest message
export async function GET() {
try {
const [message] = await db.select().from(messages).limit(1);
return NextResponse.json({
message: message?.text ?? "No message yet",
});
} catch (error) {
return NextResponse.json({ message: "Hello, World!" }, { status: 500 });
}
}
// Save a new message
export async function POST(request: Request) {
try {
const { text } = await request.json();
await db.insert(messages).values({ text });
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: "Failed to save message" },
{ status: 500 },
);
}
}
Replace placeholder values:
docker-compose.yml
: your_username, your_password, your_database_name.env
: DATABASE_URL and SUPABASE_DATABASE_URLStart the database and push schema:
pnpm setup:local
pnpm dev:db
Test your endpoints using curl:
curl -X POST http://localhost:3000/api/hello \
-H "Content-Type: application/json" \
-d '{"text":"Hello, Database!"}'
curl http://localhost:3000/api/hello
Development Best Practices
Production Best Practices
Common Issues
Docker Connection Issues:
pnpm docker:logs
to debugSchema Push Failures:
Useful Commands
pnpm dev
: Start Next.js serverpnpm db:generate
: Generate migrationspnpm db:push
: Push schema changespnpm db:studio
: Open database UIpnpm docker:up
: Start Dockerpnpm docker:down
: Stop Dockerpnpm docker:logs
: View logspnpm dev:db
: Start developmentpnpm setup:local
: Initialize setup