We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide17 min read

Neon

Neon is serverless PostgreSQL with unique database branching, auto-scaling to zero and pay-per-use pricing - perfect for modern applications.

Neon - Complete Guide to Serverless PostgreSQL

What is Neon?

Neon is a next-generation serverless PostgreSQL, designed from the ground up for modern cloud applications. Unlike traditional hosted databases, Neon offers a true serverless model with auto-scaling to zero, Git-like database branching, and pay-per-use pricing.

Neon's architecture separates compute from storage, enabling instant scaling, fast cold starts (~500ms), and unlimited database branches without additional storage costs.

Why Neon?

Key advantages

  1. Database branching - Create database copies in seconds, without physically copying data
  2. Scale to zero - No charges when the application is not in use
  3. Instant wake - Cold start under 500ms
  4. Pay per use - Only pay for actual compute hours
  5. Full PostgreSQL compatibility - Support for pgvector, PostGIS, and other extensions
  6. Secure connections - All connections encrypted with TLS

Neon vs traditional databases

FeatureNeonRDS/Cloud SQLSupabase
Scale to zeroYesNoNo
BranchingNativeSnapshotsNo
Cold start~500msN/AN/A
Pricing modelPer computePer hourFlat + usage
Free tier storage512MBNone500MB
Connection poolingBuilt-inExtra costBuilt-in

When to choose Neon?

  • Side projects and MVPs - Free plan with scale to zero
  • Preview environments - Separate database for each PR
  • CI/CD testing - Isolated test environments
  • Serverless backends - Works perfectly with Edge Functions
  • Development workflow - Branching for feature development

When to consider alternatives?

  • Constant, high load - A dedicated instance may be cheaper
  • Very low latency - A self-hosted server may be faster
  • Specific PG extensions - Check availability in Neon

Neon Architecture

Compute and Storage Separation

Code
TEXT
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Neon Cloud                            β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚   Compute    β”‚    β”‚   Compute    β”‚    β”‚   Compute    β”‚   β”‚
β”‚  β”‚   (main)     β”‚    β”‚   (dev)      β”‚    β”‚   (pr-123)   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚         β”‚                    β”‚                   β”‚           β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                              β”‚                               β”‚
β”‚                      β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚                      β”‚   Pageserver   β”‚                       β”‚
β”‚                      β”‚   (Storage)    β”‚                       β”‚
β”‚                      β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                              β”‚                               β”‚
β”‚                      β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚                      β”‚    Safekeepers β”‚                       β”‚
β”‚                      β”‚   (WAL/Durability)                    β”‚
β”‚                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How does branching work?

Code
TEXT
main branch
    β”‚
    β”œβ”€β”€ commit 1
    β”‚
    β”œβ”€β”€ commit 2
    β”‚       β”‚
    β”‚       └──► dev branch (copy-on-write)
    β”‚                 β”‚
    β”‚                 β”œβ”€β”€ dev commit 1
    β”‚                 β”‚
    β”‚                 └── dev commit 2
    β”‚
    β”œβ”€β”€ commit 3
    β”‚       β”‚
    β”‚       └──► pr-123 branch
    β”‚
    └── commit 4

A branch in Neon is a copy-on-write snapshot - it does not physically copy data, only metadata. That is why creating a branch takes seconds, regardless of the database size.

Getting started

Creating a project

  1. Sign up at console.neon.tech
  2. Create a new project
  3. Choose a region (available: US East, US West, Europe, Asia)
  4. Copy the connection string

Connection String

Code
TEXT
postgresql://[user]:[password]@[host]/[database]?sslmode=require

Example:

Code
TEXT
postgresql://neonuser:password123@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require

CLI Installation

Code
Bash
# npm
npm install -g neonctl

# Homebrew (macOS)
brew install neonctl

# Authorization
neonctl auth

Branching - Git for your database

Creating branches via CLI

Code
Bash
# List branches
neonctl branches list

# Create a branch from main
neonctl branches create --name dev

