Next.js_
Connect a Next.js App Router application to an Appwrite native PostgreSQL database from Route Handlers, Server Actions, and Server Components, pool from serverless, and fall back to the SQL API on the Edge runtime.
4 min read
An Appwrite native PostgreSQL database works with standard PostgreSQL drivers and ORMs, so a Next.js App Router application can query it from server-side code. Point your driver at the connection string from the Connections page and keep all database access on the server.
You'll need a native PostgreSQL database in a ready state and its credentials. See PostgreSQL to create one and Connections to retrieve the connection string. The primary user is admin, and Appwrite generates the database name for each database.
Where to connect
In the App Router, every server-side execution context runs on the Node.js runtime by default, and the Node.js runtime can open raw TCP sockets. That means you can use a standard database driver from any of these:
- Route Handlers (
app/api/.../route.ts) for public endpoints, webhooks, and REST-style APIs. - Server Actions (
'use server'functions) for form submissions and app-internal mutations. - Server Components (
asynccomponents) for read queries that render straight into the page.
Never import a database driver into a Client Component ('use client') or ship the connection string to the browser. Keep all database access on the server.
The one exception is the Edge runtime (export const runtime = 'edge'), which runs on a constrained environment that cannot open TCP database sockets. If a route opts into Edge, use the SQL API over HTTPS instead, see the Edge runtime section below.
Environment variables
Put the connection string in your environment and never commit it. For local development, use .env.local (Next.js loads it automatically and it's git-ignored by default):
DATABASE_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:6432/<database>?sslmode=require"DIRECT_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:5432/<database>?sslmode=require"DATABASE_URL points at the connection pooler port (6432) for runtime traffic, and DIRECT_URL points at the PostgreSQL engine port (5432) for migrations. The next section explains why. The sslmode=require parameter is already part of the string Appwrite returns, so no extra certificate configuration is needed. For full verification (verify-full) or mTLS, see the Network security page.
Pool serverless connections
When you deploy to Vercel, Netlify, or any serverless platform, each invocation can spin up a fresh instance with its own connection pool. Hundreds of concurrent invocations fan out into far more backend connections than the engine allows. Route runtime traffic through the connection pooler on the pooler port so it can multiplex those instances over a small number of backend connections.
The pooler defaults to transaction mode, which does not keep a backend connection across statements, so server-side prepared statements aren't available. Reserve the engine port for migrations, which need a session-level connection. The two-URL split above (DATABASE_URL pooled, DIRECT_URL direct) is exactly what an ORM like Prisma expects.
Use a singleton client
Instantiate one client per module scope and reuse it across invocations, so warm serverless instances don't reconnect on every request. In development, Next.js hot-reload re-evaluates modules, which can leak connections, so cache the client on globalThis.
With postgres.js (note prepare: false for transaction-mode pooling):
// lib/db.tsimport postgres from 'postgres';
const globalForDb = globalThis as unknown as { sql?: ReturnType<typeof postgres> };
export const sql = globalForDb.sql ?? postgres(process.env.DATABASE_URL!, { prepare: false, // required: pooler transaction mode has no server-side prepared statements });
if (process.env.NODE_ENV !== 'production') globalForDb.sql = sql;Query it from a Server Component or Route Handler:
// app/users/route.tsimport { sql } from '@/lib/db';
export async function GET() { const users = await sql`SELECT id, email FROM users ORDER BY created_at DESC LIMIT 10`; return Response.json(users);}Use Prisma or Drizzle
For a typed schema, migrations, and a query builder, reach for an ORM. Both integrate with the pooled DATABASE_URL plus direct DIRECT_URL pattern above.
- Prisma: point the runtime at the pooled connection string and keep the Prisma CLI on the engine port for migrations, then run
prisma migrate deploy. See the Prisma guide for the full config and migration flow. - Drizzle: use a pooled client (
prepare: falseon postgres.js) for runtime and the direct URL fordrizzle-kitmigrations. See the Drizzle guide.
Edge runtime: use the SQL API
If a Route Handler or route segment opts into the Edge runtime, it can't open a TCP socket, so no engine driver will work there:
export const runtime = 'edge'; // no TCP sockets availableFrom the Edge runtime, you can execute one parameterised SQL statement over HTTPS and get JSON back. SELECT, INSERT, UPDATE, and DELETE statements are allowed by default. Use the global fetch available in the Edge runtime:
// app/edge-users/route.tsexport const runtime = 'edge';
export async function GET() { const response = await fetch( 'https://<REGION>.cloud.appwrite.io/v1/postgresql/<DATABASE_ID>/executions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Appwrite-Project': process.env.APPWRITE_PROJECT_ID!, 'X-Appwrite-Key': process.env.APPWRITE_API_KEY!, }, body: JSON.stringify({ sql: 'SELECT id, email FROM users WHERE created_at > $1 ORDER BY created_at DESC LIMIT $2', bindings: ['2026-05-01T00:00:00Z', 10], }), }, );
const { rows } = await response.json(); return Response.json(rows);}The response is { rows, rowCount, columns, durationMs, truncated, bytes }. Bindings are sent separately and never interpolated into the SQL string. Add APPWRITE_PROJECT_ID and APPWRITE_API_KEY (a key with the databases.read scope) to your environment alongside the database URLs.
Local development
next dev runs on the Node.js runtime, so a local server connects to the native PostgreSQL database over TLS exactly like production. Keep DATABASE_URL and DIRECT_URL in .env.local.
For throwaway data in tests or experiments, create a branch, an instant, isolated copy with its own connection string, and point .env.local at it. Delete the branch when you're done.
Deploy
When you deploy, set the same DATABASE_URL (pooled) and DIRECT_URL (direct) as environment variables on your hosting platform, run your migrations in the build step, and fall back to the SQL API from any Edge Functions.
Related
Was this page helpful?
Share what worked or what we should fix. Once approved, our agents automatically apply suggested updates to the docs.