Neon, Drizzle, and Astro Environment Variables
Neon is used as a hosted PostgreSQL database. It provides a normal PostgreSQL connection string while handling serverless database infrastructure such as scale-to-zero, branching, pooled connections, and direct connections.
Drizzle ORM is used at runtime to communicate with the database in a type-safe way. Instead of writing all queries as raw SQL strings, Drizzle allows database tables to be defined in TypeScript and reused when selecting, inserting, updating, or deleting data.
Drizzle Kit is separate from Drizzle ORM. Drizzle ORM is used by the application while it runs. Drizzle Kit is a development tool used to generate and run database migrations. Migrations are versioned database structure changes, such as creating tables, adding columns, or adding indexes. They should be run intentionally during development or deployment, not from normal app actions.
A Neon setup commonly uses two connection strings:
DATABASE_URL="pooled Neon connection string"
DATABASE_DIRECT_URL="direct Neon connection string"
DATABASE_URL is the pooled connection string. It is used by the Astro app at runtime for normal queries from pages, server code, and Astro Actions.
DATABASE_DIRECT_URL is the direct connection string. It is used by Drizzle Kit for migrations and other database administration tasks. Migrations should use the direct connection, not the pooled one.
In Astro, database URLs should be treated as server-only secrets. They can be declared in astro.config.mjs with envField:
import { defineConfig, envField } from "astro/config";
export default defineConfig({
env: {
schema: {
DATABASE_URL: envField.string({
context: "server",
access: "secret",
}),
DATABASE_DIRECT_URL: envField.string({
context: "server",
access: "secret",
}),
},
},
});
Because these variables are marked as secret, runtime code should read them with getSecret():
import { getSecret } from "astro:env/server";
const databaseUrl = getSecret("DATABASE_URL");
if (!databaseUrl) {
throw new Error("Missing DATABASE_URL");
}
Drizzle Kit runs outside Astro, so it cannot use astro:env/server. For drizzle.config.ts, dotenv is used as a development dependency so Drizzle Kit can read local .env values:
// drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
if (!process.env.DATABASE_DIRECT_URL) {
throw new Error("Missing DATABASE_DIRECT_URL");
}
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_DIRECT_URL,
},
});
The clean dependency split is:
pnpm add drizzle-orm @neondatabase/serverless
pnpm add -D drizzle-kit dotenv
A clean structure is:
src/
db/
client.ts
schema.ts
actions/
index.ts
drizzle/
generated migration files
drizzle.config.ts
.env
.env.example
astro.config.mjs
Responsibilities:
Neon = hosted PostgreSQL database
Drizzle ORM = runtime TypeScript database queries
Drizzle Kit = migration generation and migration execution
.env = local development secrets
Netlify env vars = production secrets
envField = Astro validation and server-only secret rules
Astro Actions = application operations that call the database
Migrations define the database structure. Drizzle queries the database. Astro Actions validate user input and run application behavior, such as creating a project, saving an update, or submitting a form.