# Create a branch from a specific point in time
neonctl branches create --name staging --parent main --point-in-time "2024-01-15T10:00:00Z"

# Create a branch for a PR
neonctl branches create --name pr-456 --parent main

# Delete a branch
neonctl branches delete pr-456

Branching in CI/CD

.github/workflows/preview.yml
YAML
# .github/workflows/preview.yml
name: Preview Environment

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  create-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Create Neon Branch
        id: create-branch
        uses: neondatabase/create-branch-action@v5
        with:
          project_id: ${{ secrets.NEON_PROJECT_ID }}
          branch_name: pr-${{ github.event.pull_request.number }}
          api_key: ${{ secrets.NEON_API_KEY }}

      - name: Run Migrations
        run: |
          DATABASE_URL="${{ steps.create-branch.outputs.db_url }}" npm run migrate

      - name: Deploy Preview
        run: |
          # Deploy with the new DATABASE_URL
          vercel --env DATABASE_URL="${{ steps.create-branch.outputs.db_url }}"

Automatic branch cleanup

.github/workflows/cleanup.yml
YAML
# .github/workflows/cleanup.yml
name: Cleanup Preview

on:
  pull_request:
    types: [closed]

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Delete Neon Branch
        uses: neondatabase/delete-branch-action@v3
        with:
          project_id: ${{ secrets.NEON_PROJECT_ID }}
          branch_name: pr-${{ github.event.pull_request.number }}
          api_key: ${{ secrets.NEON_API_KEY }}

Connecting to your application

Neon Serverless Driver

The official driver optimized for serverless:

Code
Bash
npm install @neondatabase/serverless
Code
TypeScript
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)

// Simple query
const users = await sql`SELECT * FROM users WHERE active = true`

// With parameters
const userId = 1
const user = await sql`SELECT * FROM users WHERE id = ${userId}`

// Insert
const newUser = await sql`
  INSERT INTO users (name, email)
  VALUES (${'John Doe'}, ${'john@example.com'})
  RETURNING *
`

// Transaction (single statement)
const result = await sql`
  WITH inserted AS (
    INSERT INTO orders (user_id, total)
    VALUES (${userId}, ${99.99})
    RETURNING id
  )
  INSERT INTO order_items (order_id, product_id, quantity)
  SELECT id, ${productId}, ${quantity} FROM inserted
  RETURNING *
`

Neon with Pool (for long-lived connections)

Code
TypeScript
import { Pool } from '@neondatabase/serverless'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

// Using with pool
const client = await pool.connect()
try {
  await client.query('BEGIN')
  await client.query('INSERT INTO orders ...', [values])
  await client.query('UPDATE inventory ...', [values])
  await client.query('COMMIT')
} catch (e) {
  await client.query('ROLLBACK')
  throw e
} finally {
  client.release()
}

// Close pool on shutdown
await pool.end()

WebSocket for real-time

Code
TypeScript
import { neon, neonConfig } from '@neondatabase/serverless'
import ws from 'ws'

// For Node.js environments (Vercel Edge, Cloudflare Workers have built-in WS)
neonConfig.webSocketConstructor = ws

const sql = neon(process.env.DATABASE_URL!)

ORM Integration

Drizzle ORM

Code
Bash
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
TSdb/schema.ts
TypeScript
// db/schema.ts
import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
  isActive: boolean('is_active').default(true),
})

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id').references(() => users.id),
  publishedAt: timestamp('published_at'),
})
TSdb/index.ts
TypeScript
// db/index.ts
import { drizzle } from 'drizzle-orm/neon-http'
import { neon } from '@neondatabase/serverless'
import * as schema from './schema'

const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })

// Usage
const allUsers = await db.select().from(schema.users)

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
})

// Insert
await db.insert(schema.users).values({
  name: 'John Smith',
  email: 'john@example.com',
})

// Update
await db.update(schema.users)
  .set({ isActive: false })
  .where(eq(schema.users.id, 1))

// Delete
await db.delete(schema.users).where(eq(schema.users.id, 1))
TSdrizzle.config.ts
TypeScript
// drizzle.config.ts
import type { Config } from 'drizzle-kit'

