Self-Hosted Configuration Reference

Complete reference for environment variables, licensing, database compatibility, reverse proxy setup, and deployment modes.

This is a configuration reference, not a tutorial. For step-by-step installation, see the install guide. For day-to-day operations (backup, upgrade, key rotation), see the operations guide.

Environment Variables

Every variable supports a _FILE suffix that reads the value from a file path (e.g., API_KEY_SECRET_FILE=/run/secrets/api-key-secret). This is the recommended pattern for secrets in Docker and Kubernetes.

Required

| Variable | Default | Description | |----------|---------|-------------| | API_KEY_SECRET | (none) | HMAC-SHA256 key for API key hashing. 64-character hex string. Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" | | VAULT_SIGNING_KEY | (none) | Ed25519 private key (base64 PKCS#8 DER) for audit vault signatures. Required in production. Generate: docker compose run --rm agledger-api dist/scripts/generate-signing-key.js |

Both are generated automatically by install.sh. If deploying manually, generate them before first start.

Database

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | DATABASE_URL | No | Constructed from POSTGRES_* vars when using bundled postgres | PostgreSQL connection string. Set this when using an external database (Aurora, RDS, Cloud SQL, Azure). | | DATABASE_URL_MIGRATE | No | Falls back to DATABASE_URL | Owner-role connection string for DDL migrations. Enables role separation: owner for migrations, agledger_app for runtime DML. | | DATABASE_POOL_MAX | No | 20 | Connection pool size. Adjust based on your database's max_connections. See Pool Sizing below. | | PROXY_DATABASE_URL | No | (none) | Separate connection pool for MCP proxy tables. | | ALLOW_DB_WITHOUT_SSL | No | false | Skip DB SSL verification in production. Not recommended. | | NODE_EXTRA_CA_CERTS | No | /etc/ssl/certs/rds-global-bundle.pem | Path to CA certificate bundle. The Docker image bundles the AWS RDS/Aurora global CA. For Cloud SQL or private CAs, mount your cert and set this path. |

Bundled PostgreSQL variables (only when using docker-compose.postgres.yml):

| Variable | Default | Description | |----------|---------|-------------| | POSTGRES_USER | agledger | Bundled postgres username | | POSTGRES_PASSWORD | agledger | Bundled postgres password | | POSTGRES_DB | agledger | Bundled postgres database name |

Application

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | AGLEDGER_MODE | No | standalone | Deployment mode: standalone, gateway, or hub. See Deployment Modes. | | AGLEDGER_EXTERNAL_URL | No | (none) | Canonical external URL for this instance. Cascades to APP_BASE_URL, A2A_BASE_URL, and AGLEDGER_ENDPOINT_URL when those are not explicitly set. Production warns if unset. | | REGISTRATION_ENABLED | No | false | Open self-service registration. When disabled, admin provisions accounts via platform API key. | | TRUST_PROXY | No | false | Set to true when running behind a reverse proxy or load balancer. Required for correct client IP detection. | | LOG_LEVEL | No | info | Logging level: trace, debug, info, warn, error, fatal. | | PORT | No | 3000 | HTTP listen port for the API process. | | HOST | No | 0.0.0.0 | HTTP listen address. | | NODE_ENV | No | production | Node.js environment. | | RATE_LIMIT_ENABLED | No | true | Enable per-route rate limiting. | | RATE_LIMIT_STORE | No | memory | Rate limit store: memory (single-instance) or postgresql (multi-replica). | | CORS_ENABLED | No | true | Enable CORS headers. | | HELMET_ENABLED | No | true | Enable security headers via Helmet. | | WORKER_HEALTH_PORT | No | 3001 | Health probe port for the worker process. |

Licensing

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | AGLEDGER_LICENSE_KEY | No | (none) | Inline PEM-encoded license string. Mutually exclusive with _FILE variant. | | AGLEDGER_LICENSE_KEY_FILE | No | (none) | Path to PEM license file. Takes precedence if both are set. Preferred for Kubernetes (avoids process list leaks). | | AGLEDGER_RELEASE_DATE | No | Baked into image | YYYY-MM-DD release date for perpetual-license version gating. Override only if building from source. |

