> This page location: APIs & SDKs > Config as code > neon.ts > @neon/config-runtime
> Full Neon documentation index: https://neon.com/docs/llms.txt

> Summary: @neon/config-runtime is the lower-level, scriptable engine behind neon config / neon deploy: inspect, plan, and apply a neon.ts policy directly from Node.js, without going through the neon CLI. Use it to build a custom CI step, a deploy script, or another tool that needs to reconcile a branch against a neon.ts policy.

# @neon/config-runtime

Programmatic API for running a neon.ts policy from your own scripts and CI.

`@neon/config-runtime` is the package the [`neon` CLI](https://neon.com/docs/cli/config) itself uses to run a [`neon.ts`](https://neon.com/docs/reference/neon-ts) policy: read a branch's live state, diff a policy against it, apply the result, bundle and deploy Neon Functions, and register their custom domains. Import it directly when you're writing your own CI step or script and the CLI commands (`neon config plan`, `neon deploy`) don't fit. For example, a custom GitHub Actions job that applies a policy to a freshly created preview branch.

If you just want to run `neon.ts` from the command line, use the [`neon config` / `neon deploy` commands](https://neon.com/docs/cli/config) instead. Reach for this package only when you need to call the same logic from your own Node.js code.

**Note:** `@neon/config-runtime` doesn't read a `.neon` context file, `NEON_*` environment variables, or CLI credential files. Resolve `projectId` and `branchId` yourself, then pass either an API key or a custom `api` adapter. This is different from the `neon` CLI, which resolves project context and credentials for you.

## Install

```bash
npm install @neon/config-runtime
```

Requires Node.js 20.19 or later. Import from the `/v1` subpath to pin a specific major version:

```ts
import { inspect, plan, apply } from "@neon/config-runtime/v1";
```

This package pulls in `esbuild` (a native binary) to bundle Neon Functions for deploy, so it belongs in a CLI, CI job, or deploy script, not in `neon.ts` itself. `neon.ts` should only ever import [`@neon/config`](https://neon.com/docs/reference/neon-ts), which has no native dependencies.

## Resolving project and branch ids

Every function on this page takes `projectId` and a Neon branch id (`br-…`), not a branch name. Look these up yourself before calling in:

- `neon projects list --output json` and `neon branches list --output json` (the [Neon CLI](https://neon.com/docs/cli))
- The Neon API's [list branches](https://neon.com/docs/reference/api/branches/list-project-branches) endpoint
- The `.neon` context file written by `neon link` (the CLI's own resolution mechanism, if your script runs alongside it)

Passing a branch name where an id is expected fails with a `PLATFORM_BRANCH_NOT_FOUND` error (see [Error handling](https://neon.com/docs/reference/config-runtime#error-handling)).

## Quick example

```ts
import config from "../neon";
import { apply, inspect, plan } from "@neon/config-runtime/v1";

const target = {
  projectId: "solitary-fog-12345678",
  branchId: "br-cool-forest-12345678",
  apiKey: process.env.NEON_API_KEY!,
};

const live = await inspect(target); // read the branch's current live state
const diff = await plan(config, target); // dry-run: what would apply change?
const result = await apply(config, target); // apply the policy for real
```

`apply` throws if the branch already has settings your policy would override. See [`apply`](https://neon.com/docs/reference/config-runtime#apply) for how to allow that.

## Authentication

None of the functions on this page take a Neon session or browser login. [Create a Neon API key in the Console](https://console.neon.tech/app/settings/api-keys), then pass it in the `apiKey` option, or inject your own `NeonApi` adapter with `api`. The package doesn't read `NEON_API_KEY` or the credentials written by `neon login`. If you omit both `apiKey` and `api`, the operation throws a `PlatformError` with code `PLATFORM_MISSING_API_KEY`.

For `inspect`, `plan`, `apply`, `pullConfig`, and `pushConfig`, the optional `apiHost` selects a non-production API host. If you omit it, the package uses Neon's production API. The package doesn't read `NEON_API_HOST`. `CreateBranchOptions` has no `apiHost` field, so you can't override the host per call to `createBranch`.

In CI, read the secret from your CI environment and pass its value as `apiKey`, as shown in [A custom CI step](https://neon.com/docs/reference/config-runtime#a-custom-ci-step).

Pass `api` instead to inject your own `NeonApi` adapter (used internally for tests; most callers don't need this).

## Operations

The three functions below mirror the Terraform mental model (`inspect` reads state, `plan` previews a diff, `apply` reconciles it) and are what `neon config status` / `plan` / `apply` call internally.

### `inspect`

```ts
function inspect(options: ConfigOperationOptions): Promise<PulledBranchConfig>;
```

Reads a branch's live Neon state and reverse-engineers it into a `neon.ts`-shaped `Config`, plus raw project/branch metadata. Read-only, and never mutates anything.

| `ConfigOperationOptions` field | Type       | Description                                                               |
| ------------------------------ | ---------- | ------------------------------------------------------------------------- |
| `projectId`                    | `string`   | Required.                                                                 |
| `branchId`                     | `string`   | Required. Must already exist on the project; `inspect` never creates one. |
| `apiKey`                       | `string?`  | Required unless you pass `api`.                                           |
| `apiHost`                      | `string?`  | Optional non-production API host. Defaults to Neon's production API.      |
| `api`                          | `NeonApi?` | Inject a custom adapter (mainly for tests).                               |

Returns a `PulledBranchConfig`:

| Field     | Type                                                      | Description                                                                                                                                                 |
| --------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project` | `{ id, name, region, pgVersion, orgId? }`                 | Project metadata.                                                                                                                                           |
| `branch`  | `{ id, name, parent?, isDefault, protected, expiresAt? }` | Branch metadata.                                                                                                                                            |
| `config`  | `Config`                                                  | The branch's state, expressed as a `neon.ts`-shaped policy (static `auth`/`dataApi` toggles plus a `branch` closure carrying its lifecycle/compute tuning). |
| `preview` | `PulledPreview?`                                          | Buckets, functions, and issued credentials on the branch. Omitted entirely when the branch has none.                                                        |

A pulled function is reported as `{ slug, name }` only: the remote has no record of the local `source` file path, so a pulled config can't redeploy a function without you re-adding `source` by hand.

### `plan`

```ts
function plan(config: Config, options: ConfigOperationOptions): Promise<PushResult>;
```

Computes what `apply` would do, without mutating anything (the equivalent of `terraform plan`). Takes the same options as `inspect`.

Returns a `PushResult` with the same shape as [`apply`](https://neon.com/docs/reference/config-runtime#apply). On a dry run, `applied` describes the changes that would be applied; no remote state is modified.

### `apply`

```ts
function apply(config: Config, options: ApplyOptions): Promise<PushResult>;
```

Applies a `neon.ts` policy to an existing branch. Never creates a project or branch: both must already exist (use [`createBranch`](https://neon.com/docs/reference/config-runtime#createbranch) to provision one from a policy). `ApplyOptions` extends `ConfigOperationOptions` with:

| Field                  | Type               | Default | Description                                                                                                                                                          |
| ---------------------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `updateExisting`       | `boolean`          | `false` | Auto-confirm overriding existing remote settings (TTL, `protected`, compute settings). Without it, drift from the branch's current state throws `PushConflictError`. |
| `allowProtectedBranch` | `boolean`          | `false` | Auto-confirm applying to a branch marked protected on Neon (see the note below the table).                                                                           |
| `bundleFunction`       | `FunctionBundler?` | esbuild | Custom bundler for function source. See [Function bundling](https://neon.com/docs/reference/config-runtime#function-bundling).                                       |

`apply` doesn't accept an interactive confirmation callback (that's only on the lower-level [`pushConfig`](https://neon.com/docs/reference/config-runtime#pushconfig)), so it's either non-interactive (pass `updateExisting`/`allowProtectedBranch` up front) or it fails closed: unresolved drift throws `PushConflictError` (see [Error handling](https://neon.com/docs/reference/config-runtime#error-handling)). Note that a protected branch with **no** other drift is not itself blocked by `allowProtectedBranch: false`; that flag only matters together with the interactive `confirm` callback on `pushConfig`.

Returns a `PushResult`:

| Field           | Type                                     | Description                                                                                                                                                                                                                                              |
| --------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `projectId`     | `string`                                 | Target project id.                                                                                                                                                                                                                                       |
| `orgId`         | `string?`                                | Organization id for the target project, when the API returns one.                                                                                                                                                                                        |
| `branchId`      | `string`                                 | Target branch id.                                                                                                                                                                                                                                        |
| `branchName`    | `string`                                 | Target branch name.                                                                                                                                                                                                                                      |
| `dryRun`        | `boolean`                                | `true` for `plan` / `pushConfig({ dryRun: true })`; `applied` then records planned changes only.                                                                                                                                                         |
| `applied`       | `AppliedChange[]`                        | Ordered list of policy changes that were applied or, on dry runs, would be applied. Each entry identifies the changed resource and field.                                                                                                                |
| `conflicts`     | `ConflictReport[]`                       | Conflicts found while comparing local policy with remote state. Empty when the push can proceed.                                                                                                                                                         |
| `customDomains` | `Array<{ domain, slug, cnameTarget? }>?` | Custom domains declared for the branch after the push, or what `apply` would leave in place on a dry run. `cnameTarget` is absent for a not-yet-registered domain on a dry run, and is an empty string when the region has no custom-domains front door. |

### `createBranch`

```ts
function createBranch(config: Config, options: CreateBranchOptions): Promise<CreateBranchResult>;
```

Creates a branch **from** a `neon.ts` policy and brings it up with its declared settings in one step: it calls the Neon API directly to create the branch, then applies the rest of the policy to it with `pushConfig`. This is the flow `neon checkout <new-name>` needs when it creates a new branch, and the CLI calls this function to get it (not the other way around). Concretely, `createBranch` evaluates the policy with `branch.exists: false` (so creation-time tuning gated on `!branch.exists` actually resolves), creates the branch from the policy's `parent` (falling back to the project's default branch), then reconciles the rest of the policy onto it.

| `CreateBranchOptions` field | Type               | Description                                                                                                |
| --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------- |
| `projectId`                 | `string`           | Required.                                                                                                  |
| `branchName`                | `string`           | Required. Must not already exist on the project.                                                           |
| `apiKey`                    | `string?`          | Required unless you pass `api`.                                                                            |
| `api`                       | `NeonApi?`         | Inject a custom adapter.                                                                                   |
| `bundleFunction`            | `FunctionBundler?` | Custom bundler. See [Function bundling](https://neon.com/docs/reference/config-runtime#function-bundling). |

Returns `{ branchId, branchName, result: PushResult }`. Throws a `PlatformError` (`PLATFORM_CONFLICT`) if `branchName` already exists, or (`PLATFORM_BRANCH_NOT_FOUND`) if the policy's `parent` names a branch that doesn't exist on the project.

## Lower-level: `pullConfig` and `pushConfig`

`inspect`, `plan`, and `apply` are thin, intent-revealing wrappers over two lower-level primitives. Reach for these directly when you need control they don't expose: most commonly, an interactive confirmation prompt, or evaluating the `branch` closure as a creation (`branchExists: false`) so creation-time tuning resolves.

`pushConfig` always requires a `branchId` that already exists on the project; `branchExists: false` only changes how the policy is evaluated, not whether the branch has to exist. `pushConfig` never creates a branch, on `dryRun` or otherwise. If the branch doesn't exist yet, use [`createBranch`](https://neon.com/docs/reference/config-runtime#createbranch) instead.

### `pullConfig`

```ts
function pullConfig(options: PullConfigOptions): Promise<PulledBranchConfig>;
```

Functionally identical to [`inspect`](https://neon.com/docs/reference/config-runtime#inspect): `inspect` forwards its options (`projectId`, `branchId`, `api`, `apiKey`, `apiHost`) to `pullConfig` unchanged and returns its result directly, with no other logic in between. The two names exist so call sites can read naturally (`inspect` next to `plan`/`apply`) while the engine module stays named after what it does.

### `pushConfig`

```ts
function pushConfig(config: Config, options: PushConfigOptions): Promise<PushResult>;
```

Returns the same `PushResult` shape as [`apply`](https://neon.com/docs/reference/config-runtime#apply). With `dryRun: true`, `applied` is the ordered list of changes that would be applied, and the function does not mutate remote state.

The engine behind `plan` and `apply`. `PushConfigOptions` is `ApplyOptions` plus:

| Field          | Type                                                           | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| -------------- | -------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `branchExists` | `boolean`                                                      | `true`  | Evaluate the policy's `branch` closure as if the target branch doesn't exist yet (`branch.exists: false`), without changing whether it physically exists on Neon. `createBranch` uses this internally so creation-time tuning (TTL, compute, `parent`) resolves right after provisioning.                                                                                                                                                                              |
| `confirm`      | `(context: PushConfirmContext) => boolean \| Promise<boolean>` | none    | Invoked once, before any mutation, when the push needs confirmation: either the branch is protected (and `allowProtectedBranch` isn't `true`) or applying would override existing settings (and `updateExisting` isn't `true`). Return `true` to proceed; a `false` return throws `PushAbortedError`. Not invoked, and no mutation runs, when the plan has unresolvable conflicts: those throw `PushConflictError` regardless of `confirm`. Never invoked on `dryRun`. |
| `dryRun`       | `boolean`                                                      | `false` | Compute the full plan against live remote state without executing any mutations. `plan(config, target)` is exactly `pushConfig(config, { ...target, dryRun: true, updateExisting: true })`.                                                                                                                                                                                                                                                                            |

`PushConfirmContext` passed to `confirm`:

| Field             | Type      | Description                                                                                                                                                            |
| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `branchName`      | `string`  | Target branch's name.                                                                                                                                                  |
| `protectedBranch` | `boolean` | `true` when the branch is protected on Neon and `allowProtectedBranch` wasn't set.                                                                                     |
| `overrideUpdates` | `boolean` | `true` when the plan would override existing remote settings and `updateExisting` wasn't set. Additive changes (enabling a service for the first time) never set this. |

Use `confirm` to render your own "are you sure?" prompt instead of failing closed with `PushConflictError`, for example in an interactive CLI built on top of this package.

## Function bundling

Deploying a Neon Function means bundling its source into a ZIP archive. By default, `apply`, `pushConfig`, and `createBranch` do this with esbuild:

```ts
type FunctionBundler = (fn: ResolvedFunctionConfig) => Promise<Uint8Array>;

function buildFunctionBundle(fn: ResolvedFunctionConfig): Promise<Uint8Array>;
```

`buildFunctionBundle` is loaded lazily and only when a deploy actually bundles a function, so a caller that never deploys functions, or that supplies its own `bundleFunction`, never pulls esbuild's native binary into their build. Inject a custom bundler via the `bundleFunction` option on `apply` / `pushConfig` / `createBranch` when your runtime can't ship that binary, for example a single-file packaged CLI, or a restricted CI sandbox.

`ResolvedFunctionConfig` (what your bundler receives) has all deploy defaults already applied: `slug`, `name`, `source` (path to the entry file), `env` (resolved key/value pairs), `runtime`, and an optional `dev` block used only by `neon dev`.

**Note:** The `neon` CLI has its own separate `NEON_ESBUILD_PATH` escape hatch for "esbuild not found" errors (see [Deploy and manage Neon Functions](https://neon.com/docs/compute/functions/deploy#deploy-with-neon-functions-deploy)). That variable is read by the CLI's own bundler, not by `buildFunctionBundle` in this package, so it has no effect when you call `@neon/config-runtime` directly. Pass `bundleFunction` instead.

## Load a `neon.ts` file

```ts
function loadConfigFromFile(options?: LoadConfigOptions): Promise<{ config: Config; resolvedPath: string }>;
```

Re-exported from `@neon/config` for convenience. Use it when your script doesn't already have a `Config` object in scope, for example a CI step that runs independently of a bundler that could `import` `neon.ts` directly.

| `LoadConfigOptions` field | Type      | Description                                                             |
| ------------------------- | --------- | ----------------------------------------------------------------------- |
| `path`                    | `string?` | Explicit path to a config file. Takes precedence over the search below. |
| `cwd`                     | `string?` | Starting directory for the upward search. Defaults to `process.cwd()`.  |
| `stopAt`                  | `string?` | Hard ceiling for the upward walk. Defaults to the OS home directory.    |

Without `path`, it walks up from `cwd` looking for `neon.ts` / `neon.mts` / `neon.js` / `neon.mjs`, stopping at the first directory containing `.git` (monorepo-friendly: an intermediate `package.json` doesn't stop the walk).

## A custom CI step

Putting the pieces together: a script that plans and applies a policy against a specific branch, suitable as its own CI job:

```ts filename="scripts/deploy-branch.ts"
import { loadConfigFromFile, plan, apply } from "@neon/config-runtime/v1";

// Resolve these yourself from CI variables, `neon branches list --output json`,
// or the Neon API. This package never reads `.neon` or `NEON_*`.
const projectId = process.env.NEON_PROJECT_ID!;
const branchId = process.env.NEON_BRANCH_ID!;
const apiKey = process.env.NEON_API_KEY!;
const shouldApply = process.env.NEON_APPLY === "true";

const { config } = await loadConfigFromFile();

const target = {
  projectId,
  branchId,
  apiKey,
  updateExisting: true,
  allowProtectedBranch: true,
};

const planned = await plan(config, target);
console.log(`Plan: ${planned.applied.length} change(s) for ${planned.branchName}`);

if (!shouldApply) {
  console.log("Dry run only. Set NEON_APPLY=true to apply this plan.");
  process.exit(0);
}

const result = await apply(config, target);

console.log(`Applied ${result.applied.length} change(s) to ${result.branchName}`);
```

Run it with `tsx` or after compiling with `tsc`, the same as any Node.js script:

```bash
npx tsx scripts/deploy-branch.ts
```

## Error handling

Every error this package throws extends `PlatformError` (also re-exported here from `@neon/config`), which carries a stable `code` string plus optional `details`. Prefer `isPlatformError(err)` over `err instanceof PlatformError`: a `neon.ts` loaded through the internal TypeScript loader ([jiti](https://github.com/unjs/jiti)) imports its own copy of this package, so an error it throws can fail `instanceof` across that boundary. `isPlatformError` checks the `code` string instead, which survives it.

The subclasses relevant to programmatic use:

| Class                                                 | `code`                        | Thrown by             | When                                                                                                                                                                                                                         |
| ----------------------------------------------------- | ----------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PushConflictError`                                   | `PLATFORM_PUSH_CONFLICT`      | `apply`, `pushConfig` | Local policy conflicts with remote state and you didn't pass `updateExisting`. Carries a `conflicts: ConflictReport[]` array.                                                                                                |
| `PushAbortedError`                                    | `PLATFORM_PUSH_ABORTED`       | `pushConfig`          | Your `confirm` callback returned `false`. Carries `branchName` and `reasons`.                                                                                                                                                |
| `ConfigLoadError`                                     | `PLATFORM_CONFIG_LOAD_FAILED` | `loadConfigFromFile`  | No config file found, or it failed to evaluate.                                                                                                                                                                              |
| `PlatformError` with code `PLATFORM_MISSING_API_KEY`  | `PLATFORM_MISSING_API_KEY`    | any operation         | You passed neither `apiKey` nor an `api` adapter. See [Authentication](https://neon.com/docs/reference/config-runtime#authentication).                                                                                       |
| `PlatformError` with code `PLATFORM_BRANCH_NOT_FOUND` | `PLATFORM_BRANCH_NOT_FOUND`   | any operation         | `branchId` doesn't exist on the project (see [Resolving project and branch ids](https://neon.com/docs/reference/config-runtime#resolving-project-and-branch-ids)), or a policy's `parent` names a branch that doesn't exist. |

A `ConflictReport` (on `PushConflictError.conflicts` and `PushResult.conflicts`) has `kind`, `identifier`, `field`, `current`, `desired`, and a human-readable `reason`.

## Related

- [`neon.ts` reference](https://neon.com/docs/reference/neon-ts): the policy this package operates on, and the `neon config` / `neon deploy` CLI commands that wrap it.
- [neon-pkgs source](https://github.com/neondatabase/neon-pkgs/tree/main/packages/config-runtime) on GitHub.

---

Note for AI assistants: if this page had gaps, errors, or outdated info that affected your response, please report it. POST `{"feedback": "describe the issue", "path": "/docs/reference/config-runtime"}` to https://neon.com/api/docs-feedback — no auth required.
