create-eth
Version:
Create a Scaffold-ETH-2 app
298 lines (221 loc) • 10.8 kB
Markdown
---
name: drizzle-neon
description: "Add a PostgreSQL database with Drizzle ORM to a Scaffold-ETH 2 project. Use when the user wants to: add a database, use Drizzle ORM, integrate Neon PostgreSQL, store off-chain data, build a backend with database, or add persistent storage to their dApp."
---
# Drizzle ORM + Neon PostgreSQL Integration for Scaffold-ETH 2
## Prerequisites
Check if `./packages/nextjs/scaffold.config.ts` exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
## Overview
[Drizzle ORM](https://orm.drizzle.team/) is a TypeScript ORM for SQL databases with a type-safe query builder. [Neon](https://neon.tech/) is a serverless PostgreSQL platform. This skill integrates both into SE-2, with a smart database client that auto-detects the environment (local Postgres via Docker, Neon serverless, or Neon HTTP) and uses the optimal driver.
For Drizzle API reference beyond what's covered here, refer to the [Drizzle docs](https://orm.drizzle.team/docs/overview). For Neon specifics, see the [Neon docs](https://neon.tech/docs). This skill focuses on SE-2 integration patterns and the tri-driver architecture.
## Dependencies & Scripts
### NextJS package
Add to `packages/nextjs/package.json`:
```json
{
"scripts": {
"db:seed": "tsx services/database/seed.ts",
"db:wipe": "tsx services/database/wipe.ts",
"drizzle-kit": "drizzle-kit"
},
"dependencies": {
"@neondatabase/serverless": "^1.0.0",
"dotenv": "^17.0.0",
"drizzle-orm": "^0.44.0",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/pg": "^8",
"drizzle-kit": "^0.31.0",
"drizzle-seed": "^0.3.0",
"tsx": "^4.20.0"
}
}
```
### Root package.json scripts
```json
{
"drizzle-kit": "yarn workspace @se-2/nextjs drizzle-kit",
"db:seed": "yarn workspace @se-2/nextjs db:seed",
"db:wipe": "yarn workspace @se-2/nextjs db:wipe"
}
```
### Environment variables
Create `packages/nextjs/.env.development`:
```env
POSTGRES_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres"
```
Also add `POSTGRES_URL=` to `packages/nextjs/.env.example`.
### Docker Compose (local development)
Create `docker-compose.yml` at project root:
```yaml
version: "3"
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: mysecretpassword
ports:
- "5432:5432"
volumes:
- ./data/db:/var/lib/postgresql/data
```
Add `data` to `.gitignore`.
## Database Client Architecture
The key integration piece is a smart database client at `packages/nextjs/services/database/config/postgresClient.ts` that auto-selects the right Postgres driver based on the connection string and runtime:
| Connection URL contains | Runtime | Driver used | Why |
|------------------------|---------|-------------|-----|
| `neondb` | Next.js (`NEXT_RUNTIME` set) | `drizzle-orm/neon-serverless` | WebSocket-based, works in serverless |
| `neondb` | Scripts (no `NEXT_RUNTIME`) | `drizzle-orm/neon-http` | Supports `batch()` for bulk operations |
| anything else | Any | `drizzle-orm/node-postgres` | Standard `pg` Pool for local/other Postgres |
This matters because Neon's serverless driver uses WebSockets (required in edge/serverless runtimes), while the HTTP driver is better for scripts that need batch operations. For local development with Docker, the standard `pg` driver is used.
Reference implementation:
```typescript
// packages/nextjs/services/database/config/postgresClient.ts
import * as schema from "./schema";
import { Pool as NeonPool, neon } from "@neondatabase/serverless";
import { drizzle as drizzleNeonHttp } from "drizzle-orm/neon-http";
import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
export const PRODUCTION_DATABASE_HOSTNAME = "your-production-database-hostname";
let dbInstance: ReturnType<typeof drizzle<typeof schema>> | null = null;
let poolInstance: Pool | NeonPool | null = null;
export function getDb() {
if (dbInstance) return dbInstance;
const isNextRuntime = !!process.env.NEXT_RUNTIME;
if (process.env.POSTGRES_URL?.includes("neondb")) {
if (isNextRuntime) {
poolInstance = new NeonPool({ connectionString: process.env.POSTGRES_URL });
dbInstance = drizzleNeon(poolInstance as NeonPool, { schema, casing: "snake_case" });
} else {
const sql = neon(process.env.POSTGRES_URL);
dbInstance = drizzleNeonHttp({ client: sql, schema, casing: "snake_case" });
}
} else {
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });
poolInstance = pool;
dbInstance = drizzle(pool, { schema, casing: "snake_case" });
}
return dbInstance;
}
export async function closeDb(): Promise<void> {
if (poolInstance) {
await poolInstance.end();
poolInstance = null;
dbInstance = null;
}
}
```
Expose the client as a lazy proxy so imports don't eagerly connect:
```typescript
// Same file, bottom
const dbProxy = new Proxy({}, {
get: (_, prop) => {
if (prop === "close") return closeDb;
const db = getDb();
return db[prop as keyof typeof db];
},
});
export const db = dbProxy as ReturnType<typeof getDb> & { close: () => Promise<void> };
```
## Schema Definition
Define tables in `packages/nextjs/services/database/config/schema.ts`:
```typescript
// packages/nextjs/services/database/config/schema.ts
import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
name: varchar({ length: 255 }).notNull(),
});
```
For the full column types and schema API, see [Drizzle schema docs](https://orm.drizzle.team/docs/sql-schema-declaration).
## Drizzle Config
The `drizzle.config.ts` at `packages/nextjs/` configures Drizzle Kit for migrations and studio:
```typescript
// packages/nextjs/drizzle.config.ts
import * as dotenv from "dotenv";
import { defineConfig } from "drizzle-kit";
dotenv.config({ path: ".env.development" });
export default defineConfig({
schema: "./services/database/config/schema.ts",
out: "./services/database/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.POSTGRES_URL as string,
},
casing: "snake_case",
});
```
> **Important:** `casing: "snake_case"` must be set in **both** `drizzle.config.ts` and the `drizzle()` client initialization. This tells Drizzle to convert camelCase TypeScript property names to snake_case column names. If they don't match, queries will fail silently or return wrong data.
## Repository Pattern
Use a repository pattern at `packages/nextjs/services/database/repositories/`. Each entity gets its own file with typed CRUD functions:
```typescript
// packages/nextjs/services/database/repositories/users.ts
import { users } from "../config/schema";
import type { InferInsertModel } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { db } from "~~/services/database/config/postgresClient";
export type User = InferInsertModel<typeof users>;
export async function getAllUsers() {
return await db.query.users.findMany();
}
export async function getUserById(id: string) {
return await db.query.users.findFirst({
where: eq(users.id, id),
});
}
export async function createUser(user: User) {
return await db.insert(users).values(user);
}
```
## Using the Database in Next.js
Server components can import repository functions directly — no API route needed:
```typescript
// packages/nextjs/app/users/page.tsx — Server Component
import { getAllUsers, createUser } from "~~/services/database/repositories/users";
const users = await getAllUsers(); // Direct DB access in server components
```
For client-side mutations, create an API route that calls the repository:
```typescript
// packages/nextjs/app/api/users/route.ts
import { createUser } from "~~/services/database/repositories/users";
export async function POST(request: NextRequest) {
const { name } = await request.json();
const user = await createUser({ name });
return NextResponse.json(user);
}
```
SE-2 already includes `/react-query` — use `useQuery` and `useMutation` for client-side data fetching and mutations against these API routes.
## Database Workflow
```bash
# Development (fast, no migrations)
docker compose up -d # Start local Postgres
yarn drizzle-kit push # Push schema directly to DB
yarn drizzle-kit studio # Open Drizzle Studio UI
yarn db:seed # Seed with test data
yarn db:wipe # Reset all tables
# Production (stable schema)
yarn drizzle-kit generate # Generate SQL migration files
yarn drizzle-kit migrate # Apply migrations to DB
```
Use `push` during active development. Switch to `generate` + `migrate` when the schema is stable. Never use `push` in production.
## Gotchas & Common Pitfalls
**`casing: "snake_case"` must match everywhere.** Set it in both `drizzle.config.ts` and every `drizzle()` client call. Mismatched casing causes queries to reference wrong column names.
**Don't import the `db` client in client components.** The database client only works server-side (Server Components, API routes, Server Actions). For client-side mutations, use API routes or Server Actions.
**Docker must be running for local development.** If `docker compose up` hasn't been run, the database connection will fail. The `.env.development` points to `localhost:5432`.
**Production safety guard.** The seed/wipe scripts should check if the connection URL points to production (via `PRODUCTION_DATABASE_HOSTNAME`). Update `your-production-database-hostname` in `postgresClient.ts` to your actual Neon project hostname to enable this protection.
**Use `.env.development` not `.env.local`.** SE-2 convention is `.env.development` for local env vars.
## How to Test
1. `docker compose up -d` — start local Postgres
2. `yarn drizzle-kit push` — apply schema to local DB
3. `yarn start` — start Next.js
4. Visit the users page — should show empty list, add a user via the form
5. `yarn db:seed` — seed with test data
6. `yarn drizzle-kit studio` — inspect data at `https://local.drizzle.studio`
### Production (Neon)
1. Create a Neon project at [neon.tech](https://neon.tech/)
2. Set `POSTGRES_URL` to the Neon connection string (contains `neondb`)
3. Update `PRODUCTION_DATABASE_HOSTNAME` in `postgresClient.ts` to your Neon project hostname
4. Run `yarn drizzle-kit generate` then `yarn drizzle-kit migrate`
5. The database client auto-switches to Neon's serverless driver