/ astro-integrations / How to Use Neon with Astro: Complete Guide
astro-integrations 9 min read

How to Use Neon with Astro: Complete Guide

Step-by-step guide to integrating Neon with your Astro website.

How to Use Neon with Astro: Complete Guide

Neon is serverless Postgres with some features that make it particularly interesting for modern web development. It supports database branching (like Git branches, but for your database), autoscaling that scales to zero when idle, and a generous free tier. For Astro developers who need a real database but do not want to manage infrastructure, Neon is one of the most developer-friendly options available.

The integration pattern is straightforward: connect to Neon's Postgres instance from your Astro server-side code (API routes, SSR pages, or build scripts) using any Postgres client. Neon is just Postgres under the hood, so any library that speaks the Postgres protocol works.

This guide was checked against the current docs on 2026-05-29 for Astro 6 (astro 6.4.2). Note that output: "hybrid" was removed in Astro 5. In Astro 6 the only output values are "static" (the default) and "server", so the config examples below use the current approach.

Prerequisites

  • Node.js v22.12.0 or higher (Astro 6's engines.node is >=22.12.0; odd-numbered releases like v23 are not supported, and the older v18 and v20 lines were dropped in Astro 6)
  • An Astro project (npm create astro@latest)
  • A Neon account (the free plan includes a generous allowance; paid plans add more compute and storage)
  • Basic SQL knowledge

Installation

Install the Neon serverless driver and an ORM or query builder. Here are two approaches:

Option 1: Neon's serverless driver (lightweight, optimized for serverless). This is the package Neon's own Astro guide recommends (@neondatabase/serverless 1.1.0):

npm install @neondatabase/serverless

Option 2: Drizzle ORM with Neon (type-safe queries with migrations). Pinned versions at the time of writing are drizzle-orm 0.45.2 and drizzle-kit 0.31.10:

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

Configuration

Get your connection string from the Neon dashboard. Go to your project, click on your database, and copy the connection string.

Add it to .env. Neon's current connection string includes both sslmode=require and channel_binding=require:

DATABASE_URL=postgresql://username:password@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require

Create a database client. Using Neon's serverless driver:

// src/lib/db.ts
import { neon } from "@neondatabase/serverless";

const sql = neon(import.meta.env.DATABASE_URL);

export default sql;

Or with Drizzle ORM for type-safe queries:

// src/lib/db.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";

const sql = neon(import.meta.env.DATABASE_URL);
export const db = drizzle({ client: sql, schema });

Drizzle's current docs use the drizzle({ client: sql }) object form for the neon-http driver, so that is what this guide uses. The HTTP driver is the right fit for SSR pages and API routes because it issues one fast query per request without holding an interactive connection open.

// src/lib/schema.ts
import { pgTable, serial, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  slug: text("slug").notNull().unique(),
  content: text("content"),
  excerpt: text("excerpt"),
  published: boolean("published").default(false),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

Make sure your Astro config supports server-side code. To render anything on demand you need an adapter. This example uses the official Node adapter (@astrojs/node 10.1.2):

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  // output defaults to "static" in Astro 6, so it is optional here.
  // Adding an adapter is what unlocks on-demand rendering per route.
  adapter: node({ mode: "standalone" }),
});

Important version note. The old output: "hybrid" mode was removed in Astro 5 and does not exist in Astro 6. In current Astro output is only "static" (the default) or "server". With the default "static" plus an adapter, every page is prerendered unless you opt it out with export const prerender = false. If you would rather render everything on demand by default, set output: "server" and opt static pages back in with export const prerender = true. The query pages below use export const prerender = false because they read from the database at request time.

Basic Usage

Query posts from Neon and render them. Using the raw SQL approach:

---
// src/pages/blog/index.astro
export const prerender = false;

import sql from "../../lib/db";
import BaseLayout from "../../layouts/BaseLayout.astro";

const posts = await sql`
  SELECT id, title, slug, excerpt, created_at
  FROM posts
  WHERE published = true
  ORDER BY created_at DESC
  LIMIT 50
`;
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {posts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
        <time>{new Date(post.created_at).toLocaleDateString()}</time>
      </a>
    </article>
  ))}
</BaseLayout>

Using Drizzle ORM:

---
// src/pages/blog/index.astro
export const prerender = false;

