Skip to content

C:\> Documentation

On-prem connector

For teams whose production database must never be reachable from the cloud: a slim container that runs inside your network, dials out to Codepanion, and executes the agent's read-only queries locally. No inbound ports, and your connection string never leaves your network.

Contents

How it works

The connector is outbound-only. It opens a long-poll control channel to Codepanion and waits. When the agent needs your database, the query travels down that channel, the connector runs it locally, with the same read-only enforcement Codepanion applies everywhere (SELECT-only queries inside a transaction that is always rolled back, a read-only session on PostgreSQL, a 5-second timeout, and a 1,000-row cap), and posts the result back. Your firewall stays closed.

Good to know

The database connection string is configured on your side, as an environment variable on the container. Codepanion never sees it, never stores it, and cannot connect to your database itself. Every query is executed by the connector, inside your network, under read-only enforcement that runs on your side of the channel.

Create a connector API key

The connector signs in with an API key that has the Connector scope. Open Settings → API Keys in the Codepanion app, name the key after where it will run (for example datacentre-east), set the scope to Connector and click Create API Key. You need the Owner role to see that card.

The value is shown once, so copy it straight into the connector's CONNECTOR_TOKEN in the next step. A connector key can't upload code, and an ingest key can't open the connector channel. To rotate it, create a new connector key, restart the container with it, then revoke the old one from the same card.

Run the connector

One container, three environment variables. Run it anywhere inside your network that can reach the database, on Docker, Kubernetes, or a VM with Docker installed:

Getting the image

The image is delivered on request during early access rather than pulled anonymously. Ask us and we send registry credentials for your organisation; run docker login ghcr.io with them on the host before the first pull, and the commands on this page work as written. Every published image is signed, so you can check what you pulled before you run it: see Verify and pin the image.

docker run
docker run -d --restart unless-stopped \
  --name codepanion-connector \
  -e CODEPANION_URL="https://api.codepanion.app/api" \
  -e CONNECTOR_TOKEN="<your connector-scoped key>" \
  -e CONNECTOR_DB_CONNECTION_STRING="Server=db.internal;Database=prod;User Id=codepanion_reader;Password=...;" \
  ghcr.io/codepanion-app/connector:latest

Use a dedicated read-only database user, exactly as you would for a direct connection. Least privilege applies on your side too. SQL Server and PostgreSQL are both supported; the connector detects the provider from the connection string. The image is hardened: a chiseled, distroless-style runtime with no shell and no package manager, running as a non-root user.

Point the agent at it

Mark a database environment as connector-mode and the agent routes that environment's queries through the channel instead of a stored connection string. In Settings → Database Connections, enter the environment label (for example prod), set Connection to On-Prem Connector, leave Kind on Static and click Save. There is no connection string field and no Test Connection button in this mode, because the connector reads the string from its own environment.

Under the Connection choice the form says whether your connector is online, and the environment's row in the list says the same once it is saved. The row carries a Connector badge in place of a provider and a last test, because nothing on our side holds a connection string for it or can test one. If the status stays at "not connected yet" after the container starts, check its logs and that it can reach api.codepanion.app (see network requirements).

To move an existing direct connection behind the connector, click Edit on its row, switch Connection to On-Prem Connector and confirm the conversion. The connection string we held for it is deleted from our Key Vault as part of the save.

One database connection per environment
Settings → Database Connections. The production row is reached through the connector: no provider, no connection test, and no credential stored on our side. The other rows are direct connections and a catalog.
One database connection per environment
Screenshot of One database connection per environment. Settings → Database Connections. The production row is reached through the connector: no provider, no connection test, and no credential stored on our side. The other rows are direct connections and a catalog.
Settings → Database Connections. The production row is reached through the connector: no provider, no connection test, and no credential stored on our side. The other rows are direct connections and a catalog.

One connector environment per tenant. The channel addresses a connector by tenant, and a container serves the single database it is pointed at, so saving a second environment as connector-mode is refused, with a message naming the one you already have, instead of quietly answering staging queries from production. Tell us if you need two and we will build the routing around what you actually run.

If the connector isn't running when the agent asks, the query fails fast with a clear "no connector connected" answer. Nothing hangs, and the agent tells the support engineer what's wrong.

Catalog resolver (multi-tenant databases)

If your real database is looked up per tenant from a catalog (a registry table that maps a tenant key to its database) rather than living at one fixed connection string, the connector can resolve it for you, entirely inside your network. Point the connector at your local catalog database, and a small resolver snippet turns a tenant key into the actual connection string. The connector then runs the agent's query against the resolved database and posts back only the rows. The resolved per-tenant connection string never crosses the channel. We hold nothing.

