@rpallas/outcrop-neon
A Neon Postgres branch per preview stack for services built with @rpallas/outcrop. Each preview deployment gets its own database branch created from a parent branch, the connection details land in a Secrets Manager secret owned by the stack, and the branch is deleted with the stack. Base environments (dev, prod, ...) import a shared connection secret instead, so the same code works everywhere.
npm install @rpallas/outcrop-neonPrerequisites
A Neon project. Note its project id (
Project settings→Generalin the Neon console).A Neon API key stored as the shared secret
neon-api-keyby the account baseline (@rpallas/outcrop-account,sharedSecrets: { secrets: { "neon-api-key": {} } }). The baseline generates a placeholder value; replace it out of band once:shaws secretsmanager put-secret-value \ --secret-id platform-dev-neon-api-key \ --secret-string 'napi_...'Either a plain string or a JSON document
{ "apiKey": "napi_..." }is accepted.For base environments, a shared secret
neon-connectionholding the connection details of the environment's own branch (the same JSON shape the construct writes, see below, or any shape your service expects). Previews never read this secret.
Usage
import { PlatformFunction, PlatformStack } from "@rpallas/outcrop";
import { NeonBranch } from "@rpallas/outcrop-neon";
export class OrdersStack extends PlatformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
const db = new NeonBranch(this, "Db", {
projectId: "proud-lake-123456",
parentBranch: "main", // default
database: "neondb", // default
role: "neondb_owner", // default
});
const api = new PlatformFunction(this, "Api", {
entry: "src/api.ts",
environment: { DB_SECRET_ARN: db.connectionSecret.secretArn },
});
db.grantRead(api);
}
}At runtime read the secret (for example with getSecretJson from @rpallas/outcrop-runtime) and use connectionString:
{
"host": "ep-cool-river-123456.eu-central-1.aws.neon.tech",
"pooledHost": "ep-cool-river-123456-pooler.eu-central-1.aws.neon.tech",
"port": 5432,
"database": "neondb",
"user": "neondb_owner",
"password": "...",
"connectionString": "postgresql://neondb_owner:...@ep-cool-river-123456-pooler.eu-central-1.aws.neon.tech:5432/neondb?sslmode=require",
"directConnectionString": "postgresql://...@ep-cool-river-123456.eu-central-1.aws.neon.tech:5432/neondb?sslmode=require",
"pooledConnectionString": "postgresql://...@ep-cool-river-123456-pooler.eu-central-1.aws.neon.tech:5432/neondb?sslmode=require",
"branchId": "br-new-000002",
"projectId": "proud-lake-123456"
}connectionString points at the connection pooler (right for Lambda) unless pooled: false. directConnectionString is always the direct endpoint, which is what migrations and anything that uses session-level features (advisory locks, LISTEN/NOTIFY, prepared statements) should use.
Props
| Prop | Default | Notes |
|---|---|---|
projectId | required | Neon project id |
apiKeySecretName | neon-api-key | Shared secret name resolved through /platform/secrets/{name}/arn |
apiKeySecret | – | Explicit ISecret instead of apiKeySecretName |
parentBranch | main | Name or br-... id. main falls back to the project's default branch |
branchName | <previewId>-<service>-db | Platform naming, kind generic, name db |
database | neondb | |
role | neondb_owner | Role whose password is stored |
createInBaseEnvironment | false | Also create a branch (<service>-db) in non-preview stacks |
baseConnectionSecretName | neon-connection | Shared secret imported when no branch is created |
pooled | true | connectionString uses the pooler host |
suspendTimeoutSeconds | Neon default | Compute auto-suspend timeout for the branch endpoint |
apiBaseUrl | https://console.neon.tech/api/v2 |
What the construct exposes
connectionSecret: ISecret– the stack's own secret in previews, the shared secret otherwiseisBranch: boolean– whether this deployment manages a branchbranchId,host,pooledHost– deploy-time tokens (undefinedwhen no branch is created)handler,resource– the custom resource pieces, for extra permissions or dependenciesgrantRead(grantee)– read access to the connection secret- A stack output
<Id>ConnectionSecretArn(forDbabove:DbConnectionSecretArn) so CI can find the secret after a deploy
How previews map to branches
| Stack | Branch | Secret |
|---|---|---|
Preview pr-42 of orders | pr-42-orders-db created from main | pr-42-orders-neon-connection (created) |
dev / prod (default) | none | shared neon-connection (imported) |
dev with createInBaseEnvironment | orders-db created from main | orders-neon-connection (created) |
The custom resource:
- reads the API key, resolves the parent branch id and creates the branch with a
read_writeendpoint (POST /projects/{id}/branches), polling the returned operations until they finish; - reads the role password (
reveal_password, falling back toreset_passwordwhen revealing is disabled for the project) and writes the JSON above withPutSecretValue. The secret value never appears in a CloudFormation template or in the custom resource response; - deletes the branch when the stack is destroyed (a branch that is already gone is not an error).
A Create that finds a branch with the same name adopts it instead of failing, so a retried deploy after a timeout converges. Changing branchName (or projectId) creates a new branch and returns a new physical id; CloudFormation then deletes the old branch. A branch's parent is immutable in Neon, so changing only parentBranch is ignored with a warning – rename the branch as well.
Cost notes
Neon bills compute time and storage. Preview branches share storage with their parent (copy-on-write) and their compute auto-suspends when idle, so an idle preview costs close to nothing. Use suspendTimeoutSeconds to suspend sooner, and make sure preview stacks are destroyed when pull requests close (the platform workflows do this) so branches do not accumulate. Every branch counts towards the project's branch limit.
Running migrations after a preview deploy
The deploy job exposes stack outputs; use the connection secret output to run migrations with the direct endpoint:
- name: Deploy preview
id: deploy
run: npx cdk deploy --require-approval never --outputs-file cdk-outputs.json
- name: Run migrations
env:
STACK_NAME: ${{ steps.deploy.outputs.stack-name }}
run: |
SECRET_ARN=$(jq -r ".[\"$STACK_NAME\"].DbConnectionSecretArn" cdk-outputs.json)
export DATABASE_URL=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" \
--query SecretString --output text | jq -r .directConnectionString)
npm run migrateThe deploy role needs secretsmanager:GetSecretValue on the stack's secrets for this step.
Limitations
- One branch per construct; a stack can create several by using different
branchNames. - The handler requires outbound internet access to the Neon API. If the function runs inside a VPC it needs a NAT gateway or equivalent.
- Only the password of
roleis stored. Additional roles or databases are not created. parentBranchis resolved by name at create time; renaming the parent later has no effect.- Neon API rate limits apply; the handler retries nothing beyond polling operations, and a failed API call fails the deployment so CloudFormation can roll back.
Classes
NeonBranch
Defined in: neon-branch.ts:62
A Neon Postgres branch per preview stack. The branch is created from parentBranch by a custom resource, its connection details are written to a Secrets Manager secret owned by the stack and the branch is deleted with the stack. Base environments import the shared connection secret instead unless createInBaseEnvironment is set.
Extends
Construct
Constructors
Constructor
new NeonBranch(
scope,
id,
props
): NeonBranch;Defined in: neon-branch.ts:86
Parameters
| Parameter | Type |
|---|---|
scope | Construct |
id | string |
props | NeonBranchProps |
Returns
Overrides
Construct.constructorProperties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
branchId | readonly | string | undefined | Neon branch id (deploy-time token); undefined when no branch is created. | neon-branch.ts:70 |
branchName | readonly | string | undefined | Effective branch name (only when a branch is created). | neon-branch.ts:78 |
connectionSecret | readonly | ISecret | Secret holding { host, pooledHost, port, database, user, password, connectionString, directConnectionString, pooledConnectionString, branchId, projectId }. connectionString uses the pooler host when pooled is true (the default). | neon-branch.ts:68 |
handler | readonly | PlatformFunction | undefined | Custom resource handler (only when a branch is created). | neon-branch.ts:80 |
host | readonly | string | undefined | Endpoint host of the branch (deploy-time token); undefined when no branch is created. | neon-branch.ts:72 |
isBranch | readonly | boolean | True when this construct manages a branch, false when it imports the shared secret. | neon-branch.ts:76 |
pooled | readonly | boolean | Whether connectionString consumers should use the pooler host. | neon-branch.ts:84 |
pooledHost | readonly | string | undefined | Pooler host of the branch (deploy-time token); undefined when no branch is created. | neon-branch.ts:74 |
resource | readonly | | PlatformCustomResource<NeonBranchResourceProperties> | undefined | The custom resource (only when a branch is created). | neon-branch.ts:82 |
Methods
grantRead()
grantRead(grantee): Grant;Defined in: neon-branch.ts:174
Allow grantee to read the connection secret.
Parameters
| Parameter | Type |
|---|---|
grantee | IGrantable |
Returns
Grant
Interfaces
NeonBranchProps
Defined in: neon-branch.ts:19
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
apiBaseUrl? | readonly | string | Neon API base URL. Default https://console.neon.tech/api/v2. | neon-branch.ts:52 |
apiKeySecret? | readonly | ISecret | Explicit API key secret instead of apiKeySecretName. | neon-branch.ts:28 |
apiKeySecretName? | readonly | string | Name of the shared secret holding the Neon API key, published by the account baseline under /platform/secrets/{name}/arn. Default neon-api-key. | neon-branch.ts:26 |
baseConnectionSecretName? | readonly | string | Shared secret with the base environment connection details, used when no branch is created. Default neon-connection. | neon-branch.ts:46 |
branchName? | readonly | string | Branch name. Default <previewId>-<service>-db (platform naming, kind generic, name db). | neon-branch.ts:32 |
createInBaseEnvironment? | readonly | boolean | Create a branch in non-preview stacks too. Default false: base environments import the shared baseConnectionSecretName secret instead. | neon-branch.ts:41 |
database? | readonly | string | Database name inside the branch. Default neondb. | neon-branch.ts:34 |
parentBranch? | readonly | string | Branch the preview branch is created from (name or br-... id). Default main. | neon-branch.ts:30 |
pooled? | readonly | boolean | Prefer the connection pooler host in connectionString. Default true. | neon-branch.ts:48 |
projectId | readonly | string | Neon project id (Project settings in the Neon console). | neon-branch.ts:21 |
role? | readonly | string | Role whose password is stored in the connection secret. Default neondb_owner. | neon-branch.ts:36 |
suspendTimeoutSeconds? | readonly | number | Compute auto-suspend timeout for the branch endpoint, in seconds. | neon-branch.ts:50 |
NeonBranchResourceData
Defined in: handlers/neon-branch.ts:30
Attributes returned to CloudFormation (Fn::GetAtt). Never contains credentials.
Extends
Record<string,unknown>
Indexable
[key: string]: unknownProperties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
BranchId | readonly | string | handlers/neon-branch.ts:31 |
EndpointId | readonly | string | handlers/neon-branch.ts:32 |
Host | readonly | string | handlers/neon-branch.ts:33 |
PooledHost | readonly | string | handlers/neon-branch.ts:34 |
NeonBranchResourceProperties
Defined in: handlers/neon-branch.ts:12
Properties the NeonBranch construct passes to the custom resource. CloudFormation delivers every scalar as a string, so numbers are strings here.
Extends
Record<string,unknown>
Indexable
[key: string]: unknownProperties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
apiBaseUrl | readonly | string | - | handlers/neon-branch.ts:23 |
apiKeySecretArn | readonly | string | Secrets Manager ARN holding the Neon API key. | handlers/neon-branch.ts:22 |
branchName | readonly | string | - | handlers/neon-branch.ts:16 |
connectionSecretArn | readonly | string | Secrets Manager ARN that receives the connection details. | handlers/neon-branch.ts:20 |
database | readonly | string | - | handlers/neon-branch.ts:17 |
parentBranch | readonly | string | Parent branch name or id (br-...). | handlers/neon-branch.ts:15 |
pooled? | readonly | string | "true" when connectionString should use the pooler host. | handlers/neon-branch.ts:25 |
projectId | readonly | string | - | handlers/neon-branch.ts:13 |
role | readonly | string | - | handlers/neon-branch.ts:18 |
suspendTimeoutSeconds? | readonly | string | - | handlers/neon-branch.ts:26 |
NeonConnectionSecret
Defined in: handlers/neon-branch.ts:38
JSON document written to the connection secret.
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
branchId | readonly | string | - | handlers/neon-branch.ts:49 |
connectionString | readonly | string | Pooled or direct connection string depending on the construct's pooled option. | handlers/neon-branch.ts:46 |
database | readonly | string | - | handlers/neon-branch.ts:42 |
directConnectionString | readonly | string | - | handlers/neon-branch.ts:47 |
host | readonly | string | - | handlers/neon-branch.ts:39 |
password | readonly | string | - | handlers/neon-branch.ts:44 |
pooledConnectionString | readonly | string | - | handlers/neon-branch.ts:48 |
pooledHost | readonly | string | - | handlers/neon-branch.ts:40 |
port | readonly | number | - | handlers/neon-branch.ts:41 |
projectId | readonly | string | - | handlers/neon-branch.ts:50 |
user | readonly | string | - | handlers/neon-branch.ts:43 |
Variables
NEON_DEFAULT_API_BASE_URL
const NEON_DEFAULT_API_BASE_URL: "https://console.neon.tech/api/v2" = "https://console.neon.tech/api/v2";Defined in: neon-branch.ts:15
NEON_DEFAULT_API_KEY_SECRET
const NEON_DEFAULT_API_KEY_SECRET: "neon-api-key" = "neon-api-key";Defined in: neon-branch.ts:16
NEON_DEFAULT_CONNECTION_SECRET
const NEON_DEFAULT_CONNECTION_SECRET: "neon-connection" = "neon-connection";Defined in: neon-branch.ts:17