Removed variable: AGLEDGER_LICENSE_MODE was removed in v0.15.0. The server refuses to start if this variable is still set. Remove it from your .env before upgrading.

Federation

These variables apply only when AGLEDGER_MODE is gateway or hub.

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | AGLEDGER_HUB_URL | Gateway only | (none) | Hub endpoint URL. Required for gateway mode. | | AGLEDGER_REGISTRATION_TOKEN | Gateway only | (none) | Single-use token for initial gateway registration with the hub. | | AGLEDGER_FEDERATION_SIGNING_KEY | Yes (gateway/hub) | (none) | Ed25519 private key (base64 PKCS#8 DER) for federation authentication. Hub mode throws on startup if missing. | | AGLEDGER_FEDERATION_ENCRYPTION_KEY | Yes (gateway/hub) | (none) | X25519 private key (base64 PKCS#8 DER) for federation encryption. Must be generated independently from the signing key. | | AGLEDGER_INSTANCE_ID | No | (none) | Optional instance identifier. | | AGLEDGER_HEARTBEAT_INTERVAL_MS | No | 300000 | Gateway heartbeat interval (5 minutes). | | AGLEDGER_STALENESS_THRESHOLD_MS | No | 900000 | Hub marks a gateway stale after this interval (15 minutes). | | AGLEDGER_CLOCK_SKEW_MAX_MS | No | 300000 | Maximum clock skew for timestamp validation (5 minutes). |

Generate federation keys with scripts/init-federation.sh or scripts/federation-register.mjs.

Observability (OpenTelemetry)

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | OTEL_EXPORTER_OTLP_ENDPOINT | No | (none) | OTLP endpoint for tracing. When unset, tracing is disabled. For the bundled monitoring stack: http://otel-collector:4317. | | OTEL_PROVIDER | No | generic | Trace format: generic (W3C, Jaeger/Tempo) or xray (AWS X-Ray). | | OTEL_SERVICE_NAME | No | agledger-api | Service name in traces. |

Email

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | EMAIL_PROVIDER | No | console | Email backend: ses, smtp, or console (logs to stdout). | | EMAIL_FROM_ADDRESS | No | (none) | From address for outbound email. | | APP_BASE_URL | No | (none) | Base URL for email links. Falls back from AGLEDGER_EXTERNAL_URL if set. |

SMTP settings (when EMAIL_PROVIDER=smtp):

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | SMTP_HOST | Yes | (none) | SMTP server hostname | | SMTP_PORT | No | 587 | SMTP server port | | SMTP_USER | Yes | (none) | SMTP username | | SMTP_PASS | Yes | (none) | SMTP password | | SMTP_SECURE | No | true | Use TLS |

AWS SES settings (when EMAIL_PROVIDER=ses):

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | AWS_REGION | Yes | (none) | AWS region for SES (also used for Bedrock audit analysis). |

SIEM / Audit Export

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | SIEM_ENABLED | No | false | Enable SIEM event export. | | SIEM_FORMAT | No | ocsf | Export format: ocsf or raw. | | SIEM_FILE_PATH | No | (none) | File path for file-based export. | | SIEM_HTTP_URL | No | (none) | HTTP endpoint for push export. | | SIEM_HTTP_TOKEN | No | (none) | Bearer token for HTTP export. | | SIEM_HTTP_HEADERS | No | (none) | Additional headers (JSON string). |

Vault

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | VAULT_SIGNING_KEY | Production | (none) | See Required above. | | VAULT_SIGNING_KEY_PREVIOUS | No | (none) | Previous signing key for zero-downtime key rotation. | | VAULT_ANCHOR_ENABLED | No | false | Enable external checkpoint anchoring to S3/MinIO. |

Security

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | API_KEY_SECRET | Yes | (none) | See Required above. | | API_KEY_SECRET_PREVIOUS | No | (none) | Previous HMAC secret for zero-downtime key rotation. | | WEBHOOK_ENCRYPTION_KEY | No | Derived from API_KEY_SECRET via HKDF | Separate AES-256 key for webhook secret encryption. |

OIDC SSO

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | OIDC_ENABLED | No | false | Enable OIDC authentication for admin endpoints. | | OIDC_ISSUER_URL | When OIDC enabled | (none) | OIDC issuer URL (e.g., https://company.okta.com/oauth2/default). | | OIDC_CLIENT_ID | When OIDC enabled | (none) | OIDC client ID. | | OIDC_CLIENT_SECRET | When OIDC enabled | (none) | OIDC client secret. | | OIDC_ROLE_CLAIM | No | agledger_role | JWT claim that maps to AGLedger roles. |

Trust System

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | SANDBOX_MAX_MANDATES | No | 5 | Max mandates for sandbox-tier agents. | | REGISTRATION_CEILING_MAX | No | 100 | Max concurrent mandate registrations. |

Image Version

| Variable | Required | Default | Description | |----------|---------|---------|-------------| | AGLEDGER_VERSION | No | Set by install.sh | Docker image tag to pull. |

Licensing

Overview

AGLedger uses Ed25519-signed license files with perpetual, per-database binding. No phone-home, no expiry countdown, no quota enforcement. The server always starts with all features enabled.

Tiers

| Tier | License File | What It Means | |------|-------------|---------------| | Free | None (default) | All features enabled. Evaluation, development, single-node. | | Enterprise | Yes (from sales) | All features enabled. Standalone, Gateway, or Hub. Federation included. Per unique database instance. |

All tiers have identical software capabilities. The differences are contractual (ToS, support level, federation permission).

Fail-Open Model

Licensing never prevents startup and never blocks operations. Feature gates are audit-only loggers.

| Scenario | Behavior | |----------|----------| | Valid license, all checks pass | Tier recorded, features enabled | | No license, never had one | Free mode: all features enabled, WARN logs | | No license, but previously had one | Fail-open at last known tier + ERROR logs every 60s | | Bad signature | Fail-open: all features enabled + ERROR logs | | Instance ID mismatch | Fail-open: all features enabled + ERROR logs | | Version too new for license | Fail-open: all features enabled + ERROR logs | | Corrupted/malformed file | Fail-open: all features enabled + ERROR logs |

License Delivery

Two methods (mutually exclusive; _FILE takes precedence):

Inline PEM (Docker Compose .env):

AGLEDGER_LICENSE_KEY=-----BEGIN LICENSE FILE-----\n<base64 content>\n-----END LICENSE FILE-----

File mount (preferred for Kubernetes):

AGLEDGER_LICENSE_KEY_FILE=/run/secrets/agledger-license.pem

For Helm, use secrets.licenseKey (inline PEM in K8s Secret) or license.keyFile.enabled (mount from a separate K8s Secret).

Version Gating

The license contains licensed_through (e.g., 2027-04-04). The Docker image contains AGLEDGER_RELEASE_DATE (e.g., 2026-04-04). At startup:

A customer with licensed_through: 2027-04-04 can run any release built on or before that date.

Database Binding

Each license is bound to a specific database via the instance_id in the system_identity table (created during migration). Multiple pods behind one database require one license. Separate databases require separate licenses.

Activation Workflow

  1. Deploy AGLedger and run migrations.
  2. Retrieve instance ID: GET /admin/license/instance-id
  3. Send instance ID to AGLedger sales.
  4. Receive license PEM file.
  5. Set AGLEDGER_LICENSE_KEY_FILE and restart, or call POST /admin/license/reload (no restart needed).
  6. Verify: GET /admin/license

Removed: AGLEDGER_LICENSE_MODE

This variable was removed in v0.15.0. The server throws on startup if it is still set. Remove it from your .env file. The upgrade.sh script auto-comments it out during upgrades.

Managed Database Compatibility

AGLedger requires PostgreSQL 17+ (18 recommended). All data is in one database, one schema. No Redis, no external cache.

Tested Platforms

| Platform | Connection String Example | |----------|--------------------------| | Aurora Serverless v2 | postgresql://<user>:<pass>@<cluster>.cluster-xxx.<region>.rds.amazonaws.com:5432/agledger?sslmode=require | | RDS (single-instance) | postgresql://<user>:<pass>@<instance>.xxx.<region>.rds.amazonaws.com:5432/agledger?sslmode=require | | Cloud SQL (via proxy or direct) | postgresql://<user>:<pass>@<ip>:5432/agledger?sslmode=require | | Azure Database for PostgreSQL (Flexible Server) | postgresql://<user>:<pass>@<server>.postgres.database.azure.com:5432/agledger?sslmode=require |

Connection Poolers: Do NOT Use

AGLedger uses pg-boss for job queuing, which requires LISTEN/NOTIFY. Transaction-mode connection poolers break this silently. Do not use:

Use direct connections only. AGLedger manages its own connection pool via DATABASE_POOL_MAX.

Pool Sizing

Total connections consumed: pool_max x 2 + 5 (API + Worker + pg-boss overhead).

| Instance Size | Approximate max_connections | Recommended DATABASE_POOL_MAX | |---------------|-------------------------------|--------------------------------| | Aurora Serverless 0.5 ACU | ~90 | 10 | | Aurora Serverless 2 ACU | ~50 | 15 | | RDS db.t3.medium (4 GB) | ~420 | 20 | | RDS db.t3.large+ | ~1000+ | 20 (default is sufficient) |

SSL Configuration

The Docker image bundles the AWS RDS/Aurora global CA bundle at /etc/ssl/certs/rds-global-bundle.pem. To enable sslmode=verify-full with Aurora or RDS:

NODE_EXTRA_CA_CERTS=/etc/ssl/certs/rds-global-bundle.pem

For Cloud SQL or private CAs, mount your certificate and override the path:

NODE_EXTRA_CA_CERTS=/certs/your-ca.pem

Role Separation (Optional)

For least-privilege database access, use two connection strings:

| Connection | Role | Privileges | Variable | |------------|------|-----------|----------| | Migrations (DDL) | DB owner (e.g., clhadmin) | CREATE, ALTER, DROP | DATABASE_URL_MIGRATE | | Runtime (DML) | App role (e.g., agledger_app) | SELECT, INSERT, UPDATE, DELETE | DATABASE_URL |

When DATABASE_URL_MIGRATE is set, the migration runner uses it instead of DATABASE_URL. When unset, both use the same connection.

Reverse Proxy Configuration

AGLedger listens on port 3000 (HTTP). TLS termination should be handled by a reverse proxy. Set TRUST_PROXY=true in AGLedger's .env when running behind any proxy.

Caddy (Recommended for Simplicity)

Caddy provides automatic HTTPS via Let's Encrypt with zero certificate management.

agledger.example.com {
    reverse_proxy localhost:3000

    request_body {
        max_size 1MiB
    }

    rate_limit {
        zone static {
            key    {remote_host}
            events 100
            window 1m
        }
    }

    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }

    log {
        output file /var/log/caddy/agledger.log
    }
}

Steps:

  1. Replace agledger.example.com with your domain.
  2. Ensure DNS points to the server.
  3. Set TRUST_PROXY=true in AGLedger's .env.
  4. Run caddy reload --config /etc/caddy/Caddyfile.

Caddy obtains and renews TLS certificates automatically.

nginx

limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;

upstream agledger {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name agledger.example.com;

    ssl_certificate /etc/ssl/certs/agledger.crt;
    ssl_certificate_key /etc/ssl/private/agledger.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    client_max_body_size 1m;

    location / {
        limit_req zone=api burst=20 nodelay;

        proxy_pass http://agledger;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        proxy_connect_timeout 10s;
        proxy_read_timeout 35s;
        proxy_send_timeout 10s;
    }

    location ~ /\. { deny all; }

    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;
    add_header Referrer-Policy strict-origin-when-cross-origin always;
}

server {
    listen 80;
    server_name agledger.example.com;
    return 301 https://$host$request_uri;
}

The limit_req_zone directive must be in the http {} block. If including this as a snippet in an existing nginx.conf, move that line to the http {} level.

Steps:

  1. Replace agledger.example.com with your domain.
  2. Update ssl_certificate and ssl_certificate_key paths.
  3. Set TRUST_PROXY=true in AGLedger's .env.
  4. Run nginx -t && systemctl reload nginx.

Key Proxy Headers

These headers must be forwarded for AGLedger to function correctly behind a proxy:

| Header | Purpose | |--------|---------| | X-Forwarded-For | Client IP for rate limiting and audit logging | | X-Forwarded-Proto | Protocol detection (HTTPS redirect, secure cookie flags) | | Host | Original hostname for URL generation |

AGLedger's built-in rate limiting (via RATE_LIMIT_ENABLED) works alongside proxy-level rate limiting. The proxy rate limit is defense-in-depth; AGLedger's per-route limits are the primary control.

Deployment Modes

The same Docker image runs in all three modes, controlled by the AGLEDGER_MODE environment variable.

| Mode | Description | Federation | License Tier | |------|------------|-----------|-------------| | standalone | Full accountability engine. No network dependencies beyond the database. | None | Free or Enterprise | | gateway | Standalone engine plus federation client. Connects to a Hub for cross-instance coordination. | Client | Enterprise | | hub | Full federation coordinator. Gateway registry, routing, state sync, agent directory, reputation. | Coordinator | Enterprise |

When to Use Each Mode

Standalone — Default for all new deployments. Use for single-organization accountability tracking. No federation configuration required. Works with Free or Enterprise license.

Gateway — Use when your AGLedger instance needs to participate in cross-organization mandate workflows coordinated by a Hub. Requires AGLEDGER_HUB_URL, AGLEDGER_FEDERATION_SIGNING_KEY, and AGLEDGER_FEDERATION_ENCRYPTION_KEY. Registration with the Hub requires a single-use AGLEDGER_REGISTRATION_TOKEN.

Hub — Use when you operate the federation coordinator. Manages gateway registration, cross-boundary mandate routing, schema catalog, and global reputation. The Hub throws on startup if AGLEDGER_FEDERATION_SIGNING_KEY is not set.

Privacy Boundary

Criteria values, receipt evidence, audit vault entries, and entity references never cross federation boundaries. Only state transitions, agent IDs, contract types, settlement signals, and reputation aggregates cross.

Helm Chart Reference

The Helm chart mirrors environment variable configuration through values.yaml. Key mappings:

| Helm Value | Environment Variable | |------------|---------------------| | config.mode | AGLEDGER_MODE | | config.logLevel | LOG_LEVEL | | config.trustProxy | TRUST_PROXY | | config.registrationEnabled | REGISTRATION_ENABLED | | config.otelProvider | OTEL_PROVIDER | | config.emailProvider | EMAIL_PROVIDER | | config.nodeExtraCaCerts | NODE_EXTRA_CA_CERTS | | database.externalUrl | DATABASE_URL | | database.poolMax | DATABASE_POOL_MAX | | secrets.apiKeySecret | API_KEY_SECRET | | secrets.vaultSigningKey | VAULT_SIGNING_KEY | | secrets.webhookEncryptionKey | WEBHOOK_ENCRYPTION_KEY | | secrets.databaseUrlMigrate | DATABASE_URL_MIGRATE | | secrets.licenseKey | AGLEDGER_LICENSE_KEY | | secrets.existingSecret | Use a pre-existing K8s Secret (must contain DATABASE_URL, API_KEY_SECRET, VAULT_SIGNING_KEY as keys) | | license.keyFile.enabled | Mount PEM from a K8s Secret via AGLEDGER_LICENSE_KEY_FILE | | license.releaseDate | AGLEDGER_RELEASE_DATE |

Resource Defaults

| Component | CPU Request | CPU Limit | Memory Request | Memory Limit | |-----------|-----------|----------|---------------|-------------| | API | 250m | 1 | 256Mi | 512Mi | | Worker | 250m | 1 | 256Mi | 512Mi | | Bundled PostgreSQL | — | — | 256Mi | 1Gi |

Security Defaults

Both API and Worker pods run as non-root (UID 65532) with read-only root filesystem, no privilege escalation, and all capabilities dropped. Network policy restricts ingress to pods labeled as ingress-nginx or traefik.


Configuration reference for AGLedger self-hosted deployment.