Neon is expanding into a backend: Object Storage, Functions, and AI Gateway now in beta
/APIs & SDKs/neon.ts

neon.ts

Configuration as code for your Neon project.

neon.ts is a TypeScript config file you commit to your repository. It declares which Neon services exist on your project and how each branch is configured.

Specifically:

  • Declares services: which Neon services (auth, dataApi, preview services) exist on the project and are available on every branch.
  • Configures branches: optional per-branch tuning (TTLs, compute sizing, protected status) via a branch closure.

Services and branch policy are independent. Use one, the other, or both.

npm install @neon/config

The package source is on GitHub.

neon.ts itself is declarative: it only describes the policy. neon config / neon deploy (below) are how the CLI runs it. To call the same inspect / plan / apply logic from your own script or CI job instead of the CLI, see @neon/config-runtime.

Link your working directory to a Neon project before using neon.ts commands:

neon link

Config structure

neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  // Services: what exists on every branch
  auth: true,

  // Branch policy: per-branch tuning
  branch: (branch) => {
    if (branch.isDefault) {
      // Default branch: no overrides, uses project defaults
      return {};
    }
    if (!branch.exists) {
      // New non-default branches: auto-expire
      return { ttl: "7d" };
    }
    // Existing branch: no changes
    return {};
  },
});

defineConfig takes two optional parts:

  • Static fields (auth, dataApi, preview): declare which services exist. Same set on every branch.
  • branch closure: receives a read-only BranchTarget and returns per-branch tuning. It can adjust settings, but can't add or remove services.

Branch policy

The branch closure works on any Neon project. The examples below configure the default branch and apply TTL and compute to new branches at creation. Returning {} for existing branches is deliberate: it avoids overwriting settings on branches already in use:

neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  branch: (branch) => {
    if (branch.isDefault) {
      // Default branch: no overrides, uses project defaults
      return {};
    }
    if (!branch.exists) {
      // New non-default branches: minimum compute, auto-expire
      return {
        ttl: "7d",
        postgres: {
          computeSettings: {
            autoscalingLimitMinCu: 0.25,
            autoscalingLimitMaxCu: 0.25,
          },
        },
      };
    }
    // Existing branch: no changes
    return {};
  },
});

On paid plans, you can also protect the default branch and control suspend timeouts:

neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  branch: (branch) => {
    if (branch.isDefault) {
      // Protect and size for production
      return {
        protected: true,
        postgres: {
          computeSettings: {
            autoscalingLimitMinCu: 0.5,
            autoscalingLimitMaxCu: 4,
          },
        },
      };
    }
    if (!branch.exists) {
      // New non-default branches: minimum compute, auto-expire, suspend on idle
      return {
        ttl: "7d",
        postgres: {
          computeSettings: {
            autoscalingLimitMinCu: 0.25,
            autoscalingLimitMaxCu: 0.25,
            suspendTimeout: "5m",
          },
        },
      };
    }
    // Existing branch: no changes
    return {};
  },
});

Run neon deploy to apply. When neon checkout creates a new branch, the closure runs with branch.exists === false, so TTL, compute settings, and services take effect at creation. Checking out an existing branch doesn't apply or reconcile the policy.

BranchTarget fields

FieldTypeDescription
namestringBranch name
idstring?Branch ID. Not set during pre-create evaluation
existsbooleanfalse during pre-create evaluation
isDefaultboolean?Whether this is the project's default branch. Not set during pre-create evaluation
isProtectedboolean?Whether the branch is marked protected in Neon. Not set during pre-create evaluation
parentIdstring?ID of the parent branch. Not always present
expiresAtstring?Branch expiry timestamp. Not always present

BranchTuning fields

FieldTypeDescription
parentstringParent branch name or ID
protectedbooleanMark the branch as protected
ttlstring | numberBranch lifetime: "7d", "2h", or seconds as a number. Maximum 30 days. Validated at deploy time, not by TypeScript
postgres.computeSettings.autoscalingLimitMinCu0.25 | 0.5 | 1 | 2 | 4 | 8Minimum compute units
postgres.computeSettings.autoscalingLimitMaxCu0.25 | 0.5 | 1 | 2 | 4 | 8Maximum compute units
postgres.computeSettings.suspendTimeoutfalse | string | numberIdle suspend timeout. false disables suspend

Services

auth and dataApi declare which Neon services exist on every branch. After neon deploy, running neon env pull writes their URLs to your local .env file automatically.

FieldValuesDefaultWhat it enables
authtrue, false, { enabled: bool }falseManaged Better Auth. Injects NEON_AUTH_BASE_URL, NEON_AUTH_JWKS_URL
dataApitrue, false, DataApiConfigfalseNeon Data API. Injects NEON_DATA_API_URL

dataApiconfig

dataApi: true uses Managed Better Auth as the JWT verifier (the default). When using this form, auth: true must also be set. Omitting it raises a TypeScript error at the dataApi field that includes the fix:

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' }`"'

To use the Data API with an external identity provider instead, pass the object form:

dataApi: {
  authProvider: "external",
  jwksUrl: "https://your-idp/.well-known/jwks.json",
}

Type-safe environment variables

@neon/env gives you type-safe access to your branch's injected variables. It reads process.env at runtime and validates each variable against the services declared in your neon.ts config. Missing or empty variables throw with a clear error.

