neon.ts is Neon's native Infrastructure-as-Code (IaC) file designed for full-stack TypeScript projects. Unlike traditional IaC tools such as Terraform, Pulumi, or OpenTofu, which require learning a new DSL, managing complex state files, and wiring outputs into your application by hand, neon.ts is integrated into your local development loop. It provisions infrastructure through the Neon CLI (neon), syncs connection strings directly into .env.local, and validates those variables inside your application code with strict TypeScript typing.
With neon.ts, you can:
- Provision Neon services like Postgres, Managed Better Auth, and the Data API directly from your codebase.
- Configure branch policies programmatically, for example, auto-suspending preview branches or applying cost-saving TTLs.
- Generate type-safe environment variables so your application knows exactly which services are available, complete with IDE autocomplete.
- Skip state files entirely, since
neonreads live state directly from your Neon project.
In this guide, you will build a simple application that uses neon.ts to provision Neon services, enforce branch-level compute limits, and generate type-safe environment variables. You will learn how to:
- Define Neon services in code.
- Enforce branch-level compute limits for feature branches.
- Use
@neon/envto access type-safe environment variables. - Automatically provision isolated database environments for each branch.
Prerequisites
Before you begin, ensure you have the following:
- Node.js: Version 22 or later. Download from nodejs.org.
- Neon Account: Sign up for a free Neon account at console.neon.tech.
- Neon CLI: Installed globally (
npm i -g neon) and authenticated (neon auth). Checkout Neon CLI Quickstart for more details.
Initialize the project
Create a new Next.js project by running the following command:
npx create-next-app@latest neon-ts-demo --yes cd neon-ts-demoInstall the Neon config and env packages:
npm install @neon/config @neon/envLink your local project to a Neon project using the Neon CLI:
neon linkFollow the prompts to select an existing Neon project or create a new one. This command establishes the connection between your local environment and your Neon Project.
After linking, you will see a
.neonfile in your project root. This file contains the Neon project ID and other metadata. It is git-ignored by default.Define your infrastructure in
neon.tsCreate a file named
neon.tsin the root of your project directory. This file acts as the blueprint for your Neon services and branching logic:import { defineConfig } from "@neon/config/v1"; export default defineConfig({ // Declare the Neon services you want to provision for your project auth: true, dataApi: true, // Define branch-level policies for your Neon project branch: (branch) => { // For the main branch use a more generous compute profile if (branch.isDefault) { return { // protected: true, postgres: { computeSettings: { autoscalingLimitMinCu: 0.5, autoscalingLimitMaxCu: 2, }, }, }; } // For new feature branches, enforce cost-saving defaults if (!branch.exists) { if (branch.name.startsWith("dev")) { return { ttl: "7d", postgres: { computeSettings: { autoscalingLimitMinCu: 0.25, autoscalingLimitMaxCu: 1 }, }, }; } return { ttl: "2d", postgres: { computeSettings: { autoscalingLimitMinCu: 0.25, autoscalingLimitMaxCu: 0.25 }, }, }; } return {}; }, });What this config does:
The
neon.tsfile defines the Neon services and branch policies for your project:- Services: Enables Postgres, Managed Better Auth and the Neon Data API for your project.
- Production: Allows scaling up to 2 Compute Units (CU). You can additionally mark the main branch as
protectedto prevent accidental deletion by uncommenting theprotected: trueline. Protected branches require a paid plan. Learn more about protected branches. - Development branches (
dev*): Applies strict resource controls to new branches whose name starts withdev: capped at 1 CU, and scheduled for deletion after 7 days to prevent unnecessary costs. - Other new branches: Gets an even more minimal profile with a 2-day TTL and a fixed 0.25 CU compute ceiling.
- Existing branches: Left untouched. Returning
{}for branches that already exist avoids overwriting settings on branches already in use. This is important:neon checkoutonly applies policy when creating a new branch, never when checking out an existing one.
The config above is just a starting point. Every field shown is configurable: compute limits (
autoscalingLimitMinCu,autoscalingLimitMaxCu), idle suspend behavior (suspendTimeout), branch lifetime (ttl), protected status, and more. You can also set aparentbranch for new branches to clone from. See theneon.tsreference for the full list of available fields and their valid values.Type-safe infrastructure validation
If you remove
auth: truewhile keepingdataApi: true, your IDE will instantly throw a TypeScript error on thedataApifield:Type 'true' is not assignable to type '`dataApi` with Managed Better Auth (the default `authProvider: 'neon'`) requires Managed Better Auth, so add `auth: true`. To enable the Data API WITHOUT Managed Better Auth, verify a third-party IdP instead: `dataApi: { authProvider: 'external', jwksUrl: 'https://your-idp/.well-known/jwks.json' }`'Instead of the usual unhelpful
Type 'true' is not assignable to type 'never',neon.tsencodes the actual dependency rule and its fixes directly into the expected type. This means your IDE immediately tells you that the Data API requires Managed Better Auth unless you specify a differentauthProvider, and how to fix it either way.Deploy and sync environment variables
Now that your infrastructure is defined, apply it using the Neon CLI.
Preview what would change with a dry run:
neon config planThis shows a table of pending changes without applying them:
$ neon config plan Planned changes ┌────────┬─────────┬────────────┐ │ Action │ Kind │ Identifier │ ├────────┼─────────┼────────────┤ │ create │ service │ auth │ ├────────┼─────────┼────────────┤ │ create │ service │ dataApi │ └────────┴─────────┴────────────┘ Utilized services: Postgres, Managed Better Auth, Data APIWhen you are ready, apply the changes:
neon deploytip
neon deployis an alias forneon config apply. Useneon config planfirst if you want to preview changes before applying.Conflicting remote state
If your Neon project has different compute settings on the main branch (for example, set from the Neon Console),
neon deploymay fail with:ERROR: pushConfig refused to apply: local config conflicts with remote state.This happens because the CLI will not silently overwrite existing remote settings. To override and apply your
neon.tsconfiguration, pass the--update-existingflag:neon deploy --update-existingFor a full list of available flags, see the neon config reference.
You will see output indicating that the services are being provisioned:
$ neon deploy INFO: → Applying to branch main (br-polished-rain-ajh9uwwj) Applied changes ┌────────┬─────────┬────────────┐ │ Action │ Kind │ Identifier │ ├────────┼─────────┼────────────┤ │ create │ service │ auth │ ├────────┼─────────┼────────────┤ │ create │ service │ dataApi │ └────────┴─────────┴────────────┘ Utilized services: Postgres, Managed Better Auth, Data API INFO: Pulled 6 Neon variables into /home/neon-ts-demo/.env.local: NEON_BRANCH, DATABASE_URL, DATABASE_URL_UNPOOLED, NEON_AUTH_BASE_URL, NEON_AUTH_JWKS_URL, NEON_DATA_API_URLAfter the deploy completes,
neonautomatically updates your.env.localfile with the connection strings and URLs for the services you just provisioned. This ensures that your application can securely access the Neon services without manual configuration.Use type-safe environment variables
Traditional
.envfiles are just strings, making it easy to make a typo or forget a variable.neon.tsfixes this by exporting a strictly typed environment parser that reads your configuration blueprint.Create a new file
env.tsat the root of your project to parse the environment variables from.env.local:import { parseEnv } from "@neon/env/v1"; import config from "./neon"; export const env = parseEnv(config);Because your
neon.tsdeclaredauth: trueanddataApi: true, theenvobject now securely contains typed namespaces for those services.Update your
app/page.tsxto display the Neon configuration:import { env } from "@/env"; export default function Home() { return ( <main className="p-8 font-sans"> <h1 className="text-2xl font-bold mb-6">neon.ts Full-Stack Demo</h1> <div className="space-y-4"> <div className="p-4 border rounded bg-gray-50 dark:bg-gray-900"> <h2 className="font-semibold text-blue-600">Postgres Database</h2> <p className="text-sm font-mono mt-2 break-all"> {env.postgres.databaseUrl} </p> </div> <div className="p-4 border rounded bg-gray-50 dark:bg-gray-900"> <h2 className="font-semibold text-green-600">Managed Better Auth</h2> <p className="text-sm font-mono mt-2"> JWKS URL: {env.auth.jwksUrl} </p> </div> <div className="p-4 border rounded bg-gray-50 dark:bg-gray-900"> <h2 className="font-semibold text-purple-600">Neon Data API</h2> <p className="text-sm font-mono mt-2"> Endpoint: {env.dataApi.url} </p> </div> </div> </main> ); }Run your Next.js development server:
npm run devVisit
http://localhost:3000. You will see your actual Neon configuration loaded. If you were to removeauth: truefromneon.ts, your Next.js build would instantly fail, alerting you thatenv.auth.jwksUrlno longer exists.Demo purposes only
The example above renders connection strings directly on the frontend for demonstration. In a real application, never expose database URLs or credentials to the client. Use
env.postgres.databaseUrlin server-rendered pages, API routes, or server actions, and return only the query results to the frontend.Validating a subset of variables
Not every process needs every environment variable. If you only need the database connection string, pass an array of keys to
parseEnvto validate and return just those:import { parseEnv } from "@neon/env/v1"; import config from "./neon"; const { postgres } = parseEnv(config, ["DATABASE_URL"]); console.log(postgres.databaseUrl);The keys autocomplete from your
neon.tsconfig, so you can only select variables that the services in your config actually enable. This is useful for background jobs, scripts, or API routes that only need a single connection string.The branch-first dev loop
The branch-first dev loop is where
neon.tsbecomes most useful.Imagine you are tasked with building a new feature called "User Profiles". You would initialize a new git branch for the feature:
git checkout -b dev-user-profilesThen, run the Neon CLI to create a new isolated database branch for this feature:
neon checkout dev-user-profilesYou can also run
neon checkoutwithout a name to get an interactive branch picker with a create option.Neon will automatically provision a new isolated database branch for your feature. The following happens automatically:
- Database branch creation: Neon creates an isolated clone of your database using Copy-on-Write.
- Apply Policy: Because of your
neon.tsfile,neonrecognizes this is a new branch. Since the branch name starts withdev, it automatically applies the7dTTL and restricts compute limits to0.25 - 1 CU. - Sync environment:
neonautomatically updates your.env.localfile with the connection string and Auth URLs for this specific branch.
Now you have a completely isolated environment for your feature: a git branch, a database branch, and the correct environment variables. You can immediately start coding. Your app is now talking to your isolated database branch, and any changes you make will not affect the main branch or other developers.
When you are done with the feature development, you can merge your git branch back into
mainand apply the schema changes to the main database branch. After merging, you can delete the feature branch and its associated Neon database branch:git checkout main git merge dev-user-profiles # Apply schema changes to the main database branch # npx drizzle-kit migrate git branch -d dev-user-profiles neon branches delete dev-user-profilesTo confirm the state of your current branch at any time you can run
neon config status(similar togit status), which shows the current branch, itsexpiresAtdate, and the services provisioned for it.
Preview services
Neon is expanding into a broader serverless platform. If you are part of the platform private preview, you can use neon.ts to provision additional primitives, such as running Node.js Functions, S3-compatible Storage, and an AI Gateway.
You can declare these under a preview block in your neon.ts:
preview: {
aiGateway: true,
buckets: {
dev_assets: {}, // private (default)
blog_posts: { access: "public_read" }, // public
},
functions: {
api: {
name: "My API",
source: "./functions/api.ts",
},
},
}Running neon deploy will provision the buckets and deploy the functions, and parseEnv will automatically type your env.aiGateway and env.preview.buckets variables. For local development, you can run neon dev to hot-reload your functions against your linked branch.
Neon Functions and Storage are in beta and available only in AWS US East (Ohio) (aws-us-east-2), so create your project there to use them.
Conclusion
By using neon.ts, you bridge the gap between infrastructure and application code.
- You no longer have to manage out-of-sync
.envfiles or navigate to the Neon Console to copy connection strings. - You can enforce team-wide branch lifecycle rules (like TTLs) in pure TypeScript.
- Your application benefits from strict type safety regarding which services are currently provisioned.
Resources
neon.tsReference- neon CLI Reference
- neon config/deploy Reference
- Branching Overview
- Managed Better Auth
Need help?
Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.