add the local catalog
docker run -d --restart unless-stopped \
  --name codepanion-connector \
  -e CODEPANION_URL="https://api.codepanion.app/api" \
  -e CONNECTOR_TOKEN="<your connector-scoped key>" \
  -e CONNECTOR_CATALOG_CONNECTION_STRING="Server=catalog.internal;Database=registry;User Id=codepanion_reader;Password=...;" \
  ghcr.io/codepanion-app/connector:latest

Then mark the environment as both connector-mode and catalog. In Settings → Database Connections, set Connection to On-Prem Connector and Kind to Catalog, paste the resolver snippet into Resolver Script, and put the lookup key in Resolve Key (blank passes the environment label). There is no catalog connection string to enter: the connector uses its own. A resolver that looks the tenant up by key might read:

resolver script
return connection.query({
  table: 'tenants',
  where: [{ column: 'key', op: '=', value: resolveKey }],
  limit: 1
})[0].cs;

What runs, and how it's contained

The resolver runs on your connector, in a hardened sandbox: a JavaScript interpreter with no access to the host, the filesystem, or .NET, offering only a read-only connection.query() against your catalog, under strict time, memory, and statement limits. It is the one place the connector executes a snippet that came from Codepanion. If you would rather the connector never run a snippet delivered over the channel, pin your own resolver locally with CONNECTOR_RESOLVER_JS (inline) or CONNECTOR_RESOLVER_JS_FILE (a file path). A pinned resolver always wins, and the connector logs which source is active at startup.

Whatever a delivered snippet returns, the connector only dials it if every host in it is a private or loopback address, so a snippet cannot name a server outside your network. A host name is checked by looking it up, so a name whose DNS answer changes between that check and the connection is only ruled out by a list. If your tenant databases live elsewhere, list the hosts in CONNECTOR_RESOLVED_HOSTS: comma-separated host names, *.example.com wildcards, IP addresses or CIDR ranges. The list is checked before any DNS lookup and then replaces the private-only rule, and it applies to a pinned resolver too.

Logs through the connector

The channel carries more than SQL. An environment's log source can be marked connector-mode too, and the agent's search_logs and get_log_context calls then run inside your network against a log store we never reach. Marking it is Settings work, set out under on-prem logs. What changes on this side is the container.

A log store the connector reaches with its own key needs two more variables, and they must be set together or the connector refuses to start:

add a node-local log store
docker run -d --restart unless-stopped \
  --name codepanion-connector \
  -e CODEPANION_URL="https://api.codepanion.app/api" \
  -e CONNECTOR_TOKEN="<your connector-scoped key>" \
  -e CONNECTOR_SEQ_URL="https://seq.internal" \
  -e CONNECTOR_SEQ_APIKEY="<a Seq API key that never leaves your network>" \
  ghcr.io/codepanion-app/connector:latest

The two ambient identities need no variable at all. With Azure Managed Identity or an AWS IAM role the connector queries Log Analytics or CloudWatch Logs as the host it runs on, so the grant is made once, on your infrastructure, to a workload you control. We store nothing to revoke.

On those two the workspace ID or log group is named in Settings and travels with each call, because it is ordinary non-secret configuration rather than a credential. The connector reads only what its host identity is already permitted to read, so scope that identity to exactly the workspaces and log groups you want reachable. A Seq endpoint is different: its URL and key come from the container's own environment and are never sent to us.

A connector can serve logs and nothing else: leave CONNECTOR_DB_CONNECTION_STRING unset and a query or schema call is answered with a clear "not configured" result rather than an obscure failure. On an ambient identity that makes a logs-only node two variables in total, CODEPANION_URL and CONNECTOR_TOKEN.

Network requirements

Outbound only. No inbound rules, no port forwarding, no VPN. If your egress is allow-listed, permit HTTPS (443) to these three, plus your own database and log store:

egress allow-list
api.codepanion.app                    # the control channel
ghcr.io                               # pulling connector images
pkg-containers.githubusercontent.com  # GHCR's blob storage (image layers)

The two registry hosts cover both your initial docker pull and the automatic updates once they ship.

An ambient-identity log source needs its cloud's own endpoints as well, since the connector calls them directly as the host: login.microsoftonline.com and api.loganalytics.io for Azure Monitor, or logs.<region>.amazonaws.com for CloudWatch Logs. A node-local Seq needs no egress rule at all.