npm install @neon/env
import { parseEnv } from '@neon/env';
import config from './neon';

const env = parseEnv(config);

env.postgres.databaseUrl;         // DATABASE_URL
env.postgres.databaseUrlUnpooled; // DATABASE_URL_UNPOOLED
env.auth.baseUrl;                 // NEON_AUTH_BASE_URL  (env.auth only present when auth: true)
env.auth.jwksUrl;                 // NEON_AUTH_JWKS_URL
env.dataApi.url;                  // NEON_DATA_API_URL   (env.dataApi only present when dataApi is enabled)

env.auth only exists when auth: true, env.dataApi only when dataApi is enabled. If you access a namespace your config doesn't declare, TypeScript will catch it.

Pass an array of keys to validate and return only a subset. Useful when a process needs just one or two variables:

const { postgres } = parseEnv(config, ["DATABASE_URL"]);
postgres.databaseUrl; // string (databaseUrlUnpooled is absent)

The key list autocompletes from your config, so selecting a variable from a service you haven't declared is a type error.

CLI commands

CommandWhat it does
neon linkConnect the current directory to a Neon project. Required to use linked branch defaults in other commands
neon deployApply neon.ts to the linked branch (alias for neon config apply)
neon config planPreview what neon deploy would change, without applying
neon config statusShow the current live state of the branch as a neon.ts-shaped config
neon env pullWrite the branch's Neon-managed variables to .env.local (or .env if it already exists)
neon checkoutSwitch to or create a branch; new branches are created from the neon.ts policy (TTL, compute, services)
neon devRun functions locally against the linked branch; watches for changes and hot-reloads

Flags forneon deploy

FlagDefaultDescription
--config(auto)Path to the neon.ts file. When omitted, the CLI walks up from cwd stopping at .git
--env(none)Path to a .env file loaded before neon.ts is evaluated, so function env values resolve from it
--env-pulltruePull the branch's env vars into a local .env after a successful apply (--no-env-pull to skip)
--branchlinked branchTarget branch ID or name
--project-idlinked projectProject ID
--update-existingfalseAuto-confirm overriding existing remote settings
--allow-protectedfalseAuto-confirm applying to a protected branch

Preview services

Beta

Functions, Storage, and AI Gateway are in beta and available only in AWS US East (Ohio) (aws-us-east-2), so create your project there to use them.

Preview services are declared under the preview block. All three are optional and independent:

FieldTypeWhat it enables
preview.functionsRecord of slug → function defNeon Functions. Long-running Node.js compute on the branch
preview.bucketsRecord of name → bucket defNeon Object Storage. S3-compatible object storage, branched with your database
preview.aiGatewaytrue, false, { enabled: bool }Neon AI Gateway. Injects NEON_AI_GATEWAY_TOKEN, NEON_AI_GATEWAY_BASE_URL

preview.functions

Each key is the function's slug, the permanent identifier used in CLI commands and the invocation URL:

preview: {
  functions: {
    "<slug>": {
      name: string,       // display name shown in neon functions list and the console
      source: string,     // path to entry file, relative to neon.ts
      env?: Record<string, string>,
      dev?: {
        port?: number,    // local port for neon dev; fails if taken; auto-assigned if omitted
      },
    },
  },
},

Slugs must match ^[a-z0-9]{1,20}$ and are immutable after first deployment. Because slugs can't use separators, use name for a human-readable label. For example, slug: "myrestapi" with name: "My REST API". See Deploy and manage functions.

env values are resolved at deploy time when neon deploy runs. Reading process.env.X here captures the value in your shell at deploy time, not at function runtime. Every value must be a defined string; use a fallback to avoid a type error:

env: {
  API_KEY: process.env.API_KEY ?? "",
}

Use neon deploy --env .env.production to load a .env file before evaluation. For typed access to these variables inside your function at runtime, see Environment variables.

dev settings apply only to neon dev and never affect deploy.

preview.buckets

preview: {
  buckets: {
    "<name>": {
      access?: "private" | "public_read",  // default: "private"
    },
  },
},

Bucket names follow S3 naming rules. public_read makes objects accessible without credentials at the branch's storage endpoint.

Full stack example

All services combined. neon deploy provisions everything and writes credentials to .env.local.

neon.ts
import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  auth: true,
  dataApi: true,

  preview: {
    aiGateway: true,
    buckets: {
      uploads: {},
    },
    functions: {
      api: {
        name: "API",
        source: "./functions/api.ts",
      },
    },
  },

  branch: (branch) => {
    if (branch.isDefault) {
      // Protect and size for production
      return {
        protected: true,
        postgres: {
          computeSettings: {
            autoscalingLimitMinCu: 0.5,
            autoscalingLimitMaxCu: 4,
          },
        },
      };
    }
    if (!branch.exists) {
      // New non-default branches: minimum compute, auto-expire, suspend on idle
      return {
        ttl: "7d",
        postgres: {
          computeSettings: {
            autoscalingLimitMinCu: 0.25,
            autoscalingLimitMaxCu: 0.25,
            suspendTimeout: "5m",
          },
        },
      };
    }
    // Existing branch: no changes
    return {};
  },
});

Need help?

Join our Discord Server to ask questions or see what others are doing with Neon. For paid plan support options, see Support.

Was this page helpful?
Edit on GitHub