Skip to content
Self-hosting

Configuration

The self-host configuration model: deployment env, private per-repo policy, feature flags, and review modes.

This page is the exhaustive reference. For the short path — the required secrets plus a conservative first-boot config — start with .env.selfhost.example in Quickstart instead.

Config layers

Environment
Deployment-wide infrastructure, secrets, feature kill switches, and service URLs. Requires restart or recreate when changed.
Private repo config
Mounted GITTENSORY_REPO_CONFIG_DIR files for private per-repo policy. Read fresh each review.
Public repo config
The repo .gittensory.yml. Useful for transparent policy, but not for thresholds or rules you need to keep private.
Built-in defaults
Safe fallback when nothing is configured. Gate off, AI off, and no repo runs per-PR features until allowlisted.

Required baseline env

.env
PUBLIC_API_ORIGIN=https://reviews.example.com
GITHUB_APP_ID=123456
GITHUB_APP_SLUG=my-gittensory-app
GITHUB_APP_PRIVATE_KEY_FILE=/run/secrets/github-app-private-key.pem
GITHUB_WEBHOOK_SECRET=<random-webhook-secret>

GITTENSOR_REGISTRY_URL=https://example.invalid/registry.json
GITTENSORY_API_TOKEN=<random-32-byte-token>
GITTENSORY_MCP_TOKEN=<random-32-byte-token>
INTERNAL_JOB_TOKEN=<random-32-byte-token>

Any FOO_FILE is loaded into FOO at startup. Explicit FOO wins over the file variant.

MCP_ACTUATION_REPO_ALLOWLIST
GITTENSORY_MCP_TOKEN is a shared, end-user-obtainable CLI credential (the normal alternative to gittensory-mcp login), so it must not implicitly stage actions (merges, closes, approvals) on every repo the App happens to be installed on. MCP_ACTUATION_REPO_ALLOWLIST scopes it to an explicit, comma/whitespace-separated owner/repo list — unset denies all actuation for this token. Set it to * or all to opt back into the pre-scoping, any-repo behavior. If you already rely on GITTENSORY_MCP_TOKEN for approval-queue actuation, set this variable after upgrading or MCP actuation stops working.
.env
# Deny-by-default: unset means the static MCP token cannot stage or decide any action.
MCP_ACTUATION_REPO_ALLOWLIST=owner/repo-one, owner/repo-two
# Restore pre-upgrade any-repo behavior:
# MCP_ACTUATION_REPO_ALLOWLIST=*

MCP_READ_REPO_ALLOWLIST is the same fail-closed/wildcard model, kept as a separate allowlist so read-only MCP tools (repo context, issue quality, watch subscriptions) can be granted independently of actuation trust. The full */all wildcard additionally unlocks the non-repo-scoped contributor/operator tools.

GitHub API cache

Redis backs shared caching for stable GitHub GET responses, including repeated installation, repo/user metadata, and branch-protection required-status reads. Keys include the caller identity and response-shaping headers, and cold misses are single-flighted so concurrent jobs do not stampede GitHub.

.env
GITHUB_CACHE_TTL_SECONDS=20
GITHUB_BRANCH_PROTECTION_CACHE_TTL_SECONDS=1200
GITHUB_METADATA_CACHE_TTL_SECONDS=600
GITHUB_CACHE_TTL_SECONDS is the short default for repeated safe GitHub GETs. Stable repo/user metadata and branch-protection required-status reads use the per-class TTLs above so operators can keep repeated policy reads hot without broadening stale cache risk. Live CI status, check-run, check-suite, pull/issue subresources, pull mergeability, token minting, rate-limit, and collaborator-permission endpoints are never served from this cache. Prometheus exports gittensory_github_response_cache_total, and the bundled self-host Grafana dashboard includes the hit/miss/coalesced/error breakdown.

Generated env reference

