diff --git a/docs/docs.json b/docs/docs.json
index e4db782131c..7263daee638 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -129,6 +129,14 @@
}
]
},
+ {
+ "group": "Private networking",
+ "pages": [
+ "private-networking/overview",
+ "private-networking/aws-console-setup",
+ "private-networking/troubleshooting"
+ ]
+ },
{
"group": "Realtime",
"pages": [
diff --git a/docs/guides/frameworks/drizzle.mdx b/docs/guides/frameworks/drizzle.mdx
index 5ea84d9e108..49708a87f62 100644
--- a/docs/guides/frameworks/drizzle.mdx
+++ b/docs/guides/frameworks/drizzle.mdx
@@ -24,6 +24,12 @@ This guide will show you how to set up [Drizzle ORM](https://orm.drizzle.team/)
- Drizzle ORM [installed and initialized](https://orm.drizzle.team/docs/get-started) in your project
- A `DATABASE_URL` environment variable set in your `.env` file, pointing to your PostgreSQL database (e.g. `postgresql://user:password@localhost:5432/dbname`)
+
+ If your Postgres lives in a private AWS VPC (e.g. RDS without a public endpoint), connect it via
+ [Private networking](/private-networking/overview) instead of opening it to the public internet
+ (Pro and Enterprise plans).
+
+
## Initial setup (optional)
Follow these steps if you don't already have Trigger.dev set up in your project.
diff --git a/docs/images/priv-connections-allow-principal.png b/docs/images/priv-connections-allow-principal.png
new file mode 100644
index 00000000000..c088f460137
Binary files /dev/null and b/docs/images/priv-connections-allow-principal.png differ
diff --git a/docs/images/priv-connections-copy-endpoint-name.png b/docs/images/priv-connections-copy-endpoint-name.png
new file mode 100644
index 00000000000..9bca21e1e82
Binary files /dev/null and b/docs/images/priv-connections-copy-endpoint-name.png differ
diff --git a/docs/images/priv-connections-create-endpoint-service.png b/docs/images/priv-connections-create-endpoint-service.png
new file mode 100644
index 00000000000..e01f9a5b16f
Binary files /dev/null and b/docs/images/priv-connections-create-endpoint-service.png differ
diff --git a/docs/images/priv-connections-network-load-balancer-add-target-group.png b/docs/images/priv-connections-network-load-balancer-add-target-group.png
new file mode 100644
index 00000000000..5d02b487fb9
Binary files /dev/null and b/docs/images/priv-connections-network-load-balancer-add-target-group.png differ
diff --git a/docs/images/priv-connections-network-load-balancer-basic.png b/docs/images/priv-connections-network-load-balancer-basic.png
new file mode 100644
index 00000000000..23a499a2a58
Binary files /dev/null and b/docs/images/priv-connections-network-load-balancer-basic.png differ
diff --git a/docs/images/priv-connections-network-load-balancer-vpc-az.png b/docs/images/priv-connections-network-load-balancer-vpc-az.png
new file mode 100644
index 00000000000..f3164aa9333
Binary files /dev/null and b/docs/images/priv-connections-network-load-balancer-vpc-az.png differ
diff --git a/docs/images/priv-connections-target-group-basic.png b/docs/images/priv-connections-target-group-basic.png
new file mode 100644
index 00000000000..a6c8c9fbbe1
Binary files /dev/null and b/docs/images/priv-connections-target-group-basic.png differ
diff --git a/docs/images/priv-connections-target-group-register-nlb.png b/docs/images/priv-connections-target-group-register-nlb.png
new file mode 100644
index 00000000000..72d9f2f65eb
Binary files /dev/null and b/docs/images/priv-connections-target-group-register-nlb.png differ
diff --git a/docs/private-networking/aws-console-setup.mdx b/docs/private-networking/aws-console-setup.mdx
new file mode 100644
index 00000000000..3082bf91aea
--- /dev/null
+++ b/docs/private-networking/aws-console-setup.mdx
@@ -0,0 +1,274 @@
+---
+title: "Setting up PrivateLink in the AWS Console"
+sidebarTitle: "AWS Console setup"
+description: "Step-by-step guide for exposing a resource from your AWS account to Trigger.dev via PrivateLink."
+---
+
+This guide walks through setting up the AWS side of a private connection: a Network Load Balancer (NLB), a target group pointing at the resource you want to expose, and a VPC Endpoint Service that authorizes Trigger.dev to consume it.
+
+
+ Prefer Terraform? Open the "Add connection" page in the Trigger.dev dashboard and use the
+ Terraform wizard to generate a ready-to-apply script. The wizard creates everything described
+ below and pre-fills our AWS account ID for you.
+
+
+## Prerequisites
+
+Before you start you'll need:
+
+- An **AWS account** with permission to create VPC, EC2, and ELB resources
+- A **resource** in a VPC subnet that you want to expose (RDS instance, ElastiCache cluster, internal API, etc.)
+- The **Trigger.dev AWS account ID** — find this on the "Add connection" page in your Trigger.dev dashboard, in the "I have my details" or "Step-by-step guide" cards
+- A **VPC** that contains the resource, with at least one private subnet per Availability Zone you want to serve from
+
+
+ PrivateLink connections are zonal. If your resource lives in a single AZ, your connection will
+ only be available from that AZ. For higher availability, ensure target groups can route to
+ multiple AZs.
+
+
+## Step 1: Create a target group pointing at your resource
+
+The target group is how the NLB will know where to forward traffic. AWS requires a target group when creating a load balancer, so we'll set this up first.
+
+
+
+ Go to **EC2 → Target Groups → Create target group**.
+
+
+ - **IP addresses** for RDS, ElastiCache, or any resource you can reach by IP
+ - **Instances** for EC2 instances you own
+ - **Application Load Balancer** if your resource sits behind an ALB
+
+ For most database use cases, **IP addresses** is correct. NLBs don't support Lambda targets
+ directly — if you need to expose a Lambda, put it behind an ALB and use the ALB target type.
+
+
+
+ - **Name**: e.g. `trigger-postgres-tg`
+ - **Protocol**: TCP
+ - **Port**: the port your resource listens on (5432 for Postgres, 6379 for Redis, 3306 for MySQL, etc.)
+ - **VPC**: the VPC where your resource lives (this must match the VPC you'll use for the NLB)
+ - **Health check protocol**: TCP
+
+ 
+
+
+
+ Add the IP addresses of the resource. For RDS, look up the writer endpoint's IPs (`dig ` from inside the VPC).
+ For ElastiCache, use the primary endpoint IPs.
+
+ 
+
+
+ RDS and ElastiCache endpoints' IP addresses can change after failover or maintenance. For long-lived
+ connections, consider running a small Lambda or sidecar that periodically resolves the DNS name and
+ updates the target group, or use a [DNS-resolved](https://aws.amazon.com/blogs/networking-and-content-delivery/hostname-as-target-for-network-load-balancers/)
+ target if your setup supports it.
+
+
+
+
+ Click **Create target group**.
+
+
+
+## Step 2: Create an internal Network Load Balancer
+
+The NLB is what PrivateLink exposes to Trigger.dev. It must be **internal** (not internet-facing).
+
+
+
+ Go to **EC2 → Load Balancers → Create load balancer** and choose **Network Load Balancer**.
+
+
+ - **Name**: something descriptive, e.g. `trigger-postgres-nlb`
+ - **Scheme**: **Internal**
+ - **IP address type**: IPv4
+
+ 
+
+
+
+ Pick the same VPC as your target group. Select one private subnet per AZ that should serve traffic.
+ Each subnet you select adds an availability zone to the endpoint.
+
+ 
+
+
+
+ Under **Listeners and routing**, configure:
+
+ - **Protocol**: TCP
+ - **Port**: same as your target group port (5432 for Postgres, 6379 for Redis, etc.)
+ - **Default action**: forward to the target group you created in Step 1
+
+ 
+
+
+
+ Click **Create load balancer**. Provisioning takes 1–2 minutes — wait until the NLB's **State**
+ column shows **Active** before moving on. The endpoint service in the next step won't list the
+ NLB until it's fully active.
+
+
+
+
+ Test connectivity from a bastion host or another instance in the same VPC before continuing —
+ e.g. `psql -h -p 5432 -U user -d db`. If the NLB can't reach your resource, the
+ PrivateLink connection won't either.
+
+
+## Step 3: Create a VPC Endpoint Service
+
+This is the resource that PrivateLink consumers connect to.
+
+
+ Confirm the NLB from Step 2 is in the **Active** state before starting this step. It won't appear
+ in the **Available load balancers** dropdown until it has finished provisioning.
+
+
+
+
+ Go to **VPC → Endpoint services → Create endpoint service**.
+
+
+ - **Name**: optional, but useful for identification, e.g. `trigger-postgres-endpoint`
+ - **Load balancer type**: Network
+ - **Available load balancers**: select the NLB you created
+ - **Require acceptance for endpoint**: **No** (recommended)
+
+ 
+
+
+ If you set "Require acceptance" to **Yes**, every connection request from Trigger.dev will
+ sit in a pending state until you manually approve it. Setting it to **No** lets connections
+ come up automatically once the principal is allow-listed.
+
+
+
+
+ Leave the "Private DNS name" option disabled. Trigger.dev tasks dial the endpoint by its
+ private IP, so private DNS isn't needed.
+
+
+ If your Trigger.dev tasks run in a **different AWS region** from your endpoint service, expand
+ the **Supported Regions** section and add the region(s) where Trigger.dev should be allowed to
+ create the VPC Endpoint from (for example, add `eu-central-1` if your service is in
+ `us-east-1` but tasks run in `eu-central-1`).
+
+ If your tasks and resource are in the same region, you can skip this — same-region access is
+ enabled by default.
+
+
+ Cross-region PrivateLink adds AWS data-transfer cost and ~10–30ms of latency depending on the
+ region pair. Prefer same-region when possible.
+
+
+
+
+ Click **Create**. The service is created immediately — you'll come back to copy its **Service
+ name** once you've authorized Trigger.dev in the next step.
+
+
+
+## Step 4: Authorize the Trigger.dev AWS account
+
+By default, no one can connect to your endpoint service. You need to explicitly allow Trigger.dev's AWS account.
+
+
+
+ Go to **VPC → Endpoint services**, select the service you just created.
+
+
+ Click the **Allow principals** tab, then **Allow principals**.
+
+
+ Paste the principal ARN in this format, replacing `` with the Trigger.dev AWS
+ account ID shown in your dashboard:
+
+ ```text
+ arn:aws:iam:::root
+ ```
+
+ 
+
+
+ You will find the correct AWS account ID in the **Add connection** page of the Trigger.dev
+ dashboard. Do not assume an account ID — it differs between Trigger.dev environments.
+
+
+
+
+ The principal is now authorized to create a VPC Endpoint targeting your service.
+
+
+ On the endpoint service detail page, copy the **Service name** value — it looks like
+ `com.amazonaws.vpce.us-east-1.vpce-svc-0123abcd...`. You'll paste this into the Trigger.dev
+ dashboard in the next step.
+
+ 
+
+
+
+
+## Step 5: Add the connection in Trigger.dev
+
+
+
+ In Trigger.dev, go to **Organization Settings → Private Connections** and click **Add
+ connection**.
+
+
+ Then fill in:
+
+ - **Friendly name**: a short, human-readable label for this connection.
+ - **VPC Endpoint Service name**: paste the `com.amazonaws.vpce..vpce-svc-...` value from Step 4.
+ - **Target region**: the AWS region your endpoint service lives in.
+
+
+
+ Submit the form. The connection's status moves through **Pending → Provisioning → Active**.
+ Provisioning typically takes 30–90 seconds.
+
+
+ Once **Active**, the dashboard shows the assigned private IP. Plug it into the
+ connection-string environment variable your task already uses (for example, `DATABASE_URL` set
+ on the **Environment Variables** page) and your tasks will reach the resource over
+ PrivateLink.
+
+
+
+## Troubleshooting
+
+See the dedicated [Troubleshooting](/private-networking/troubleshooting) page for common problems
+such as the "Private link not found" wizard error. A few quick checks specific to this setup flow:
+
+
+ - Confirm Trigger.dev's AWS account ID is in your endpoint service's **Allow principals** list.
+ - Confirm the endpoint service is **Available** in the AWS console.
+ - Confirm "Require acceptance" is set to **No** on the endpoint service. If it's set to **Yes**,
+ the request is sitting in your pending queue and you must approve it manually.
+
+
+
+ - Confirm the NLB has a target registered and the target's health check is passing.
+ - Confirm the listener port matches the port your task code is dialing.
+ - Confirm the security group on your resource allows inbound traffic from the NLB or the VPC's
+ private IP range.
+ - Try connecting from inside the VPC first (e.g., a bastion host) to rule out resource-side
+ issues.
+
+
+
+ - Cross-region connections add ~10–30ms RTT depending on the regions involved. If your tasks run
+ in a different region than your resource, expect higher latency.
+ - The NLB and target group's health checks influence connection setup time. Tighter health check
+ intervals reduce failover time after a backend goes unhealthy.
+
+
+
+ Delete the connection from **Organization Settings → Private Connections** in the Trigger.dev
+ dashboard. We'll tear down our VPC Endpoint and remove the network policy automatically. You can
+ then delete your VPC Endpoint Service, NLB, and target group on the AWS side.
+
diff --git a/docs/private-networking/overview.mdx b/docs/private-networking/overview.mdx
new file mode 100644
index 00000000000..8c54828b71b
--- /dev/null
+++ b/docs/private-networking/overview.mdx
@@ -0,0 +1,144 @@
+---
+title: "Private networking"
+sidebarTitle: "Overview"
+description: "Connect your tasks to private resources in your AWS account using AWS PrivateLink."
+---
+
+Private networking lets your Trigger.dev tasks reach databases, caches, and internal APIs that live inside your own AWS VPC, without exposing them to the public internet. Connectivity is established over [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html), so traffic stays on the AWS backbone.
+
+
+ Private networking is a Pro and Enterprise feature. If you'd like access, [get in touch](/community).
+
+
+## What is AWS PrivateLink
+
+AWS PrivateLink is a managed service that creates a private, one-way connection between two AWS accounts without using the public internet, NAT gateways, internet gateways, or VPN tunnels.
+
+It works by pairing two resources:
+
+- A **VPC Endpoint Service** in the account that owns the resource (yours). This is fronted by a Network Load Balancer (NLB) and exposes whatever ports you choose.
+- A **VPC Endpoint** in the account that wants to consume the resource (Trigger.dev's). The endpoint is an Elastic Network Interface (ENI) inside our VPC with a private IP that your task pods can dial directly.
+
+The connection is unidirectional: only the endpoint side can initiate connections. Your VPC cannot reach into ours.
+
+## What you can use it for
+
+Any TCP service running inside your VPC. Common use cases include:
+
+- **Databases**: PostgreSQL (RDS, Aurora), MySQL, MongoDB, ClickHouse, Redshift
+- **Caches**: ElastiCache Redis, Memcached
+- **Internal APIs**: services on EKS, ECS, EC2, or Lambda exposed through an internal NLB
+- **Message brokers**: self-hosted Kafka, RabbitMQ, NATS
+- **Vector databases and ML services** running in private subnets
+
+If your resource is reachable from a Network Load Balancer in the same VPC, it can be exposed to Trigger.dev via PrivateLink.
+
+## How it works with Trigger.dev
+
+When you add a private connection in the dashboard, the following happens:
+
+
+
+ You create an internal NLB in front of your resource and a VPC Endpoint Service that points to it. You add Trigger.dev's AWS account as an allowed principal so we're permitted to connect.
+
+
+ Once you submit the endpoint service name in the Trigger.dev dashboard, we provision a VPC Endpoint in our AWS account in the region you chose. The endpoint creates an ENI with a private IP that we wire up to reach your service.
+
+
+ Once the connection is **Active**, the dashboard shows the assigned IP. Pods running your tasks are network-authorized to connect to it.
+
+
+
+### Connecting from your task code
+
+When the connection becomes **Active**, the dashboard shows the assigned endpoint IP. Plug it into the connection-string environment variable your task already reads (for example, `DATABASE_URL` set on the **Environment Variables** page):
+
+
+ A private connection is scoped to your **organization**, not to a single environment. The same
+ assigned IP works from any deployed environment your tasks run in — Preview branches, Staging,
+ and Production — so you can set the connection-string env var per environment and point them
+ all at the same private resource. (Local **Development** runs on your own machine, so it can't
+ reach the endpoint IP — use a regular public connection there.)
+
+
+
+```typescript
+import { task } from "@trigger.dev/sdk";
+import { Client } from "pg";
+
+export const queryDatabase = task({
+ id: "query-database",
+ run: async () => {
+ // DATABASE_URL is set in the Trigger.dev dashboard to the connection's
+ // assigned IP shown in Private Connections.
+ const client = new Client({
+ connectionString: process.env.DATABASE_URL,
+ });
+
+ await client.connect();
+ const result = await client.query("SELECT NOW()");
+ await client.end();
+
+ return result.rows;
+ },
+});
+```
+
+## Isolation between organizations
+
+Private networking is set up so that each organization's connections are completely isolated from every other organization. Three layers enforce that:
+
+### 1. Dedicated AWS account
+
+Customer VPC Endpoints are provisioned in a **dedicated AWS account** that is separate from the account that runs Trigger.dev's task workers. The dedicated account does nothing else — it only hosts customer endpoints. This limits the blast radius of any misconfiguration: even a misbehaving endpoint cannot reach worker infrastructure beyond the routes we explicitly define.
+
+### 2. Per-organization network policy
+
+Inside the Kubernetes cluster that runs your tasks, the default network policy denies all traffic to private IP ranges. When your organization creates a connection, we generate a [CiliumNetworkPolicy](https://docs.cilium.io/en/stable/network/kubernetes/policy/) that:
+
+- Targets **only pods labeled with your organization's ID**
+- Allows egress **only to the specific endpoint IPs we provisioned for you**
+
+A pod from another organization has neither the matching label nor a matching policy — its connection attempts to your endpoint IPs are dropped at the network layer before they ever reach an ENI.
+
+### 3. AWS-level authorization
+
+PrivateLink itself enforces a second layer of authorization. Your VPC Endpoint Service has an explicit list of `allowed_principals` — only AWS accounts you list can even establish a connection. Trigger.dev provides each org with the same Trigger.dev AWS account ID, but the AWS account ID alone is useless without the matching CiliumNetworkPolicy on our side. To reach your endpoint, traffic must:
+
+1. Originate from a pod labeled with your org ID (enforced by us)
+2. Match an egress rule with your endpoint's IPs (enforced by us)
+3. Hit a VPC Endpoint Service that has authorized our account (enforced by you)
+
+All three conditions must be true. No organization can route traffic to another organization's resources.
+
+
+ AWS account IDs are not secrets, but the VPC Endpoint Service name is also not enough on its own —
+ you must explicitly add Trigger.dev's account to your endpoint service's allowed principals before
+ any connection works. We'll never see your service unless you authorize us.
+
+
+## Limits
+
+- **Two connections per organization.** This is a soft limit — get in touch if you need more.
+- **All ports accepted.** Our security group allows all TCP ports from peered CIDRs. You control which ports are reachable through your NLB and target groups.
+- **Same-region or cross-region.** Connections work within the same region as your tasks or across regions (cross-region adds AWS-level cost and ~10-30ms latency).
+
+## Next steps
+
+
+
+ Step-by-step instructions for creating the NLB, target group, and VPC Endpoint Service in your
+ AWS account.
+
+
+ Common problems when setting up a private connection and how to resolve them.
+
+
diff --git a/docs/private-networking/troubleshooting.mdx b/docs/private-networking/troubleshooting.mdx
new file mode 100644
index 00000000000..a2c07b63eff
--- /dev/null
+++ b/docs/private-networking/troubleshooting.mdx
@@ -0,0 +1,45 @@
+---
+title: "Troubleshooting private networking"
+sidebarTitle: "Troubleshooting"
+description: "Common problems when setting up an AWS PrivateLink connection to Trigger.dev, and how to resolve them."
+---
+
+This page collects common issues when adding a private connection. If your problem isn't listed here, [get in touch](/community).
+
+## "Private link not found" in the setup wizard
+
+If the setup wizard errors out with **Private link not found** when you submit the VPC Endpoint Service name, it almost always means your endpoint service has not been shared with Trigger.dev's AWS account.
+
+Trigger.dev cannot provision a VPC Endpoint until your endpoint service explicitly authorizes our AWS account as a consumer. Until that happens, the service name is invisible to us — even though the name itself is correct.
+
+### How to fix it
+
+
+
+ Go to **VPC → Endpoint services** in the AWS region where you created the service, and select
+ your service.
+
+
+ Click the **Allow principals** tab and check whether Trigger.dev's AWS account is listed.
+
+
+ Click **Allow principals** and add an entry in this format, replacing `` with the
+ Trigger.dev AWS account ID shown on the **Add connection** page in your dashboard:
+
+ ```text
+ arn:aws:iam:::root
+ ```
+
+
+ Always copy the account ID from your Trigger.dev dashboard. The correct value differs between
+ environments — don't reuse an ID from another source.
+
+
+
+
+ Once the principal is allow-listed, return to the **Add connection** page in Trigger.dev and
+ submit the form again. The wizard should now find your endpoint service and start provisioning.
+
+
+
+For full setup instructions including this step, see [Setting up PrivateLink in the AWS Console](/private-networking/aws-console-setup).