export default {
  schema: './db/schema.ts',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config
Code
Bash
# Generate migrations
npx drizzle-kit generate:pg

# Run migrations
npx drizzle-kit push:pg

# Studio (GUI)
npx drizzle-kit studio

Prisma

Code
Bash
npm install prisma @prisma/client
npx prisma init
prisma/schema.prisma
Prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  previewFeatures = ["driverAdapters"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
  isActive  Boolean  @default(true)
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  content     String?
  author      User      @relation(fields: [authorId], references: [id])
  authorId    Int
  publishedAt DateTime?
}
TSlib/prisma.ts
TypeScript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaNeon } from '@prisma/adapter-neon'
import { Pool } from '@neondatabase/serverless'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const adapter = new PrismaNeon(pool)

export const prisma = new PrismaClient({ adapter })

// Usage
const users = await prisma.user.findMany({
  include: { posts: true },
})

await prisma.user.create({
  data: {
    name: 'John Smith',
    email: 'john@example.com',
    posts: {
      create: {
        title: 'First post',
        content: 'Post content...',
      },
    },
  },
})
Code
Bash
# Migrations
npx prisma migrate dev --name init
npx prisma migrate deploy

# Studio
npx prisma studio

Kysely

Code
Bash
npm install kysely @neondatabase/serverless
TSdb/types.ts
TypeScript
// db/types.ts
import { Generated, Insertable, Selectable, Updateable } from 'kysely'

export interface Database {
  users: UsersTable
  posts: PostsTable
}

interface UsersTable {
  id: Generated<number>
  name: string
  email: string
  created_at: Generated<Date>
  is_active: Generated<boolean>
}

interface PostsTable {
  id: Generated<number>
  title: string
  content: string | null
  author_id: number
  published_at: Date | null
}

export type User = Selectable<UsersTable>
export type NewUser = Insertable<UsersTable>
export type UserUpdate = Updateable<UsersTable>
TSdb/index.ts
TypeScript
// db/index.ts
import { Kysely } from 'kysely'
import { NeonDialect } from 'kysely-neon'
import { Database } from './types'

export const db = new Kysely<Database>({
  dialect: new NeonDialect({
    connectionString: process.env.DATABASE_URL!,
  }),
})

// Usage
const users = await db
  .selectFrom('users')
  .selectAll()
  .where('is_active', '=', true)
  .execute()

await db
  .insertInto('users')
  .values({ name: 'John', email: 'john@example.com' })
  .execute()

Next.js Integration

App Router with Server Actions

TSapp/actions/users.ts
TypeScript
// app/actions/users.ts
'use server'

import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { revalidatePath } from 'next/cache'

export async function getUsers() {
  return db.select().from(users)
}

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string

  await db.insert(users).values({ name, email })

  revalidatePath('/users')
}

export async function deleteUser(id: number) {
  await db.delete(users).where(eq(users.id, id))

  revalidatePath('/users')
}
TSapp/users/page.tsx
TypeScript
// app/users/page.tsx
import { getUsers } from '@/app/actions/users'
import { UserForm } from './user-form'
import { UserList } from './user-list'

export default async function UsersPage() {
  const users = await getUsers()

  return (
    <div>
      <h1>Users</h1>
      <UserForm />
      <UserList users={users} />
    </div>
  )
}
TSapp/users/user-form.tsx
TypeScript
// app/users/user-form.tsx
'use client'

import { createUser } from '@/app/actions/users'

export function UserForm() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Add User</button>
    </form>
  )
}

API Routes

TSapp/api/users/route.ts
TypeScript
// app/api/users/route.ts
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'

export async function GET() {
  try {
    const result = await db.select().from(users)
    return NextResponse.json(result)
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch users' },
      { status: 500 }
    )
  }
}

