Environment Variables
All environment variables can be set in your shell, a .env file (loaded by python-dotenv if installed), or as Docker -e / Compose environment: entries.
Core server
| Variable | Default | Required | Description |
|---|---|---|---|
P8G_JWT_SECRET | insecure dev default | Yes (production) | JWT signing secret — generate with python -c "import secrets; print(secrets.token_hex(32))". The server logs a warning and refuses to start in P8G_ENV=production with the default secret. |
P8G_ADMIN_EMAIL | — | Yes | Bootstrap admin email (used on first start to create the initial admin user) |
P8G_ADMIN_PASSWORD | — | Yes | Bootstrap admin password |
P8G_ADMIN_USERNAME | — | No | Bootstrap admin username (defaults derived from email if unset) |
P8G_HOST | 0.0.0.0 | No | Interface the REST server binds to |
P8G_PORT | 8000 | No | TCP port (the serve CLI subcommand and Docker images default to 8100 instead — check your entry point) |
P8G_DATA_PATH | ./data | No | Root directory for graph storage, licensing state, and CDC journal |
WORKERS | 4 (via production.py) / unset (via secure_server.py, which defaults to core-count heuristics) | No | Number of Uvicorn worker processes. P8G_ALLOW_MULTI_WORKER=1 required to run more than 4 |
LOG_LEVEL | info | No | debug / info / warning / error |
P8G_CORS_ORIGINS | * | No | Comma-separated list of allowed CORS origins |
P8G_ENV | — | No | development / production / staging — gates dev-only conveniences (insecure default secret, verbose errors) |
P8G_READONLY | 0 | No | 1 = start this instance as a read replica (rejects writes) |
P8G_JWT_SECRET
Never use a short or guessable secret in production. Generate one with:
python -c "import secrets; print(secrets.token_hex(32))"LLM / embedding provider configuration
LLM and embedding providers are configured at runtime via PUT /rag/config, not via environment variables. The 39-model LLM registry and 21-model embedding registry (RAG Studio) are swapped live with no restart — see the RAG Studio guide. There is no LLM_PROVIDER / LLM_MODEL / ANTHROPIC_API_KEY / GOOGLE_API_KEY env-var scheme in this codebase.
A few narrow env vars remain as fallbacks/managed-proxy options, not the primary configuration path:
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY | — | Fallback key used by a small number of code paths (journey AI advisor, late chunking) when no per-request/per-config key is supplied |
PURPLE8_OPENAI_KEY | — | Alternate OpenAI key override consulted by the RAG router |
PURPLE8_MANAGED_LLM_KEY / PURPLE8_MANAGED_LLM_MODEL | — / gpt-5.4-mini | Purple8-managed LLM proxy credentials (used only when the managed-LLM feature is enabled) |
Vector index
| Variable | Default | Description |
|---|---|---|
P8G_EMBEDDING_DIMENSION | 1536 | Embedding vector dimension — must match your embedding model. Used as the fallback when not set explicitly on GraphEngine(embedding_dimension=...) |
P8G_HNSW_M | 16 | HNSW M parameter — higher = better recall, more RAM |
P8G_HNSW_EF_CONSTRUCTION | 100 (segmented engine index) / 200 (standalone VectorIndex) | Build-time recall quality (higher = slower build, better index) |
P8G_HNSW_SEARCH_WORKERS | auto | Upper bound on parallel segment workers (logical CPUs, capped at 32). Native hnswlib searches with Python eligibility predicates run segments inline to avoid callback contention; unfiltered and other-backend searches retain parallel fan-out. A limit of 1 runs inline without an executor. |
P8G_HNSW_SEGMENT_SIZE | 50000 | Nodes per HNSW segment before a new segment is sealed |
P8G_HNSW_MAX_HOT_SEGS | hardware-derived | Max segments kept hot in memory before NVMe tiering seals them |
P8G_HNSW_STORAGE_PATH | — | Optional separate storage path for sealed (cold) HNSW segments |
P8G_VECTOR_QUANTIZATION | off | Set to enable int8/binary quantization mode |
P8G_VECTOR_BACKEND | hnswlib | Vector index backend selector |
AUTO_CONFIG | n/a | There is no AUTO_CONFIG env var — pass GraphEngine(..., auto_config=True) in code instead. See Architecture for CortexAutoConfig. |
CortexAutoConfig
GraphEngine(..., auto_config=True) enables CortexAutoConfig, which probes available RAM at startup and derives (M, ef_construction, segment_size, batch_size) such that peak process RSS never exceeds ~75% of available RAM. This is a constructor argument, not an environment variable — set it in your GraphEngine(...) call.
Construction settings are read when a new index is created, not when the vector module is imported. Auto-config overrides the environment for new indexes; explicit vector-index constructor arguments take precedence over environment settings. Loading an existing index retains its saved construction settings: changing M or ef_construction requires rebuilding the index to affect existing vectors. Hardware sizing is not a measured recall guarantee; validate retrieval quality on representative queries separately.
KMS / Encryption at rest
| Variable | Required | Description |
|---|---|---|
KMS_PROVIDER | When encrypting | local | vault | aws | gcp | azure |
KMS_KEY_ID | When provider is external | Provider-specific key identifier |
KMS_LOCAL_KEYSTORE | No (local provider) | Path to the local keystore directory (default: ./data/kms) |
VAULT_ADDR | Vault | HashiCorp Vault server URL (default: http://127.0.0.1:8200) |
VAULT_TOKEN | Vault | Vault token |
VAULT_MOUNT | No | Vault transit engine mount path (default: transit) |
AWS_DEFAULT_REGION | AWS | AWS region (e.g. us-east-1) — not AWS_REGION |
AWS_KMS_ENDPOINT | No | Override KMS endpoint (e.g. for LocalStack testing) |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | AWS (key auth) | Standard AWS SDK credential vars — falls back to IAM role/instance profile if unset |
GCP_PROJECT | GCP | GCP project ID |
GCP_KMS_LOCATION | No | KMS key ring location (default: global) |
GCP_KMS_KEYRING | No | KMS key ring name (default: purple8) |
GOOGLE_APPLICATION_CREDENTIALS | GCP | Standard Google Cloud SDK var — path to service account JSON, read implicitly by the google-cloud client library |
AZURE_VAULT_URL | Azure | Azure Key Vault URL |
AZURE_TENANT_ID / AZURE_CLIENT_ID / AZURE_CLIENT_SECRET | Azure (SP auth) | Standard azure-identity SDK vars, read implicitly by DefaultAzureCredential — not read directly by Purple8 code |
Multi-tenancy, write admission & backups (Six-Sigma operational controls)
Tenancy mode (shared key-prefix vs. dedicated per-tenant storage path) is configured programmatically through tenancy.py / the admin REST API. The following operational environment variables control sharding, write-admission safety gates, and streaming backup archives:
| Variable | Default | Description |
|---|---|---|
P8G_SHARD_COUNT | 1 | Number of horizontal shards for tenant data distribution |
P8G_WRITE_ADMISSION_LANES | 4 | Concurrent write-admission lanes |
P8G_WRITE_ADMISSION_RSS_LIMIT_MB | 0.85 × available_ram | Maximum process RSS (in MB) before the write admission controller rejects incoming writes with 503 + Retry-After |
P8G_WRITE_ADMISSION_DISK_MAX_PCT | 95.0 | Six-Sigma disk high-watermark gate: rejects writes before the underlying storage volume hits ENOSPC (default 95.0%; set 0 to disable) |
P8G_BACKUP_MAX_ARCHIVE_MB | 2048 | Six-Sigma restore safety ceiling: maximum backup archive size in MB accepted during restore_backup (default 2048 MB; raises ValueError if exceeded) |
P8G_BACKUP_JSONL | 0 | Six-Sigma streaming backup mode: set P8G_BACKUP_JSONL=1 to write and restore backup archives line-by-line in streaming JSONL format without materializing a monolithic JSON payload in RAM |
Online Storage Scrub
In addition to backup and write-admission controls, operators can trigger an online, non-destructive bit-rot and CRC32 integrity verification across BrickWAL frames and cold BrickCoreStorage slabs via POST /admin/storage/scrub.
DocIntel integration
| Variable | Default | Description |
|---|---|---|
PURPLE8_DOCINTEL_URL | — | URL of the Purple8 DocIntel service (e.g. http://localhost:8200) |
PURPLE8_DOCINTEL_KEY | — | API key for the DocIntel service |
PURPLE8_DOCINTEL_URL_{SUFFIX} / PURPLE8_DOCINTEL_KEY_{SUFFIX} | — | Per-suffix overrides for multi-instance DocIntel routing |
WAL / durability
There is no P8G_WAL_SYNC toggle — WAL fdatasync is unconditional (durability-by-default; see the Architecture guide on the Cortex seal-and-evict cycle). The tunables that do exist:
| Variable | Default | Description |
|---|---|---|
P8G_WAL_DIR_FSYNC_INTERVAL | 64 | How often (in writes) the containing directory itself is fsync'd |
P8G_WAL_CHECKPOINT_RECORDS | 1000000 | WAL record count between checkpoints |
P8G_WAL_REJECT_NFS | 0 | 1 = refuse to start if the data path is on an NFS mount (NFS does not guarantee fdatasync semantics) |
JWT / auth token TTL
There are no JWT_ACCESS_TOKEN_EXPIRE_MINUTES / JWT_REFRESH_TOKEN_EXPIRE_DAYS environment variables in the current codebase — token TTLs are fixed in code, not environment-configurable today.
OpenTelemetry (optional)
| Variable | Default | Description |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | — | OTLP endpoint (e.g. http://jaeger:4317) — enables tracing when set |
OTEL_SERVICE_NAME | purple8-hyper-graph | Service name reported to the OTEL collector |
Alerting (optional)
| Variable | Default | Description |
|---|---|---|
P8G_ALERT_MIN_LEVEL | ERROR | Minimum severity that triggers an alert dispatch |
P8G_SLACK_WEBHOOK_URL | — | Slack incoming webhook URL |
P8G_PD_KEY | — | PagerDuty integration key |
P8G_OG_KEY / P8G_OG_TEAM | — | OpsGenie API key / team |
P8G_ALERT_WEBHOOK_URL | — | Generic webhook fallback |
Replication (optional)
| Variable | Default | Description |
|---|---|---|
P8G_REPLICATION_ROLE | standalone | standalone | primary | follower |
P8G_REPLICATION_MODE | async | async | sync |
P8G_PRIMARY_HOST / P8G_PRIMARY_PORT | — / 8101 | Primary node address (follower config) |
P8G_FOLLOWER_ADDRESSES | — | Comma-separated follower addresses (primary config) |
P8G_REPLICATION_SYNC_REPLICAS | 1 | Number of replicas that must ack before a sync write returns |
P8G_REPLICATION_AUTH_TOKEN | — | Shared auth token between primary and followers. Also authorises automated POST /replication/promote via the X-Replication-Token header |
P8G_REPLICATION_TLS | 0 | Set to 1 for active replication; authentication and TLS are required unless the explicit loopback development exception applies |
P8G_REPLICATION_BIND_HOST | 127.0.0.1 | Primary listener address; explicitly configure a network interface for cross-host replication |
P8G_REPLICATION_TLS_CERT / P8G_REPLICATION_TLS_KEY | — | Server certificate and key; configure on followers that may be promoted too |
P8G_REPLICATION_TLS_CA | System trust store | CA bundle for follower certificate verification and automated HTTPS promotion |
P8G_REPLICATION_ALLOW_INSECURE_LOCAL | 0 | Explicit plaintext/tokenless WAL development exception; requires P8G_ENV=development and loopback binding/primary destination. Never permits insecure cross-host replication |
Automated failover (follower nodes, optional)
| Variable | Default | Description |
|---|---|---|
P8G_FAILOVER_ENABLED | 0 | 1 = run the failover watchdog on this follower. Ignored on primary/standalone |
P8G_FAILOVER_PRIMARY_ADDRESS | P8G_PRIMARY_HOST:P8G_FAILOVER_HTTP_PORT | Primary HTTP address (host:port) probed at /health |
P8G_FAILOVER_HTTP_PORT | P8G_PORT or 8080 | HTTP port used when deriving addresses |
P8G_NODE_ADDRESS | <hostname>:port | This node's HTTP address; the promote call is sent here |
P8G_NODE_ID | hostname | Registry member ID for this node |
P8G_FAILOVER_PRIMARY_ID | primary@<address> | Registry member ID for the primary |
P8G_FAILOVER_UNHEALTHY_THRESHOLD | 3 | Consecutive failed probes before promotion |
P8G_FAILOVER_INTERVAL_S | 5 | Seconds between probes |
P8G_FAILOVER_PROBE_TIMEOUT_S | 3 | Per-probe HTTP timeout |
P8G_FAILOVER_COOLDOWN_S | 60 | Minimum seconds between two failovers |
P8G_FAILOVER_STARTUP_GRACE_S | 300 | Probe failures are not counted until the primary has been seen healthy once, or this many seconds have elapsed since the watchdog started. Prevents a follower from promoting itself against a primary that is merely still booting (image pull, WAL replay). 0 disables the gate |
P8G_FAILOVER_MAX_LAG | 10000 | Max replication_lag (ops) for a node to be eligible for promotion |
P8G_FAILOVER_TARGET_BACKEND | noop | noop (log only) | elbv2 (AWS ALB/NLB target group swap; requires boto3) |
P8G_FAILOVER_WRITE_TG_ARN | — | elbv2: target group receiving write traffic |
P8G_FAILOVER_READ_TG_ARN | — | elbv2: optional target group receiving GET/HEAD |
P8G_FAILOVER_TARGET_PORT | port from address | elbv2: port registered on the target group |
P8G_NODE_TARGET_ID / P8G_FAILOVER_PRIMARY_TARGET_ID | host from address | elbv2: target Id override (EC2 instance ID for instance-type target groups) |
Slot-based role discovery (rebuild-safe; set on every node)
A slot is a stable identity for an Auto Scaling group / launch-template position (e.g. a for AZ-a, b for AZ-b). Unlike an instance ID or hostname it survives instance replacement. Recording which slot is primary durably is what lets an instance rebuilt in the old primary's ASG boot as a follower after a failover, instead of re-asserting itself as primary from a hard-coded launch template.
| Variable | Default | Description |
|---|---|---|
P8G_NODE_SLOT | — | This node's slot. Stored as ClusterMember.metadata["slot"]. Required for every hook below |
P8G_FAILOVER_PRIMARY_SLOT | — | Follower only: the slot the current primary occupies (read from the slot parameter at boot) |
P8G_FAILOVER_PRIMARY_SLOT_PARAM | — | AWS SSM Parameter Store name (e.g. /purple8/<instance>/primary-slot). Follower: on promotion the watchdog writes its own slot here (SsmPrimarySlotRecorder). Primary: starts PrimarySlotFence — polls the parameter every P8G_FAILOVER_INTERVAL_S and freezes all writes (503) the moment it no longer names this node's slot. Read errors never fence |
P8G_FAILOVER_PRIMARY_ALIAS_ZONE_ID | — | Route 53 hosted zone containing the stable primary alias |
P8G_FAILOVER_PRIMARY_ALIAS_FQDN | — | The CNAME to keep pointing at the primary, e.g. primary.<instance>.purple8.internal. Used by consumers configured with a fixed hostname rather than the load balancer (e.g. DocIntel GRAPH__BASE_URL) |
P8G_FAILOVER_PRIMARY_ALIAS_TARGET_TEMPLATE | — | CNAME target with a {slot} placeholder, e.g. slot-{slot}.<instance>.purple8.internal. All three _ALIAS_* vars must be set to activate Route53PrimaryAliasRecorder |
All hooks are cumulative: the load-balancer swap, the slot record and the alias update run on every promotion, and one hook's failure does not skip the others. Required IAM: ssm:GetParameter, ssm:PutParameter on the parameter; route53:ChangeResourceRecordSets on the zone. Inside a container on EC2 set the launch template's IMDSv2 HttpPutResponseHopLimit to 2, otherwise boto3 cannot obtain instance-role credentials and every hook fails.
With more than one follower, enable the Raft coordination backend (P8G_RAFT_ENABLED) so the election guard and STONITH fencing apply; with a single follower it self-promotes.
Licensing (self-hosted deployments)
Self-hosted deployments use a two-tier licensing model:
- Developer (Self-Hosted Free):
$0, up to50Knodes,1MCP agent, full local engine for development, prototyping, and evaluation (no license key required). - Enterprise / Custom (Self-Hosted): The single commercial self-hosted tier — tailored to customer capacity, cluster topology, HA, and SLA.
| Variable | Default | Description |
|---|---|---|
PURPLE8_LICENSE_JWT / PURPLE8_LICENSE_DATA / PURPLE8_LICENSE_FILE | — | License lookup order: JWT string → base64 data → file path |
PURPLE8_INSTALL_TYPE | — | Deployment channel hint (affects license enforcement gating) |
PURPLE8_DEV_MODE | 0 | 1 = skip license enforcement entirely (development only) |
LICENSE__ENFORCE | true | false = disable enforcement even in non-dev environments |