# AWS (https://docs.ctxpipe.ai/docs/self-hosting/deployment/aws)

`@ctxpipe/aws-cdk` is the AWS self-hosting path for ctxpipe. It gives you one high-level construct, `CtxPipe`, that provisions the required infrastructure and wires runtime configuration for you.

<Callout type="warn" title="Single-tenant only">
  Self-hosted ctxpipe on `@ctxpipe/aws-cdk` is **single-tenant**. Run one stack
  per organization.
</Callout>

## Step-by-step deployment flow

### 1) Install `@ctxpipe/aws-cdk` in your CDK repo

Use an existing CDK repo, or create a new one first.

Then install:

```bash
pnpm add @ctxpipe/aws-cdk aws-cdk-lib constructs
```

### 2) Create or reuse a Route 53 hosted zone

You need a hosted zone for your custom domain because the construct configures
DNS and TLS from it.

* Reuse an existing public hosted zone if you already manage your domain in
  Route 53.
* Or create a new hosted zone and delegate your domain/subdomain to it.
* Keep both values ready:
  * `customDomain.domainName` (example: `app.example.com`)
  * `customDomain.hostedZoneId` (example: `Z0123456789ABCDEF`)

### 3) Import `CtxPipe` and configure it

Instantiate the construct in your stack. Follow the same pattern as
[`examples/aws-cdk-self-host`](https://github.com/ctxpipe-ai/ctxpipe/tree/main/examples/aws-cdk-self-host).

`modelProvider` is a union: **openai-like** (default) or **bedrock**. Both variants configure tiers through the same `models` prop (`fast` required).

#### OpenAI-compatible provider

```ts
import * as cdk from "aws-cdk-lib";
import { CtxPipe } from "@ctxpipe/aws-cdk";

const app = new cdk.App();
const stack = new cdk.Stack(app, "CtxPipeStack");
const modelApiKey = app.node.tryGetContext("modelApiKey");

if (!modelApiKey) {
  throw new Error('Missing CDK context "modelApiKey". Pass -c modelApiKey=...');
}

new CtxPipe(stack, "CtxPipe", {
  orgSlug: "acme",
  size: "medium",
  customDomain: {
    domainName: "app.example.com",
    hostedZoneId: "Z0123456789ABCDEF",
  },
  modelProvider: {
    baseUrl: "https://api.openai.com/v1",
    apiKey: cdk.SecretValue.unsafePlainText(String(modelApiKey)),
    models: {
      fast: "gpt-4.1-mini",
      embedding: "text-embedding-3-large",
      // Optional: medium, high
      // medium: "gpt-4.1",
      // high: "gpt-4.1",
    },
  },
});
```

Set `models.embedding` for an OpenAI-compatible provider. Without it, the
backend uses `openai/text-embedding-3-large`, an OpenRouter model id that other
providers do not know.

#### Amazon Bedrock (task role)

For Bedrock, set `kind: "bedrock"` and tier model IDs only. The construct grants the backend/worker ECS task roles `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`, injects `MODEL_PROVIDER=bedrock` and `MODEL_BEDROCK_AWS_REGION`, and does **not** create a `MODEL_PROVIDER_API_KEY` secret. The backend calls Bedrock Runtime with SigV4 credentials from the ECS task role.

Enable each model ID in the [Amazon Bedrock console](https://console.aws.amazon.com/bedrock/) for your stack region before relying on chat or embeddings.

<Callout type="info" title="Bedrock embeddings (Cohere only)">
  Bedrock embeddings currently support **Cohere** embed models only (the backend uses Cohere's invoke format). Default when `models.embedding` is omitted: `cohere.embed-v4:0`. You can also use inference-profile IDs such as `global.cohere.embed-v4:0`. Non-Cohere Bedrock embed models (for example Titan) are not supported yet.
</Callout>

<Callout type="warn" title="Organizations SCPs can still block Bedrock">
  The construct attaches `bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream` to the backend and worker task roles, but an AWS Organizations **service control policy (SCP)** can override that with an **explicit deny**. Typical log text: `AccessDeniedException` … `explicit deny in a service control policy`. Allow your chat and Cohere embed foundation-model ARNs in the SCP (denies for `global.cohere.embed-v4:0` often name `…/foundation-model/cohere.embed-v4:0`). This is separate from chat stream stalls such as `Bedrock Converse stream timed out … without receiving a chunk`—those need CloudTrail `ConverseStream` investigation, not a wider task-role policy.
</Callout>

```ts
new CtxPipe(stack, "CtxPipe", {
  orgSlug: "acme",
  customDomain: {
    domainName: "app.example.com",
    hostedZoneId: "Z0123456789ABCDEF",
  },
  modelProvider: {
    kind: "bedrock",
    region: "us-east-1", // optional; defaults to stack region
    models: {
      fast: "anthropic.claude-sonnet-4-20250514-v1:0",
      medium: "anthropic.claude-sonnet-4-20250514-v1:0",
      high: "anthropic.claude-opus-4-20250514-v1:0",
      embedding: "cohere.embed-v4:0", // optional; Cohere models only (e.g. global.cohere.embed-v4:0)
    },
  },
});
```

### 4) Run CDK deploy

**OpenAI-like:**

```bash
cdk synth
cdk deploy -c modelApiKey="$OPENAI_API_KEY"
```

**Bedrock**, with the example app in
[`examples/aws-cdk-self-host`](https://github.com/ctxpipe-ai/ctxpipe/tree/main/examples/aws-cdk-self-host).
It always uses Bedrock, and reads `orgSlug`, `domainName`, `hostedZoneId`,
`size`, `stackName`, `modelFast`, `modelMedium`, `modelHigh`, and
`modelEmbedding` from the context:

```bash
cdk deploy \
  -c modelFast="anthropic.claude-sonnet-4-20250514-v1:0" \
  -c orgSlug=acme \
  -c domainName=app.example.com \
  -c hostedZoneId=Z0123456789ABCDEF
```

### 5) Wait for deployment and open your custom domain

After `cdk deploy` completes, open `https://<customDomain.domainName>`.

### 6) Complete onboarding with the same `orgSlug`

When you create the first organization in onboarding, use the **same** slug
as the `orgSlug` of `CtxPipe`. The construct uses the slug for the graph
database settings of the organization (`GRAPH_DB_URI_<orgSlug>`). If the two
slugs are different, the graph of the organization does not work. The slug can
contain only lowercase letters, digits, and `-`.

### 7) Upgrade an existing stack

A new `@ctxpipe/aws-cdk` version on npm is the release unit. It pins backend, worker, UI, codesearch, and migrate images to one git SHA and includes that release’s Fargate size profiles. After it is published:

```bash
pnpm update @ctxpipe/aws-cdk
cdk synth
cdk deploy
```

Before the first deploy of a new version, check the Aurora minor version. See
[Aurora PostgreSQL minor version](https://github.com/ctxpipe-ai/ctxpipe/blob/main/packages/aws-cdk/README.md#aurora-postgresql-minor-version).

`cdk deploy` runs Postgres migrations, then rolls the ECS services. You do not set image tags in `CtxPipe` props, and you should not retag tasks to `:latest` yourself. The migrate task provisions `ctxpipe_app`, and then the stack writes the runtime `DATABASE_URL` secret; CDK operators do not set either URL by hand and do not run `psql`.

This is how codesearch memory bumps (4 / 8 / 12 GiB) and ingest behavior land together. Updating only container images, or only task memory, leaves the stack split across releases.

The first upgrade to a version with the [chat sandbox host](#chat-sandbox-host)
adds it with no new required props. That deploy only adds resources and
replaces nothing that holds data. It waits until the host reports ready,
then rolls backend and worker.

To automate upgrades, let Renovate or Dependabot open a pull request for each
new `@ctxpipe/aws-cdk` version, and run `cdk synth` and `cdk deploy` from your
pipeline.

## Why each prop exists

### Required props

| Prop            | Why it is required                                                                                                                                                                  |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orgSlug`       | Self-hosted AWS CDK deployments are single-tenant. The slug identifies the one org for this stack and is used for org-scoped graph runtime values such as `GRAPH_DB_URI_<orgSlug>`. |
| `customDomain`  | ctxpipe needs one stable public HTTPS origin for browser traffic, auth, API routes, connector callbacks, and MCP. The construct uses this to configure DNS + TLS + ALB listeners.   |
| `modelProvider` | Backend and worker need model IDs and either an OpenAI-compatible endpoint + API key, or Bedrock via ECS task role. See union below.                                                |

### Optional props

| Prop               | Behavior                                                                                                                                                                                                                                                                                         |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `size`             | Capacity profile for single-tenant deployments. Valid values: `small`, `medium`, `large`. Defaults to `small` when omitted.                                                                                                                                                                      |
| `otel`             | Optional OTLP export. Omit it and the tasks export nothing. The construct does not deploy a collector, Langfuse, or ClickStack.                                                                                                                                                                  |
| `sandboxHost`      | The [chat sandbox host](#chat-sandbox-host): `instanceType`, `dockerVolumeSizeGiB` (a whole number, at least 20), `agentVaultNetworkAllowlist` for a [private git server](#private-git-servers), and `enabled`. The host is on by default. `enabled: false` creates no host, and turns chat off. |
| `connectorSecrets` | Deployment-wide connector credentials. See [Optional connector secrets](#optional-connector-secrets).                                                                                                                                                                                            |

### `modelProvider` union

| Variant           | `kind`                   | Required nested fields             | Runtime auth                                                            |
| ----------------- | ------------------------ | ---------------------------------- | ----------------------------------------------------------------------- |
| OpenAI-compatible | omitted or `openai-like` | `baseUrl`, `apiKey`, `models.fast` | `MODEL_PROVIDER_API_KEY` in Secrets Manager                             |
| Amazon Bedrock    | `bedrock`                | `models.fast`                      | ECS task role → native Bedrock Runtime (SigV4); no model API key secret |

Optional on both variants: `models.medium`, `models.high`, and `models.embedding`.

Omitted tier IDs cascade: **medium** → **fast**, **high** → **medium**. The construct maps them to `MODEL_FAST_NAME`, `MODEL_MEDIUM_NAME`, `MODEL_HIGH_NAME`, and `MODEL_EMBEDDING_NAME`. See [Model configuration](/docs/self-hosting/models) for what each tier is used for.

For **bedrock**, `modelProviderSecret` / `modelProviderSecretArn` stack outputs are omitted.

### Upgrading from older `@ctxpipe/aws-cdk` versions

| Removed                      | Replacement                            |
| ---------------------------- | -------------------------------------- |
| `modelProvider.defaultModel` | `modelProvider.models.fast` (required) |

Replace `defaultModel: "gpt-4.1-mini"` with `models: { fast: "gpt-4.1-mini" }`.

### Required nested fields (openai-like)

| Field                       | Why it is required                                                                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customDomain.domainName`   | Becomes the public app origin (`AUTH_BASE_URL` / `CTXPIPE_PUBLIC_APP_URL`) served over HTTPS. This is the URL users visit for sign-in and onboarding. |
| `customDomain.hostedZoneId` | Used to create/validate Route 53 records for ACM certificate validation, ALB alias records, and SES domain identity/DKIM records.                     |
| `modelProvider.baseUrl`     | Endpoint for OpenAI-compatible model APIs used by backend and worker.                                                                                 |
| `modelProvider.apiKey`      | Secret used to authenticate model API requests at runtime.                                                                                            |
| `modelProvider.models.fast` | Model ID for the fast tier (`MODEL_FAST_NAME`).                                                                                                       |

### Required nested fields (bedrock)

| Field                       | Why it is required                                                                                             |
| --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `customDomain.domainName`   | Same as openai-like — public HTTPS origin.                                                                     |
| `customDomain.hostedZoneId` | Same as openai-like — DNS/TLS/SES records.                                                                     |
| `modelProvider.models.fast` | Bedrock model ID for the fast tier (`MODEL_FAST_NAME`). Enable it in the Bedrock console for the stack region. |

### Optional connector secrets

Pass deployment-wide connector credentials through `connectorSecrets`. Each
value is a `cdk.SecretValue`. The construct stores them in Secrets Manager and
gives them to the ECS tasks that need them. Most connectors can also take their
credentials in the setup wizard of the product, so these are optional.

| Connector  | Keys                                                                                                     |
| ---------- | -------------------------------------------------------------------------------------------------------- |
| GitHub App | `githubAppId`, `githubPrivateKey`, `githubWebhookSecret`, `githubClientId`, `githubClientSecret`         |
| Atlassian  | `atlassianClientId`, `atlassianClientSecret`                                                             |
| Slack      | `slackClientId`, `slackClientSecret`, `slackSigningSecret` (set all three)                               |
| Linear     | `linearClientId`, `linearClientSecret`, `linearRedirectUri`, `linearWebhookSecret`                       |
| Notion     | `notionClientId`, `notionClientSecret`, `notionWebhookSecret`                                            |
| PagerDuty  | `pagerdutyClientId`, `pagerdutyClientSecret`, `pagerdutyRedirectUri`. There is no shared webhook secret. |

See
[Self-host Slack](/docs/self-hosting/slack) and
[Self-host PagerDuty](/docs/self-hosting/pagerduty) for callback and webhook
settings.

## Observability

| Prop                 | Env on backend, worker, UI, and codesearch          |
| -------------------- | --------------------------------------------------- |
| `endpoint`           | `OTEL_EXPORTER_OTLP_{TRACES,LOGS,METRICS}_ENDPOINT` |
| `headers`            | `OTEL_EXPORTER_OTLP_HEADERS` (Secrets Manager)      |
| `resourceAttributes` | `OTEL_RESOURCE_ATTRIBUTES`                          |

Browser telemetry and the signal URLs the apps read are in [Configuration](/docs/self-hosting/configuration). Prop reference: [package README](https://github.com/ctxpipe-ai/ctxpipe/blob/main/packages/aws-cdk/README.md#observability).

## High-level architecture

`CtxPipe` provisions a single AWS stack with these layers:

| Layer              | What is provisioned                                                                         | Purpose                                                                          |
| ------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Network            | VPC with public/private subnets and NAT egress                                              | Isolates internal services while allowing controlled outbound access.            |
| Edge + ingress     | Public ALB + ACM certificate + Route 53 records                                             | Exposes one HTTPS origin and routes public traffic to backend.                   |
| Compute            | ECS cluster + Fargate services (`backend`, `worker`, `ui`, `codesearch`)                    | Runs application APIs, background jobs, UI, and code indexing/search workloads.  |
| Chat sandboxes     | One EC2 Docker host in an Auto Scaling group, Docker volume, TLS secrets, CloudWatch alarms | Runs each Workspace conversation's agent in its own container.                   |
| Stateful data      | Aurora PostgreSQL 16, Neptune cluster, EFS for `/data`                                      | Persists relational state, graph data, and codesearch cache/indexes.             |
| Secrets + identity | Secrets Manager secrets, generated `AUTH_SECRET`, optional connector secrets                | Stores runtime credentials and injects them into tasks securely.                 |
| Email delivery     | SES domain identity, DKIM, SMTP credentials                                                 | Enables transactional email from backend (`ctxpipe-noreply@<hosted-zone-apex>`). |
| Deploy safety      | Migration custom resource (one-off ECS task) + ECS deployment rollback controls             | Runs migrations during deploy and prevents partial/broken rollouts.              |

Only the backend is public. UI and codesearch stay internal and are reached
through backend routing.

## Chat sandbox host

Workspace chat runs each conversation's agent (OpenCode) in its own Docker
container. `CtxPipe` creates one EC2 host for those containers by default. The
sandbox keeps the agent's commands away from the backend and your data stores.

To create no host, set `sandboxHost: { enabled: false }`. Without the host,
Workspace chat is off: the product answers chat requests with a "sandbox
unavailable" error (503). The rest of the product keeps working.

### Sizes and cost

The host mostly holds idle sandboxes, so it uses burstable Graviton (ARM)
instances. Memory is the limit: an active sandbox uses about 0.3–0.5 GiB.
Sandboxes stop after 5 minutes idle and resume on the next message. Stopped
sandboxes are removed 30 days after last use. An organization runs at most 50
sandboxes at once.

| `size`            | Instance     | vCPU / RAM | Docker volume (gp3) | \~Cost/month | Running sandboxes (est.) |
| ----------------- | ------------ | ---------- | ------------------- | ------------ | ------------------------ |
| `small` (default) | `t4g.medium` | 2 / 4 GiB  | 30 GB               | \~$28        | \~6                      |
| `medium`          | `t4g.large`  | 2 / 8 GiB  | 50 GB               | \~$54        | \~14                     |
| `large`           | `t4g.xlarge` | 4 / 16 GiB | 100 GB              | \~$107       | \~30                     |

Costs are us-east-1 on-demand prices for the instance, the Docker volume, and
a 16 GB root volume. `t4g` instances run in unlimited mode, so sustained CPU
above baseline adds a small surcharge.

Override either value with `sandboxHost`:

```ts
import * as ec2 from "aws-cdk-lib/aws-ec2";

new CtxPipe(stack, "CtxPipe", {
  // ...orgSlug, customDomain, modelProvider
  sandboxHost: {
    instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.XLARGE),
    dockerVolumeSizeGiB: 150,
  },
});
```

The instance type must be Graviton (arm64); other architectures are rejected
at synth.

### Private git servers

Ingestion can read a plain git repository on a git server inside your network
(see [Plain git repositories](/docs/workspaces/linked-repositories#plain-git-repositories)).
The ingestion sandbox fetches it through Agent Vault on the sandbox host, and
Agent Vault connects only to the private addresses in its allowlist. By
default, the allowlist holds only the backend subnets. Add the single host
address of your git server with `sandboxHost.agentVaultNetworkAllowlist`:

```ts
new CtxPipe(stack, "CtxPipe", {
  // ...orgSlug, customDomain, modelProvider
  sandboxHost: {
    agentVaultNetworkAllowlist: ["10.20.0.15/32"],
  },
});
```

<Callout type="warn">
  Chat-sandbox vaults forward each host that no rule matches. An address or CIDR in this list therefore lets every chat agent reach every port and path at that address. Add only the single host address of the git server, never a whole subnet.
</Callout>

The stack adds each entry after the backend subnets. Each entry must be an
IPv4 or IPv6 address or CIDR. Synth rejects an entry with a cloud metadata or
link-local address. The sandbox host must also have a route to that address,
for example VPC peering or a transit gateway. The git server must accept
connections from the host. A change to the list replaces the host, as each
change to its boot script does.

### What it sets up

| Piece              | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Instance           | Amazon Linux 2023 in a single-instance Auto Scaling group in the private subnets. EC2 health checks replace a failed host. `cdk deploy` waits until the host reports ready. New AMIs apply when the host is replaced, not on every deploy.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Docker volume      | gp3 volume at `/var/lib/docker`. It belongs to the instance, so a replaced host starts empty. Conversations keep their work in their git branch and continue on the new host.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| Docker API         | Port 2376 with mutual TLS at `sandbox-host.ctxpipe.local`, a Cloud Map name the host registers when it boots. The host creates the certificates on first boot and stores them in Secrets Manager; a replacement host reuses them. The CA key is not kept.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| Network            | Backend and worker reach the Docker API and the Agent Vault API (port 14321). Only the backend reaches the sandboxes' agent ports. `DOCKER-USER` rules let sandboxes reach only the Agent Vault proxy (port 14322); a direct connection, a non-HTTP protocol, and DNS are rejected. Through the proxy, sandboxes reach Git hosts, the internet, and the backend (model proxy and per-run tool bridges), but not Postgres, Neptune, EFS, codesearch, the UI, or the worker. This is the [chat sandbox network policy](/docs/self-hosting/architecture#chat-sandbox-network-policy) shared with Docker Compose.                                                                                                                                                                                                                                                                                         |
| Credentials        | Agent Vault (`infisical/agent-vault:latest`) runs on the host on its own Docker bridge. Sandboxes hold no credential: Agent Vault adds the GitHub token and the backend credentials to each request. Agent Vault may dial the backend's subnets, and the entries of `sandboxHost.agentVaultNetworkAllowlist`. The backend subnets are the stack's only private subnets, so they also hold RDS, Neptune, EFS, and the UI and codesearch tasks; only security groups keep the proxy out of them. If you change security groups, never admit the sandbox host's group to those services. The stack generates its master password and owner password in Secrets Manager; the host reads the master password, and backend and worker get the owner password. If Agent Vault does not answer, chat fails closed. A replaced host starts an empty Agent Vault, and the backend registers again on first use. |
| Isolation          | Each agent port needs a per-conversation password. Agent ports are reachable inside the VPC, so the host also blocks sandboxes from each other, from the host itself, and from instance metadata.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Limits             | All sandbox containers share one systemd slice capped at 85% of host memory. Container logs rotate at 3 × 10 MB.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Chat image         | `ghcr.io/ctxpipe-ai/chat-sandbox`, published for arm64 and amd64 with the same release tag as the service images. The backend pulls it through the host's Docker daemon on the first chat after a release, so releases never replace the host.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Backend and worker | Get `SANDBOX_PROVIDER=docker`, `SANDBOX_CHAT_IMAGE`, `DOCKER_HOST=tcp://sandbox-host.ctxpipe.local:2376`, `DOCKER_TLS_VERIFY=1`, and `DOCKER_CERT_PATH`. A short init container in each task writes the client certificates there as files. They also get `AGENT_VAULT_ADDR` and the `AGENT_VAULT_OWNER_PASSWORD` secret. Sandboxes call the backend back at its task's VPC DNS name (`ip-….compute.internal`), so they reach the replica that runs their turn. The stack sets `SANDBOX_CALLBACK_DNS_SUFFIX` for this. The VPC must have DNS host names and DNS resolution on; the stack's VPC has both.                                                                                                                                                                                                                                                                                              |

### Alarms

| Alarm       | Fires when                                           | What to do                                                                                                                                                                                     |
| ----------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Docker disk | `/var/lib/docker` is 80% full or more for 10 minutes | Check `docker system df` on the host (see [Operations](/docs/self-hosting/operations#chat-sandbox-host-aws)). Raise `sandboxHost.dockerVolumeSizeGiB` if the volume fills with live sandboxes. |
| Memory      | Host memory is 85% used or more for 15 minutes       | Sandboxes are near their cap. Move to a larger `size` or `sandboxHost.instanceType`.                                                                                                           |

The alarms have no actions. Subscribe them to your own topic through the
construct:

```ts
import * as actions from "aws-cdk-lib/aws-cloudwatch-actions";

const ctxPipe = new CtxPipe(stack, "CtxPipe", { /* ... */ });
for (const alarm of ctxPipe.sandboxHostAlarms) {
  alarm.addAlarmAction(new actions.SnsAction(opsTopic));
}
```

<Callout type="warn" title="Chat needs a sandbox">
  The backend and worker images refuse `SANDBOX_PROVIDER=unsandboxed`: an
  agent without a sandbox would run with the backend's credentials. `CtxPipe`
  configures a Docker host for chat sandboxes.
</Callout>

## What this gives you by default

* `GRAPH_DB_PROVIDER=neptune` and graph URI wiring from Neptune endpoints
* Internal service routing (`UI_PROXY_URL`, `CODESEARCH_URL`) so only backend is public
* Generated `AUTH_SECRET` and managed `DATABASE_URL` (runtime `ctxpipe_app`) / SMTP secrets
* Public app URL output and secret ARN outputs for operations

## How to choose `size`

Use `size` to tune compute and database capacity for your single-tenant workload:

* `small` (default): best for pilot/small-team deployments with moderate repository churn and cost sensitivity. Aurora and Neptune both use `db.t4g.medium` — AWS’s minimum supported burstable size for these engines (dev/test oriented). Codesearch is 4 GiB (ingest peak, not idle RSS).
* `medium`: better for sustained ingestion/reindex workloads where worker and codesearch need more headroom. Codesearch is 8 GiB. Aurora uses `db.t4g.large`; Neptune uses memory-optimized `db.r6g.large`.
* `large`: for high-ingestion and lower-latency expectations, with bigger tasks, scaled backend/worker replicas, and `db.r6g.xlarge` for both Aurora and Neptune. Codesearch is 12 GiB and still **one task**. The two workers each get `OPENWORKFLOW_CONCURRENCY=8` so the cluster cannot stampede codesearch. Extra workers help extract and connectors, not clone/SCIP parallelism.

See the [package README sizing table](https://github.com/ctxpipe-ai/ctxpipe/blob/main/packages/aws-cdk/README.md#sizing-profiles) for the full preset matrix.

Do **not** raise codesearch desired count. Ingest throughput grows with codesearch memory (4 / 8 / 12 GiB) and the env the construct injects (`OPENWORKFLOW_CONCURRENCY`, `CODESEARCH_INDEXER_CONCURRENCY`, `CODESEARCH_INDEX_PIPELINE_CONCURRENCY`).

Signals that you should move up a size:

* ingestion jobs queueing up or taking too long,
* frequent codesearch reindex delays,
* higher p95 response latency during active sync/index windows.

`@ctxpipe/aws-cdk` keeps services in private subnets for every size. NAT remains required because backend/worker/codesearch need outbound access for model providers, GitHub, Atlassian, and Linear APIs.

## Related references

* [`packages/aws-cdk/README.md`](https://github.com/ctxpipe-ai/ctxpipe/blob/main/packages/aws-cdk/README.md)
* [`examples/aws-cdk-self-host`](https://github.com/ctxpipe-ai/ctxpipe/tree/main/examples/aws-cdk-self-host)