export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { name, email } = body

    const result = await db
      .insert(users)
      .values({ name, email })
      .returning()

    return NextResponse.json(result[0], { status: 201 })
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create user' },
      { status: 500 }
    )
  }
}
TSapp/api/users/[id]/route.ts
TypeScript
// app/api/users/[id]/route.ts
import { db } from '@/db'
import { users } from '@/db/schema'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await db
    .select()
    .from(users)
    .where(eq(users.id, parseInt(params.id)))
    .limit(1)

  if (!user.length) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 })
  }

  return NextResponse.json(user[0])
}

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const body = await request.json()

  const result = await db
    .update(users)
    .set(body)
    .where(eq(users.id, parseInt(params.id)))
    .returning()

  return NextResponse.json(result[0])
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.delete(users).where(eq(users.id, parseInt(params.id)))

  return new NextResponse(null, { status: 204 })
}

Edge Functions (Vercel, Cloudflare)

Vercel Edge Functions

TSapp/api/edge/route.ts
TypeScript
// app/api/edge/route.ts
import { neon } from '@neondatabase/serverless'

export const runtime = 'edge'

export async function GET() {
  const sql = neon(process.env.DATABASE_URL!)

  const users = await sql`SELECT * FROM users LIMIT 10`

  return Response.json(users)
}

Cloudflare Workers

TSsrc/index.ts
TypeScript
// src/index.ts
import { neon } from '@neondatabase/serverless'

export interface Env {
  DATABASE_URL: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sql = neon(env.DATABASE_URL)

    const url = new URL(request.url)

    if (url.pathname === '/users') {
      const users = await sql`SELECT * FROM users`
      return Response.json(users)
    }

    return new Response('Not Found', { status: 404 })
  },
}
wrangler.toml
TOML
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[vars]
# Set in Cloudflare Dashboard as a secret
# DATABASE_URL = "..."

pgvector - Embeddings and AI

Enabling pgvector

Code
SQL
-- In Neon Console or via SQL
CREATE EXTENSION IF NOT EXISTS vector;

Schema with vectors

TSdb/schema.ts
TypeScript
// db/schema.ts
import { pgTable, serial, text, vector } from 'drizzle-orm/pg-core'

export const documents = pgTable('documents', {
  id: serial('id').primaryKey(),
  content: text('content').notNull(),
  embedding: vector('embedding', { dimensions: 1536 }), // OpenAI ada-002
})

Semantic Search

Code
TypeScript
import { db } from '@/db'
import { documents } from '@/db/schema'
import { sql } from 'drizzle-orm'
import OpenAI from 'openai'

const openai = new OpenAI()

async function getEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: 'text-embedding-ada-002',
    input: text,
  })
  return response.data[0].embedding
}

export async function semanticSearch(query: string, limit = 5) {
  const queryEmbedding = await getEmbedding(query)

  // Drizzle with raw SQL for vector operations
  const results = await db.execute(sql`
    SELECT
      id,
      content,
      1 - (embedding <=> ${queryEmbedding}::vector) as similarity
    FROM documents
    ORDER BY embedding <=> ${queryEmbedding}::vector
    LIMIT ${limit}
  `)

  return results.rows
}

// Adding a document with an embedding
export async function addDocument(content: string) {
  const embedding = await getEmbedding(content)

  await db.insert(documents).values({
    content,
    embedding,
  })
}

RAG Pipeline

TSlib/rag.ts
TypeScript
// lib/rag.ts
import { semanticSearch, addDocument } from './vector-search'
import OpenAI from 'openai'

const openai = new OpenAI()

export async function askQuestion(question: string): Promise<string> {
  // 1. Find relevant documents
  const relevantDocs = await semanticSearch(question, 3)

  // 2. Build context
  const context = relevantDocs
    .map(doc => doc.content)
    .join('\n\n')

  // 3. Generate answer with LLM
  const response = await openai.chat.completions.create({
    model: 'gpt-4-turbo-preview',
    messages: [
      {
        role: 'system',
        content: `Answer questions based on the provided context.
                  If the answer is not in the context, say so.

                  Context:
                  ${context}`,
      },
      {
        role: 'user',
        content: question,
      },
    ],
  })

  return response.choices[0].message.content || ''
}