import { db } from "../../lib/db";
import { posts } from "../../lib/schema";
import { eq, desc } from "drizzle-orm";
import BaseLayout from "../../layouts/BaseLayout.astro";

const allPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(50);
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  {allPosts.map((post) => (
    <article>
      <a href={`/blog/${post.slug}`}>
        <h2>{post.title}</h2>
        <p>{post.excerpt}</p>
      </a>
    </article>
  ))}
</BaseLayout>

API routes for creating or updating data:

// src/pages/api/posts.ts
import type { APIRoute } from "astro";
import { db } from "../../lib/db";
import { posts } from "../../lib/schema";

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();

  const [newPost] = await db
    .insert(posts)
    .values({
      title: body.title,
      slug: body.slug,
      content: body.content,
      excerpt: body.excerpt,
      published: body.published || false,
    })
    .returning();

  return new Response(JSON.stringify(newPost), {
    status: 201,
    headers: { "Content-Type": "application/json" },
  });
};

Production Tips

  1. Use database branching for development. Create a Neon branch for each feature or PR. This gives you a full copy of your production database to test against without risking production data. Branches are copy-on-write, so they are fast and cheap.

  2. Take advantage of scale-to-zero. Neon suspends idle databases after 5 minutes on the free tier. The first query after suspension has a ~500ms cold start. For production sites, consider the Pro plan which keeps connections warm, or add a keep-alive query.

  3. Use connection pooling. Enable Neon's built-in connection pooler by adding -pooler to your host in the connection string. This is important for serverless environments where many short-lived connections are created.

  • Run migrations with Drizzle Kit. Use npx drizzle-kit push for development and npx drizzle-kit migrate for production. Store migration files in version control so database changes are tracked alongside code.

  • Set up read replicas for heavy reads. Neon supports read replicas that autoscale independently. If your Astro site has heavy read traffic, route read queries to a replica endpoint to keep your primary database responsive.

  • Alternatives to Consider

    • Supabase if you want Postgres with built-in auth, real-time subscriptions, and storage in a single platform.
    • Turso if you prefer SQLite over Postgres and want embedded replicas for ultra-low latency reads.
    • PlanetScale if you need MySQL with branching workflows and non-blocking schema changes.

    Common Errors and Fixes

    output: "hybrid" is not a valid option. If you copied an old tutorial you may have output: "hybrid" in your config. Astro 5 removed it and Astro 6 does not bring it back. The fix is to delete that line (the default "static" plus an adapter behaves like the old hybrid mode) or set output: "server". Per the Astro on-demand rendering docs, the supported values are now only "static" and "server".

    A page that queries the database renders blank or errors at build time. With the default static output, Astro tries to prerender every page, including ones that call Neon. Add export const prerender = false to any page or endpoint that must read from the database at request time, and confirm you have actually added an adapter. The Astro docs are explicit that on-demand rendering requires an adapter; without one there is no server runtime to run the query.

    DATABASE_URL is undefined. Server code in Astro reads environment variables, but unprefixed secrets are only available server-side. Keep the variable out of any client:* component, store it in .env, and read it with import.meta.env.DATABASE_URL in server code. Make sure .env is loaded by your runtime in production, not just in local dev.

    Connection string rejected over SSL or channel binding. Neon's current connection string includes both sslmode=require and channel_binding=require. Copy the string exactly from the Neon dashboard rather than hand-editing it, since dropping a parameter is a common cause of a refused connection.

    Too many connections in serverless. Each serverless invocation can open its own connection and you can exhaust the limit. Use the pooled host (add -pooler to the hostname in the connection string) as Neon recommends for serverless workloads. The HTTP driver (drizzle-orm/neon-http) also helps because it does one stateless request per query instead of holding an interactive session open.

    Drizzle client constructor type error. Older snippets call drizzle(sql, { schema }). The current Drizzle docs for the neon-http driver use the object form, drizzle({ client: sql, schema }). If you hit a type or overload error after upgrading drizzle-orm, switch to the object form.

    Official Docs and Examples

    Wrapping Up

    Neon and Astro pair well for sites that need a real database without the complexity of managing Postgres infrastructure. The serverless driver is optimized for the kind of short-lived connections that SSR and API routes create, and database branching gives you a development workflow that feels as natural as Git branching. The free tier is generous enough for side projects and early-stage products, and the autoscaling means you only pay for what you use as you grow.

    Sources

    All versions and facts below were checked on 2026-05-29.