This table is generated from process.env.NAME reads in src/selfhost/** and src/server.ts. It intentionally includes names and first source references only, never example values.

self-host env vars
| Name | First reference |
| --- | --- |
| `AI_COMBINE` | `src/selfhost/ai.ts:982` |
| `AI_EMBED_API_KEY` | `src/server.ts:440` |
| `AI_EMBED_BASE_URL` | `src/server.ts:437` |
| `AI_EMBED_MODEL` | `src/selfhost/ai.ts:872` |
| `AI_ON_MERGE` | `src/selfhost/ai.ts:984` |
| `AI_PROVIDER` | `src/selfhost/ai-config.ts:43` |
| `ANTHROPIC_AI_BASE_URL` | `src/selfhost/ai.ts:876` |
| `ANTHROPIC_AI_MODEL` | `src/selfhost/ai.ts:85` |
| `ANTHROPIC_API_KEY` | `src/selfhost/ai.ts:875` |
| `BACKUP_ACKNOWLEDGED` | `src/server.ts:379` |
| `BROWSER_WS_ENDPOINT` | `src/selfhost/stubs/puppeteer.ts:11` |
| `CLAUDE_AI_EFFORT` | `src/selfhost/ai.ts:136` |
| `CLAUDE_AI_MODEL` | `src/selfhost/ai.ts:77` |
| `CLAUDE_AI_TIMEOUT_MS` | `src/selfhost/ai.ts:136` |
| `CODEX_AI_EFFORT` | `src/selfhost/ai.ts:140` |
| `CODEX_AI_MODEL` | `src/selfhost/ai.ts:81` |
| `CODEX_AI_TIMEOUT_MS` | `src/selfhost/ai.ts:140` |
| `CODEX_HOME` | `src/selfhost/ai.ts:302` |
| `CRON_INTERVAL_MS` | `src/server.ts:916` |
| `DATABASE_PATH` | `src/server.ts:249` |
| `DATABASE_URL` | `src/selfhost/preflight.ts:201` |
| `DISCORD_REPO_WEBHOOKS` | `src/services/notify-discord.ts:41` |
| `DISCORD_WEBHOOK_URL` | `src/services/notify-discord.ts:78` |
| `FOREGROUND_LIVENESS_CHECK_INTERVAL_MS` | `src/selfhost/foreground-liveness.ts:52` |
| `FOREGROUND_LIVENESS_ENABLED` | `src/selfhost/foreground-liveness.ts:41` |
| `FOREGROUND_LIVENESS_MAX_DEFER_MS` | `src/selfhost/foreground-liveness.ts:51` |
| `FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP` | `src/selfhost/foreground-liveness.ts:53` |
| `GITHUB_APP_ID` | `src/selfhost/orb-collector.ts:59` |
| `GITHUB_APP_PRIVATE_KEY` | `src/selfhost/orb-collector.ts:166` |
| `GITHUB_CACHE_TTL_SECONDS` | `src/server.ts:508` |
| `GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS` | `src/selfhost/installation-concurrency-admission.ts:47` |
| `GITHUB_INSTALLATION_CONCURRENCY_ENABLED` | `src/selfhost/installation-concurrency-admission.ts:34` |
| `GITHUB_INSTALLATION_CONCURRENCY_LIMIT` | `src/selfhost/installation-concurrency-admission.ts:43` |
| `GITTENSORY_REPO_CONFIG_DIR` | `src/server.ts:288` |
| `GITTENSORY_VERSION` | `src/selfhost/otel.ts:62` |
| `HOME` | `src/selfhost/ai.ts:302` |
| `MAINTENANCE_ADMISSION_DEFER_MS` | `src/selfhost/maintenance-admission.ts:171` |
| `MAINTENANCE_ADMISSION_DRAIN_AGE_MS` | `src/selfhost/maintenance-admission.ts:145` |
| `MAINTENANCE_ADMISSION_ENABLED` | `src/selfhost/maintenance-admission.ts:126` |
| `MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING` | `src/selfhost/maintenance-admission.ts:167` |
| `MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS` | `src/selfhost/maintenance-admission.ts:141` |
| `MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS` | `src/selfhost/maintenance-admission.ts:155` |
| `MAINTENANCE_ADMISSION_MAX_LIVE_PENDING` | `src/selfhost/maintenance-admission.ts:151` |
| `MAINTENANCE_ADMISSION_MAX_PENDING` | `src/selfhost/maintenance-admission.ts:159` |
| `MIGRATIONS_DIR` | `src/server.ts:392` |
| `OBSERVABILITY_SMOKE_POLL_MS` | `scripts/smoke-observability-traces.mjs:8` |
| `OBSERVABILITY_SMOKE_TIMEOUT_MS` | `scripts/smoke-observability-traces.mjs:6` |
| `OLLAMA_AI_API_KEY` | `src/selfhost/ai.ts:869` |
| `OLLAMA_AI_BASE_URL` | `src/selfhost/ai.ts:865` |
| `OLLAMA_AI_MODEL` | `src/selfhost/ai.ts:89` |
| `OPENAI_AI_BASE_URL` | `src/selfhost/ai.ts:867` |
| `OPENAI_AI_MODEL` | `src/selfhost/ai.ts:90` |
| `OPENAI_API_KEY` | `src/selfhost/ai.ts:869` |
| `OPENAI_COMPATIBLE_AI_API_KEY` | `src/selfhost/ai.ts:869` |
| `OPENAI_COMPATIBLE_AI_BASE_URL` | `src/selfhost/ai.ts:868` |
| `OPENAI_COMPATIBLE_AI_MODEL` | `src/selfhost/ai.ts:91` |
| `ORB_AIR_GAP` | `src/selfhost/orb-collector.ts:161` |
| `ORB_ANONYMIZE` | `src/selfhost/orb-collector.ts:174` |
| `ORB_APP_ID` | `src/selfhost/orb-collector.ts:59` |
| `ORB_BROKER_URL` | `src/server.ts:965` |
| `ORB_COLLECTOR_TOKEN` | `src/selfhost/orb-collector.ts:205` |
| `ORB_COLLECTOR_URL` | `src/selfhost/orb-collector.ts:172` |
| `ORB_ENROLLMENT_SECRET` | `src/selfhost/orb-collector.ts:165` |
| `ORB_RELAY_MODE` | `src/server.ts:967` |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `src/selfhost/otel.ts:47` |
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `src/selfhost/otel.ts:45` |
| `OTEL_SERVICE_ENVIRONMENT` | `src/selfhost/otel.ts:60` |
| `OTEL_SERVICE_NAME` | `src/selfhost/otel.ts:59` |
| `OTEL_TRACES_EXPORTER` | `src/selfhost/otel.ts:40` |
| `OTEL_TRACES_SAMPLER` | `src/selfhost/otel.ts:74` |
| `OTEL_TRACES_SAMPLER_ARG` | `src/selfhost/otel.ts:76` |
| `PGPOOL_MAX` | `src/selfhost/queue-common.ts:713` |
| `PGVECTOR_ENABLED` | `src/server.ts:229` |
| `PORT` | `src/server.ts:715` |
| `PUBLIC_API_ORIGIN` | `src/selfhost/preflight.ts:192` |
| `QDRANT_API_KEY` | `src/selfhost/qdrant-vectorize.ts:50` |
| `QDRANT_DIM` | `src/selfhost/qdrant-vectorize.ts:71` |
| `QDRANT_URL` | `src/server.ts:527` |
| `QUEUE_BACKGROUND_CONCURRENCY` | `src/selfhost/queue-common.ts:130` |
| `QUEUE_CONCURRENCY` | `src/selfhost/pg-queue.ts:285` |
| `QUEUE_DEAD_LETTER_AUTO_RETRY_MAX_EXTRA_ATTEMPTS` | `src/selfhost/queue-common.ts:721` |
| `QUEUE_STARTUP_JITTER_MIN_JOBS` | `src/selfhost/queue-common.ts:702` |
| `REDIS_URL` | `src/selfhost/preflight.ts:144` |
| `REVIEW_AUDIT_DIR` | `src/server.ts:572` |
| `SELFHOST_BUNDLE_ALL` | `scripts/build-selfhost.mjs:13` |
| `SELFHOST_SERVICE` | `scripts/smoke-observability-traces.mjs:5` |
| `SELFHOST_SETUP_TOKEN` | `src/selfhost/preflight.ts:186` |
| `SENTRY_DSN` | `src/selfhost/sentry.ts:365` |
| `SENTRY_ENVIRONMENT` | `src/selfhost/otel.ts:60` |
| `SENTRY_RELEASE` | `src/selfhost/otel.ts:62` |
| `SENTRY_SERVER_NAME` | `src/selfhost/sentry.ts:383` |
| `SENTRY_TRACES_SAMPLE_RATE` | `src/selfhost/sentry.ts:171` |
| `SETUP_OUTPUT_PATH` | `src/server.ts:832` |
| `SLACK_WEBHOOK_URL` | `src/services/notify-discord.ts:173` |

Per-PR feature flags

Most review capabilities need both their own flag and the repo in GITTENSORY_REVIEW_REPOS. This gives you a global kill switch and a per-repo rollout switch.

.env
GITTENSORY_REVIEW_REPOS=owner/repo,owner/another
GITTENSORY_REVIEW_UNIFIED_COMMENT=true
GITTENSORY_REVIEW_INLINE_COMMENTS=false
GITTENSORY_REVIEW_SAFETY=true
GITTENSORY_REVIEW_GROUNDING=true
GITTENSORY_REVIEW_RAG=false
GITTENSORY_REVIEW_ENRICHMENT=false
GITTENSORY_REVIEW_REPUTATION=false
Empty GITTENSORY_REVIEW_REPOS means no repos run the per-PR feature path, regardless of the individual flags.

Private per-repo config

Mount a gitignored directory and point GITTENSORY_REPO_CONFIG_DIR at it. If either a per-repo file or the dir-root global default (.gittensory.yml at the mount root) exists, the public repo .gittensory.yml is never fetched for that review. With only one of the two present, its contents are used as-is; with both present, they are deep-merged — the per-repo file overlaid onto the global default, nested mappings merging key by key and arrays replacing wholesale.

config directory
gittensory-config/
  owner__repo/.gittensory.yml
  repo-name/.gittensory.yml
  owner__repo.yml
  .gittensory.yml
owner__repo/.gittensory.yml
gate:
  enabled: true
  aiReview:
    mode: advisory
    allAuthors: true
settings:
  commentMode: all_prs
  includeMaintainerAuthors: true
  autonomy:
    merge: observe
    close: observe
  agentDryRun: false
features:
  safety: true
  unifiedComment: true
  rag: false
  reputation: false

Instance-wide write switches

Unset
Normal mode. Per-repo autonomy and GitHub permissions decide what can be written.
dry-run
Compute reviews and audit as shadow, but suppress comments, checks, labels, merges, and closes.
disabled
Suppress writes as denied. Use when you need a hard instance-wide stop.

Next steps

Configure the GitHub integration in GitHub App and Orb, then add optional context through AI providers, REES, or RAG.