Point-in-Time Recovery

Restoring to a point in time

Code
Bash
# Via CLI
neonctl branches create \
  --name recovery-branch \
  --parent main \
  --point-in-time "2024-01-15T14:30:00Z"

Via API

Code
TypeScript
const response = await fetch('https://console.neon.tech/api/v2/projects/{project_id}/branches', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${NEON_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    branch: {
      name: 'recovery-branch',
      parent_id: 'main',
    },
    endpoints: [
      {
        type: 'read_write',
      },
    ],
    // Restore to a specific moment
    parent_timestamp: '2024-01-15T14:30:00Z',
  }),
})

Connection Pooling

Neon offers a built-in connection pooler (PgBouncer):

Code
TEXT
# Pooled connection (for serverless)
postgresql://user:pass@ep-xxx.pooler.region.aws.neon.tech/dbname?sslmode=require

# Direct connection (for migrations)
postgresql://user:pass@ep-xxx.region.aws.neon.tech/dbname?sslmode=require

Application configuration

Code
TypeScript
// For serverless (most use cases)
const pooledUrl = process.env.DATABASE_URL // use pooled

// For migrations (you need a direct connection)
const directUrl = process.env.DIRECT_DATABASE_URL
Code
ENV
# .env
DATABASE_URL="postgresql://user:pass@ep-xxx.pooler.us-east-2.aws.neon.tech/neondb?sslmode=require"
DIRECT_DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"

Monitoring and metrics

Neon Console

  • Compute usage - Active compute units
  • Storage - Disk usage per branch
  • Connections - Active connections
  • Query performance - Query analysis

Custom monitoring

Code
TypeScript
import { neon } from '@neondatabase/serverless'

const sql = neon(process.env.DATABASE_URL!)

// Connection statistics
const connectionStats = await sql`
  SELECT
    count(*) as total_connections,
    count(*) FILTER (WHERE state = 'active') as active,
    count(*) FILTER (WHERE state = 'idle') as idle
  FROM pg_stat_activity
`

// Table sizes
const tableSizes = await sql`
  SELECT
    relname as table_name,
    pg_size_pretty(pg_total_relation_size(relid)) as total_size,
    pg_size_pretty(pg_relation_size(relid)) as data_size
  FROM pg_catalog.pg_statio_user_tables
  ORDER BY pg_total_relation_size(relid) DESC
`

// Slow queries
const slowQueries = await sql`
  SELECT
    query,
    calls,
    mean_exec_time,
    total_exec_time
  FROM pg_stat_statements
  ORDER BY mean_exec_time DESC
  LIMIT 10
`

Security

Roles and permissions

Code
SQL
-- Create a role for the application
CREATE ROLE app_user WITH LOGIN PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE neondb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;

-- Read-only role for analytics
CREATE ROLE analytics_user WITH LOGIN PASSWORD 'analytics_pass';
GRANT CONNECT ON DATABASE neondb TO analytics_user;
GRANT USAGE ON SCHEMA public TO analytics_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics_user;

Row Level Security

Code
SQL
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Policy: user can only see their own posts
CREATE POLICY user_posts ON posts
  FOR ALL
  USING (author_id = current_setting('app.user_id')::int);

-- Policy: public posts visible to everyone
CREATE POLICY public_posts ON posts
  FOR SELECT
  USING (is_public = true);
Code
TypeScript
// Setting user context
const sql = neon(process.env.DATABASE_URL!)

async function getUserPosts(userId: number) {
  await sql`SELECT set_config('app.user_id', ${userId.toString()}, false)`
  return sql`SELECT * FROM posts`
}

Pricing

Free Tier

  • Compute: 0.25 vCPU, 1GB RAM
  • Storage: 512MB
  • Branching: Unlimited
  • Projects: 1
  • Price: $0/month

Launch

  • Compute: Up to 4 vCPU
  • Storage: 10GB included
  • Compute hours: 300h/month included
  • Price: $19/month