Platform limits

  • linux/amd64 only. The image is built for x86-64 Linux. An arm64 host (AWS Graviton, arm64 Kubernetes nodes, Apple silicon) can only run it under emulation. Tell us if you need a native arm64 image.
  • SQL authentication for SQL Server. The connector signs in with the user name and password in its connection string. Windows authentication (Integrated Security, Kerberos) does not work from the container, which carries no Kerberos libraries, so the server must allow SQL Server authentication (mixed mode). PostgreSQL GSSAPI sign-in is unavailable for the same reason.
  • Behind an HTTPS proxy. Set HTTPS_PROXY on the container and the control channel and the Azure and AWS log calls go through it. Put internal HTTP endpoints, such as a node-local Seq, in NO_PROXY. Database connections are plain TCP and never use the proxy.
  • TLS inspection. The image trusts the standard public certificate authorities. If your proxy re-signs HTTPS with a corporate root, mount a PEM bundle holding the public roots plus yours and point SSL_CERT_FILE at it. The image has no shell or package manager, so the certificate cannot be installed inside it.
proxy and corporate root
  -e HTTPS_PROXY="http://proxy.internal:3128" \
  -e NO_PROXY="seq.internal" \
  -v /etc/pki/codepanion-bundle.pem:/certs/bundle.pem:ro \
  -e SSL_CERT_FILE="/certs/bundle.pem" \

To check a host before connecting it to Codepanion, run the image with --self-test and the same database variables. It needs no API key: it opens each configured database the way a real query does, runs SELECT 1 and reads the schema, prints the result, and exits non-zero with the driver's error if either step fails.

What we can and can't reach

Through the connector, the agent can do two things: run read-only queries expressed in Codepanion's structured query language, and read your database schema (tables, columns, keys, indexes). That's it. It cannot write: every query is compiled as a SELECT and runs inside a transaction that is always rolled back, never committed, and on PostgreSQL the session is also read-only. On SQL Server the connection itself is not read-only, so the one limit your database enforces is the permissions of the user you connect as: use a dedicated user that can only read. It cannot run arbitrary SQL, because queries arrive as a structured definition that is compiled and parameterised locally, against your actual schema. And it cannot reach anything else on your network: the connector connects only to the database(s) you configured, and nothing in the protocol can change that from our side.

Logs are searching only, and a Seq store is pinned the same way a database is: its URL and key are read from the container's own environment, so nothing in the protocol can aim the connector at a different one. The two ambient identities work differently, and the section above says how. The workspace ID or log group is named in Settings and travels with the call, bounded by whatever the host identity you granted is allowed to read.

The exception on the database side is the optional catalog resolver. If you enable it, the connector runs a resolver snippet and then dials whichever connection string that snippet returns, so the snippet decides which database is reached rather than a fixed environment variable. The snippet runs in a hardened, locked-down sandbox (no host, filesystem, or .NET access; read-only catalog queries only; strict resource limits). The resolved string never crosses the channel, and the connector refuses to dial it unless every host in it is a private address or one you listed in CONNECTOR_RESOLVED_HOSTS. Without a list the connector still looks a host name up to check where it points, so a snippet could put data into that lookup, or answer it with a private address and connect to a public one moments later. For the tightest setting, list your hosts (a listed name is matched before any lookup) or pin your own resolver with CONNECTOR_RESOLVER_JS. See that section.

Verify and pin the image

Every image we publish is signed with cosign keyless signing: the signature is bound to the image digest and to the GitHub Actions workflow that built it, and recorded in the public Sigstore transparency log. Each image also carries an SBOM and SLSA build provenance. Check the signature with your registry login in place:

verify the signature
cosign verify ghcr.io/codepanion-app/connector@sha256:<digest> \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/codepanion-app/codepanion\.app/\.github/workflows/connector\.yml@refs/heads/main$'

The examples on this page use :latest to stay short. In production, run what you verified: an exact version tag such as :0.1.42, or better the digest itself, so a restart can never pick up an image nobody checked. docker buildx imagetools inspect ghcr.io/codepanion-app/connector:0.1.42 prints a tag's digest, and ghcr.io/codepanion-app/connector@sha256:<digest> then replaces the image name in the docker run command.

Updates

The connector stamps its version on every request it makes, so we can see who is running what. Today, updating is a normal docker pull + restart (or your orchestrator's rolling update).

Coming: automatic updates

The connector is designed to keep itself current so you don't have to: roughly every 24 hours it will check ghcr.io for a newer image, verify the publisher's cosign signature, pin the exact image digest it verified, and restart onto it, so the update channel can't be hijacked between check and run. Until that ships, releases are infrequent by design (the connector is deliberately tiny) and we email connector customers when one is out.

Need help getting set up?

We do the first setup on a call with you: the CI step, the connection string, and a first investigation against your real code.

Or email us at hello@codepanion.app