Deployment

AWS

Deploy ctxpipe on AWS with the @ctxpipe/aws-cdk CtxPipe construct.

@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.

Single-tenant only

Self-hosted ctxpipe on @ctxpipe/aws-cdk is single-tenant. Run one stack per organization.

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:

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.

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

OpenAI-compatible provider

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 for your stack region before relying on chat or embeddings.

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.

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.

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:

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

Bedrock, with the example app in examples/aws-cdk-self-host. It always uses Bedrock, and reads orgSlug, domainName, hostedZoneId, size, stackName, modelFast, modelMedium, modelHigh, and modelEmbedding from the context:

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:

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.

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 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

PropWhy it is required
orgSlugSelf-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>.
customDomainctxpipe 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.
modelProviderBackend and worker need model IDs and either an OpenAI-compatible endpoint + API key, or Bedrock via ECS task role. See union below.

Optional props

PropBehavior
sizeCapacity profile for single-tenant deployments. Valid values: small, medium, large. Defaults to small when omitted.
otelOptional OTLP export. Omit it and the tasks export nothing. The construct does not deploy a collector, Langfuse, or ClickStack.
sandboxHostThe chat sandbox host: instanceType, dockerVolumeSizeGiB (a whole number, at least 20), agentVaultNetworkAllowlist for a private git server, and enabled. The host is on by default. enabled: false creates no host, and turns chat off.
connectorSecretsDeployment-wide connector credentials. See Optional connector secrets.

modelProvider union

VariantkindRequired nested fieldsRuntime auth
OpenAI-compatibleomitted or openai-likebaseUrl, apiKey, models.fastMODEL_PROVIDER_API_KEY in Secrets Manager
Amazon Bedrockbedrockmodels.fastECS 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 for what each tier is used for.

For bedrock, modelProviderSecret / modelProviderSecretArn stack outputs are omitted.

Upgrading from older @ctxpipe/aws-cdk versions

RemovedReplacement
modelProvider.defaultModelmodelProvider.models.fast (required)

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

Required nested fields (openai-like)

FieldWhy it is required
customDomain.domainNameBecomes 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.hostedZoneIdUsed to create/validate Route 53 records for ACM certificate validation, ALB alias records, and SES domain identity/DKIM records.
modelProvider.baseUrlEndpoint for OpenAI-compatible model APIs used by backend and worker.
modelProvider.apiKeySecret used to authenticate model API requests at runtime.
modelProvider.models.fastModel ID for the fast tier (MODEL_FAST_NAME).

Required nested fields (bedrock)

FieldWhy it is required
customDomain.domainNameSame as openai-like — public HTTPS origin.
customDomain.hostedZoneIdSame as openai-like — DNS/TLS/SES records.
modelProvider.models.fastBedrock 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.

ConnectorKeys
GitHub AppgithubAppId, githubPrivateKey, githubWebhookSecret, githubClientId, githubClientSecret
AtlassianatlassianClientId, atlassianClientSecret
SlackslackClientId, slackClientSecret, slackSigningSecret (set all three)
LinearlinearClientId, linearClientSecret, linearRedirectUri, linearWebhookSecret
NotionnotionClientId, notionClientSecret, notionWebhookSecret
PagerDutypagerdutyClientId, pagerdutyClientSecret, pagerdutyRedirectUri. There is no shared webhook secret.

See Self-host Slack and Self-host PagerDuty for callback and webhook settings.

Observability

PropEnv on backend, worker, UI, and codesearch
endpointOTEL_EXPORTER_OTLP_{TRACES,LOGS,METRICS}_ENDPOINT
headersOTEL_EXPORTER_OTLP_HEADERS (Secrets Manager)
resourceAttributesOTEL_RESOURCE_ATTRIBUTES

Browser telemetry and the signal URLs the apps read are in Configuration. Prop reference: package README.

High-level architecture

CtxPipe provisions a single AWS stack with these layers:

LayerWhat is provisionedPurpose
NetworkVPC with public/private subnets and NAT egressIsolates internal services while allowing controlled outbound access.
Edge + ingressPublic ALB + ACM certificate + Route 53 recordsExposes one HTTPS origin and routes public traffic to backend.
ComputeECS cluster + Fargate services (backend, worker, ui, codesearch)Runs application APIs, background jobs, UI, and code indexing/search workloads.
Chat sandboxesOne EC2 Docker host in an Auto Scaling group, Docker volume, TLS secrets, CloudWatch alarmsRuns each Workspace conversation's agent in its own container.
Stateful dataAurora PostgreSQL 16, Neptune cluster, EFS for /dataPersists relational state, graph data, and codesearch cache/indexes.
Secrets + identitySecrets Manager secrets, generated AUTH_SECRET, optional connector secretsStores runtime credentials and injects them into tasks securely.
Email deliverySES domain identity, DKIM, SMTP credentialsEnables transactional email from backend (ctxpipe-noreply@<hosted-zone-apex>).
Deploy safetyMigration custom resource (one-off ECS task) + ECS deployment rollback controlsRuns 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.

sizeInstancevCPU / RAMDocker volume (gp3)~Cost/monthRunning sandboxes (est.)
small (default)t4g.medium2 / 4 GiB30 GB~$28~6
mediumt4g.large2 / 8 GiB50 GB~$54~14
larget4g.xlarge4 / 16 GiB100 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:

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). 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:

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

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.

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

PieceBehavior
InstanceAmazon 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 volumegp3 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 APIPort 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.
NetworkBackend 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 shared with Docker Compose.
CredentialsAgent 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.
IsolationEach 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.
LimitsAll sandbox containers share one systemd slice capped at 85% of host memory. Container logs rotate at 3 × 10 MB.
Chat imageghcr.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 workerGet 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

AlarmFires whenWhat to do
Docker disk/var/lib/docker is 80% full or more for 10 minutesCheck docker system df on the host (see Operations). Raise sandboxHost.dockerVolumeSizeGiB if the volume fills with live sandboxes.
MemoryHost memory is 85% used or more for 15 minutesSandboxes 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:

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));
}

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.

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 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.