Scale

  • Compute: Up to 8 vCPU, autoscaling
  • Storage: 50GB included
  • Compute hours: 750h/month included
  • Price: $69/month

Business

  • Compute: Custom
  • Storage: 500GB+
  • SLA: 99.95%
  • Support: Priority
  • Price: Custom

Pay-as-you-go pricing

  • Compute: $0.102/compute hour
  • Storage: $0.000164/GiB-hour (~$0.12/GiB-month)
  • Data transfer: Included in price

Best Practices

1. Use branching for development

Code
Bash
# Each developer has their own branch
neonctl branches create --name dev-john
neonctl branches create --name dev-anna

# Each PR has its own branch
neonctl branches create --name pr-${PR_NUMBER}

2. Connection pooling for serverless

Code
TypeScript
// Always use a pooled connection in serverless
const sql = neon(process.env.DATABASE_URL!) // pooled URL

3. Optimize cold starts

Code
TypeScript
// Prewarming via a scheduled function
// Run every 4 minutes to keep the compute active
export async function warmup() {
  const sql = neon(process.env.DATABASE_URL!)
  await sql`SELECT 1`
}

4. Indexes for performance

Code
SQL
-- Indexes on frequently used columns
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_posts_author ON posts(author_id);

-- Partial index for active records
CREATE INDEX CONCURRENTLY idx_active_users ON users(id) WHERE is_active = true;

-- Index for full-text search
CREATE INDEX CONCURRENTLY idx_posts_content_gin ON posts USING gin(to_tsvector('english', content));

5. Monitor and optimize

Code
SQL
-- Find missing indexes
SELECT
  relname,
  seq_scan,
  idx_scan,
  seq_scan - idx_scan as diff
FROM pg_stat_user_tables
WHERE seq_scan > idx_scan
ORDER BY diff DESC;

-- Query analysis
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com';

Common problems and solutions

Cold start timeout

Code
TypeScript
// Problem: Query timeout during cold start

// Solution: Increase timeout and add retry
const sql = neon(process.env.DATABASE_URL!, {
  fetchOptions: {
    timeout: 10000, // 10 seconds
  },
})

async function queryWithRetry(query: string, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await sql(query)
    } catch (error) {
      if (i === retries - 1) throw error
      await new Promise(resolve => setTimeout(resolve, 1000))
    }
  }
}

Connection limit exceeded

Code
TypeScript
// Problem: Too many connections

// Solution: Use pooled connection string
// Instead of: ep-xxx.us-east-2.aws.neon.tech
// Use: ep-xxx.pooler.us-east-2.aws.neon.tech

Branch sync issues

Code
Bash
# Problem: Branch outdated relative to main

# Solution: Create a new branch
neonctl branches delete dev-old
neonctl branches create --name dev-new --parent main

FAQ - Frequently asked questions

Is Neon production-ready?

Yes. Neon is used by thousands of companies in production. It offers a 99.95% SLA on Business plans.

How long does a cold start take?

Typically 300-500ms. You can avoid it through scheduled warming or paid "always on" compute.

Can I use all PostgreSQL extensions?

Most popular extensions are available: pgvector, PostGIS, pg_trgm, hstore, uuid-ossp. The full list is available in the Neon documentation.

How to migrate from RDS/Supabase?

  1. Export data: pg_dump
  2. Create a Neon project
  3. Import: psql < dump.sql
  4. Update the connection string in your application

Is the data safe?

  • All connections TLS encrypted
  • Data at-rest encrypted (AES-256)
  • SOC 2 Type II certified
  • GDPR compliant

Summary

Neon is a groundbreaking approach to PostgreSQL in the cloud, offering:

  • Serverless with true scale-to-zero - Pay only for usage
  • Git-like branching - Isolated environments in seconds
  • Lightning-fast cold starts - Ideal for serverless
  • Full PG compatibility - No compromises
  • Modern integrations - Drizzle, Prisma, Next.js

Neon is ideal for startups, side projects, and modern serverless applications where flexibility and cost are key.