Configuration

On this page 204

Every variable this instance reads, from .env.example and from the source that reads it. Generated, because a configuration page written separately drifts and does so silently: this one replaced a hand-written table that still named a search engine the application had stopped using months earlier.

Nothing is committed. .env is gitignored, values can come from a file instead by naming it in <NAME>_FILE, and buddy instance:check reads the ones that have to be right and says which are wrong and what to do about them.

Generated from .env.example and the source that reads it.

The application

APP_NAME

Default: ReviewOS.

No reader in app/, routes/ or resources/: this one is the framework's.

APP_ENV

Default: development.

Read by app/Ops/config.ts.

APP_KEY

Default: empty. Checked at boot, so a wrong value stops the instance with a sentence rather than failing quietly later.

Read by app/Actions/Auth/LoginAction.ts, app/Actions/Auth/SsoAction.ts.

APP_URL

Default: reviewos.localhost. Checked at boot, so a wrong value stops the instance with a sentence rather than failing quietly later.

Pretty HTTPS URLs are the default development experience. ./buddy dev routes this hostname through rpx, which uses tlsx for local certificates.

Read by app/Actions/Atproto/client.ts, app/Actions/Auth/passkeys.ts, app/Actions/Auth/SsoAction.ts, app/Actions/Notification/vapid.ts, app/Actions/Screenshot/capture.ts, app/Jobs/CheckpointRepositoriesJob.ts, app/Jobs/SendNotificationJob.ts, resources/components/CloneUrlBox.stx, resources/components/RepoBrowser.stx, resources/views/register.stx.

APP_MAINTENANCE

Default: false.

No reader in app/, routes/ or resources/: this one is the framework's.

APP_MAINTENANCE_SECRET

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

APP_COMING_SOON

Default: false.

No reader in app/, routes/ or resources/: this one is the framework's.

APP_COMING_SOON_SECRET

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

APP_PORT

Default: unset.

Two values the git hooks read, set by the process that starts them rather than by hand:

APP_PORT is a fallback for PORT when building the loopback URL a hook posts back to, so a hook still reaches the instance when only APP_PORT is set.

REVIEWOS_ACTOR_ID attributes a push to an account. The SSH server sets it for the connection it authenticated; a value in .env would attribute every push on the instance to one person.

Read by app/Actions/Git/hooks.ts.

Local development

SUDO_PASSWORD

Default: empty.

Optional automation credential for non-interactive environments. Interactive users should run ./buddy setup:ssl once instead of storing a sudo password. Local-only, never commit a real value because .env is gitignored.

No reader in app/, routes/ or resources/: this one is the framework's.

Encrypted values

DOTENV_PUBLIC_KEY

Default: empty.

.env encryption (stacksjs/stacks#1053). Generated by buddy key:generate. Public key encrypts new values (buddy env:set, buddy env:encrypt). Private key decrypts at runtime — keep out of source control on prod. Encrypted values look like encrypted:... or enc:... in this file and are auto-decrypted at boot by the env plugin in bunfig.toml preload. Leave both blank in dev; run buddy key:generate to materialize.

No reader in app/, routes/ or resources/: this one is the framework's.

DOTENV_PRIVATE_KEY

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

Ports

PORT

Default: 3000.

Read by app/Actions/Git/hooks.ts.

Development

DEBUG

Default: true.

No reader in app/, routes/ or resources/: this one is the framework's.

Git

GIT_HOOK_SECRET

Default: empty.

The shared secret the git hooks post back with. The hooks run on this machine and post to loopback, so this is a local trust boundary rather than a public one - but the endpoints answer 404 without it rather than accepting anything, because a default shared secret is a published shared secret.

Without it a push lands on disk and nothing is recorded about it: no closed issues, no cross references, no pushed_at. Generate one with openssl rand -hex 32, then run buddy git:hooks.

No reader in app/, routes/ or resources/: this one is the framework's.

GIT_HOOK_URL

Default: empty.

Where the hooks reach the application. Defaults to loopback on PORT, which is what you want: a push should not depend on the instance being able to resolve and reach its own public hostname from the inside.

Read by app/Actions/Git/hooks.ts.

GIT_WAL

Default: advisory, and the line is commented out.

The push write-ahead log: every push recorded before it is acknowledged, with a bundle of its objects in blob storage. That is continuous point-in-time backup on one box, and buddy git:restore is what reads it back.

off (the default), advisory, or required. required refuses a push that cannot be logged, which deliberately inverts the fail-open rule that governs branch protection - see config/git-wal.ts for why the two want opposite failure modes. Run advisory first.

No reader in app/, routes/ or resources/: this one is the framework's.

GIT_WAL_KEEP

Default: 500, and the line is commented out.

How many committed entries to keep behind the newest checkpoint.

No reader in app/, routes/ or resources/: this one is the framework's.

Database

DB_CONNECTION

Default: mysql.

The metadata database. postgres or mysql; both are supported, each has its own generated migration corpus, and buddy setup installs whichever one this names - postgresql.org@17.10 or mysql.com@9.2 - as a pantry service.

MySQL is the default from phase 17: it is what the sharded mode in phase 18 is built on, and the suite runs green against both engines in CI. Postgres stays supported for one release cycle and is then deprecated - an existing instance moves with buddy db:migrate-engine, which copies through the drivers and verifies both the row counts and a checksum of the values. See docs/self-hosting.md.

root with no password is what a pantry-managed local server initialises with, so it is not a placeholder. On Postgres, DB_PORT is 5432 and DB_USERNAME is postgres for the same reason. vitess is the third option, for an instance large enough to shard: vtgate speaks the MySQL wire protocol on 15306, so nothing but this line and the port changes. It is opt-in and nobody self-hosting a forge needs it - see docs/todo/17-database.md for what the keyspaces look like and what a sharded keyspace refuses.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts.

DB_HOST

Default: 127.0.0.1.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts.

DB_PORT

Default: 3306. Checked at boot, so a wrong value stops the instance with a sentence rather than failing quietly later.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts.

DB_DATABASE

Default: reviewos.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts, app/Commands/Doctor.ts.

DB_USERNAME

Default: root.

A pantry-managed MySQL initialises with --initialize-insecure, so root exists with no password. This value is also load-bearing for automatic database creation: pantry skips creating one entirely when DB_USERNAME is empty.

On Postgres it is postgres, for the same kind of reason: pantry initialises that cluster with initdb --auth-local=trust --auth-host=trust --username=postgres, so that is the only role there is.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts.

DB_PASSWORD

Default: empty.

Read by app/Commands/DbBackfillShardKey.ts, app/Commands/DbKeyspaces.ts, app/Commands/DbMigrateEngine.ts.

DATABASE_URL

Default: mysql://root@127.0.0.1:3306/reviewos.

Read natively by bun:sql as a fallback for connections that setConfig does not reach. Keep it in sync with the DB_* values above.

No reader in app/, routes/ or resources/: this one is the framework's.

Stripe

STRIPE_SECRET_KEY

Default: "".

No reader in app/, routes/ or resources/: this one is the framework's.

Apple

APPLE_CLIENT_ID

Default: empty.

Sign in with Apple. APPLE_CLIENT_ID is the Services ID, not the primary App ID. Store the downloaded .p8 contents with literal \n separators when setting APPLE_PRIVATE_KEY through buddy env:set.

No reader in app/, routes/ or resources/: this one is the framework's.

APPLE_TEAM_ID

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

APPLE_KEY_ID

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

APPLE_PRIVATE_KEY

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

APPLE_REDIRECT_URL

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS

AWS_ACCOUNT_ID

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_ACCESS_KEY_ID

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_SECRET_ACCESS_KEY

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_DEFAULT_REGION

Default: us-east-1.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_PROFILE

Default: stacks.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_BUCKET

Default: empty.

AWS S3 Storage

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_S3_PREFIX

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_ENDPOINT

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_URL

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

AWS_USE_PATH_STYLE_ENDPOINT

Default: false.

No reader in app/, routes/ or resources/: this one is the framework's.

Storage

FILESYSTEM_DISK

Default: local.

Filesystem / Storage

No reader in app/, routes/ or resources/: this one is the framework's.

FILESYSTEM_LOCAL_ROOT

Default: storage/app, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

FILESYSTEM_PUBLIC_ROOT

Default: public, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

Pages

PAGES_DOMAIN

Default: empty.

The driver. log writes what would have been sent to the log and sends nothing, which is the framework's default and is fine in development and a silent failure in production - a password reset nobody receives looks like a broken account. buddy instance:check warns about it when APP_ENV=production. Pages. The hostname published sites are served under, e.g. pages.example.com, so a site is reached at https://<owner>.pages.example.com/<repository>/.

Empty means Pages is off, and it is empty by default deliberately: a published site is somebody else's HTML and JavaScript, and the only thing separating it from every session on this instance is the origin. Serving it under this instance's own host would hand every repository owner a script tag on everybody's dashboard. One wildcard DNS record and one wildcard certificate is the price, and it is the whole boundary.

No reader in app/, routes/ or resources/: this one is the framework's.

PAGES_CUSTOM_DOMAINS

Default: true.

Whether a site may claim a domain of its own. Leave off unless the gateway in front of this instance can obtain a certificate for a name it does not own.

No reader in app/, routes/ or resources/: this one is the framework's.

Public

PUBLIC_DIFF_ENABLED

Default: false.

The public diff viewer: open any public GitHub pull request, commit or compare range here by swapping the hostname. Off by default, because it is a fetcher pointed at the internet running on the server that holds every repository on this instance - the host allowlist and the byte ceiling are in the code and not configurable, but turning it on is still a decision. See config/publicdiff.ts for where it is served and why.

No reader in app/, routes/ or resources/: this one is the framework's.

PUBLIC_DIFF_RATE

Default: 30.

Proxy requests per five minutes per address. Each one is an outbound fetch of somebody else's patch, so this is what keeps a shared instance from being used as a crawler.

No reader in app/, routes/ or resources/: this one is the framework's.

Email

MAIL_MAILER

Default: smtp.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_HOST

Default: 127.0.0.1. Checked at boot, so a wrong value stops the instance with a sentence rather than failing quietly later.

The local mail catcher: ./buddy mail:dev runs the mail server with delivery switched off, so every notification this instance sends is accepted, stored and readable at http://localhost:8025, and none of it leaves the machine. It is the same binary a deployed instance runs, so a digest that renders locally has been through the code that will handle it for real.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_PORT

Default: 1025.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_USERNAME

Default: null.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_PASSWORD

Default: null.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_ENCRYPTION

Default: null.

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_FROM_NAME

Default: "${APP_NAME}".

No reader in app/, routes/ or resources/: this one is the framework's.

MAIL_FROM_ADDRESS

Default: "no-reply@example.com".

The address notifications come from. A domain you control and that this instance is allowed to send for: a From on somebody else's domain fails SPF and lands in spam, which reads as the notifications being broken.

No reader in app/, routes/ or resources/: this one is the framework's.

The queue

QUEUE_DRIVER

Default: database.

Queue Configuration Drivers: sync (default, immediate execution), database, redis, sqs, memory

database is the deployment default, and not optional in production: the framework's fallback is sync, which runs every job inline in the request that dispatched it - so the whole push pipeline (issue closing, webhooks, notifications) would run inside the post-receive request, and a slow webhook would hold a git push open. Set it, and run a worker (see the queue section of docs/self-hosting.md).

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_PREFIX

Default: stacks:queue.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_CONCURRENCY

Default: 5.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_LOG_LEVEL

Default: info.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_WORKER_CONCURRENCY

Default: 5.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_FAILED_DRIVER

Default: database.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_RATE_LIMIT_ENABLED

Default: false, and the line is commented out.

Queue Rate Limiting (optional)

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_RATE_LIMIT_MAX

Default: 100, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_RATE_LIMIT_DURATION

Default: 1000, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_DLQ_ENABLED

Default: true, and the line is commented out.

Queue Dead Letter Queue (optional)

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_DLQ_MAX_RETRIES

Default: 3, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_HORIZONTAL_SCALING_ENABLED

Default: false, and the line is commented out.

Queue Horizontal Scaling (optional, for multi-instance deployments)

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_MAX_WORKERS

Default: 10, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_JOBS_PER_WORKER

Default: 10, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

QUEUE_METRICS_ENABLED

Default: false, and the line is commented out.

Queue Metrics (optional)

No reader in app/, routes/ or resources/: this one is the framework's.

Redis

REDIS_URL

Default: unset.

Redis Configuration (required when QUEUE_DRIVER=redis, CACHE_DRIVER=redis, or BROADCAST_REDIS_ENABLED=true). One connection, read by all three. The server pantry installs and manages is valkey, which speaks the same protocol.

No reader in app/, routes/ or resources/: this one is the framework's.

REDIS_HOST

Default: localhost, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REDIS_PORT

Default: 6379, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REDIS_PASSWORD

Default: unset.

No reader in app/, routes/ or resources/: this one is the framework's.

REDIS_DB

Default: 0, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

Cache

CACHE_DRIVER

Default: memory, and the line is commented out.

Cache. memory is correct for one process and only one process: presence on pull requests rides the cache, so a second app process needs redis here. See "Running more than one process" in docs/self-hosting.md.

No reader in app/, routes/ or resources/: this one is the framework's.

Realtime

BROADCAST_REDIS_ENABLED

Default: false, and the line is commented out.

Broadcast. The websocket server is per-process; a second app process needs the shared bus or live updates only reach readers on the process that happened to serve them.

No reader in app/, routes/ or resources/: this one is the framework's.

SQS

SQS_PREFIX

Default: unset.

SQS Configuration (required when QUEUE_DRIVER=sqs)

No reader in app/, routes/ or resources/: this one is the framework's.

SQS_SUFFIX

Default: unset.

No reader in app/, routes/ or resources/: this one is the framework's.

SEARCH_ENGINE_DRIVER

Default: typesense.

Typesense, started by ./buddy setup and declared in config/deps.ts. These named meilisearch until 2026-08-10, which is why a fresh checkout had no search node: deps.yaml is generated from config/deps.ts and what this file says, so the engine the config asks for was never the one installed.

The key is the one pantry's service runs with. It is a development default and an instance reachable from anywhere needs its own: a search node with a guessable key answers anybody's question about private repositories.

8208 rather than Typesense's default 8108, and it has to agree with the pantry start typesense --port 8208 in config/deps.ts - there is no third place deriving one from the other. The point is one instance per project: every project asking for 8108 means whichever starts second fails to bind, and its health check gets the first project's answer and reports success. Working since pantry 0.11.31; see the note in config/deps.ts for what had to be fixed upstream, twice, to make it true.

No reader in app/, routes/ or resources/: this one is the framework's.

TYPESENSE_HOST

Default: 127.0.0.1.

No reader in app/, routes/ or resources/: this one is the framework's.

TYPESENSE_PORT

Default: 8208.

No reader in app/, routes/ or resources/: this one is the framework's.

TYPESENSE_PROTOCOL

Default: http.

No reader in app/, routes/ or resources/: this one is the framework's.

TYPESENSE_API_KEY

Default: pantry-dev.

No reader in app/, routes/ or resources/: this one is the framework's.

Blob

BLOB_LOCAL_ROOT

Default: storage, and the line is commented out.

Blob storage: workflow artifacts, LFS objects, release assets, and the push bundles the write-ahead log writes. Local disk is the default and is a supported driver forever - an instance on one box needs none of the rest.

storage rather than a new directory on purpose: every one of those features already keeps its bytes under storage/<something>, so a key of artifacts/aa/bb/<digest> lands exactly where artifacts have always been. Pointing this somewhere new makes every file already on disk read as missing.

Read by app/Actions/Git/blobs.ts.

BLOB_S3_BUCKET

Default: unset.

Naming a bucket is what switches the driver to object storage. There is no separate BLOB_DRIVER on purpose: "use s3" without a bucket is a setting that tells you nothing until the first upload fails.

Read by app/Actions/Git/blobs.ts, app/Actions/Git/blobsS3.ts.

BLOB_S3_PREFIX

Default: unset.

Everything this instance writes goes under this prefix, so one bucket can hold several instances.

Read by app/Actions/Git/blobsS3.ts.

BLOB_S3_PROVIDER

Default: aws, and the line is commented out.

aws, hetzner, or backblaze. Credentials come from the provider's own variables - HETZNER_S3_* for Hetzner, the AWS chain for AWS.

Read by app/Actions/Git/blobsS3.ts.

Object

OBJECT_STORAGE_PROVIDER

Default: aws, and the line is commented out.

What ts-cloud itself calls the same setting. Read only when BLOB_S3_PROVIDER is unset, so an instance that already configures object storage for ts-cloud does not have to say it twice.

Read by app/Actions/Git/blobsS3.ts.

Ci

CI_LOG_MAX_BYTES

Default: 2097152, and the line is commented out.

What a job's output costs this instance: how much of it one job may store, and how long any of it is kept.

The ceiling is enforced on the way in, because a runner that streams forever is not stopped by a policy that runs tomorrow - it fills the disk tonight. Past it an append is accepted and discarded with one line saying so, which is honest; failing the upload would fail the job for a reason nobody can act on.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_LOG_RETENTION_DAYS

Default: 90, and the line is commented out.

Retention is off by default, and that is the decision rather than an omission: the first time anybody wants a build log is usually weeks after they stopped caring about the run. Setting it says the text is worth less than the disk - true on a busy instance, false on most. The job, its steps, their timings and the run's conclusion survive it; only the text goes.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_MAX_RUNNING_PER_REPOSITORY

Default: 20, and the line is commented out.

How much of the fleet one repository may hold at once.

Concurrency groups stop a workflow racing itself and queues decide which machines serve whom; neither stops a monorepository's push fanning out into eighty jobs while everybody else's one-job build waits behind all of them. Off by default, because on a single-team instance a ceiling only gets in the way - and the operator who needs one knows they need it.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_MAX_RUNNING_PER_OWNER

Default: 60, and the line is commented out.

The same for an owner, which is the limit that matters on an instance hosting several organizations: a per-repository ceiling does nothing against an owner with forty repositories.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_FAIR_QUEUEING

Default: off, and the line is commented out.

Fair queueing offers the repository holding fewer machines first. On by default: it costs one ordering pass, changes nothing when only one repository is pushing, and is the whole difference when four teams push at once. Set off for strict first-in, first-out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MODEL

Default: claude-opus-5, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_EFFORT

Default: high, and the line is commented out.

How hard it thinks. high by default rather than xhigh, because a repair is reviewed by a person before it lands and the marginal quality is not obviously worth the marginal spend on somebody else's bill.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_TOKENS

Default: 16000, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_LOG_LINES

Default: 200, and the line is commented out.

How much of the failing log the model sees, from the tail - which is where a failure's cause is. Bounded because a log is attacker-controlled in the ordinary case: it is whatever the repository's own test suite printed, and an unbounded one is both a bill and a prompt somebody else wrote.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_FILES

Default: 10, and the line is commented out.

The most files one repair may change. A blast radius limit rather than a cost one: a repair touching forty files is a refactor nobody asked for, and the reviewer who has to read it is who this protects.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_RUNNING

Default: 4, and the line is commented out.

How many repairs may run at once - per repository, per owner, and across the instance. On by default, which is the deliberate difference from the machine ceilings above: a machine ceiling that idles a runner wastes capacity somebody already paid for, while a repair holds a call to somebody else's API. Past its rate limit those calls are refused for everybody, including the repairs that mattered, and each one costs money. The failure mode with no ceiling is a monorepository whose push fans out into eighty failing jobs and eighty simultaneous model calls.

Zero means no limit, the same spelling the fleet ceilings use. A value that is not a number keeps the default here rather than falling back to unlimited - the opposite direction from CI_MAX_RUNNING_PER_REPOSITORY, because a typo should cost a slower repair queue rather than an unmetered one.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_RUNNING_PER_REPOSITORY

Default: 2, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_RUNNING_PER_OWNER

Default: 3, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_WAIT_SECONDS

Default: 30, and the line is commented out.

Being over a ceiling is a wait rather than a refusal: the repair goes back on the queue and asks again. These bound how long it does that before giving the attempt back - as a refusal, so a run is not charged for capacity the instance did not have. The defaults are ten minutes of asking.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_MAX_WAITS

Default: 20, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_STALE_MINUTES

Default: 60, and the line is commented out.

How long a started repair counts against those ceilings before it is presumed dead. Without a horizon, a repair whose process died holds a slot for ever - one crash at a time, until repair quietly stops happening and nothing says why.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_REPAIR_CALL_SECONDS

Default: 300, and the line is commented out.

How long one model call may take before the process running it is killed. Generous, because adaptive thinking on a hard failure is genuinely slow, and bounded because a call that never returns holds an attempt open - and that attempt is holding a slot against the ceilings above. A ceiling whose entries can never leave is not a ceiling.

No reader in app/, routes/ or resources/: this one is the framework's.

CI_STEP_MEMORY_MB

Default: 4096, and the line is commented out.

Ceilings a step cannot raise, applied before its first instruction.

Not isolation - a step still runs as ordinary processes on the host, and the sandbox that would change that is gated by docs/ci-security-review.md. What these stop is the ordinary accident: a loop that writes a forty-gigabyte file, a build that spins a core forever. Generous on purpose, because a limit that trips on ordinary work is one an operator removes.

Read by app/Actions/Runner/limits.ts.

CI_STEP_FILE_SIZE_MB

Default: 4096, and the line is commented out.

Read by app/Actions/Runner/limits.ts.

CI_STEP_PROCESSES

Default: 512, and the line is commented out.

Off by default and worth understanding before setting: both count something wider than one step. RLIMIT_NPROC is per user, so on a machine where the runner shares a user with anything else a generous number is already spent - set it only on a machine dedicated to the runner. CPU seconds are not wall time: a build using eight cores for two minutes has spent sixteen CPU minutes without being slow.

Read by app/Actions/Runner/limits.ts.

CI_STEP_CPU_SECONDS

Default: 3600, and the line is commented out.

Read by app/Actions/Runner/limits.ts.

Anthropic

ANTHROPIC_API_KEY

Default: unset.

What an automated repair runs on.

Two switches, and neither implies the other. repair_settings.enabled is a repository saying an agent may push branches there; the key below is the operator saying this instance can call a model at all. With no key, repair is refused with a reason somebody can read from the repository - not left as attempts that fail silently.

Whoever pays for the tokens picks the model, which is why it is named here rather than per repository. What a repository controls is how much may be spent on it, and that is already max_attempts and max_cost.

Read by app/Actions/Workflow/repairModelChild.ts.

ReviewOS

REVIEWOS_EXECUTION

Default: microvm, and the line is commented out.

How this runner executes a job.

host is the default and is what a runner told nothing has always done: steps run as ordinary processes, and an untrusted run - a fork's pull request - is refused, because a host runner is not a boundary.

microvm is a claim about the machine this runner is on: that it has KVM, a hypervisor, a kernel and an image. None of that can usefully be guessed, so it is asked for explicitly - and when a piece is missing the runner fails the job rather than running it somewhere weaker. A runner that quietly fell back to the host would be claiming an isolation it does not have.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_FIRECRACKER

Default: /usr/local/bin/firecracker, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_GUEST_KERNEL

Default: /var/lib/reviewos/vmlinux, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_GUEST_IMAGE

Default: /var/lib/reviewos/base.ext4, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_GUEST_IMAGE_DIGEST

Default: sha256:..., and the line is commented out.

Required, and about honesty rather than booting: a machine boots perfectly well from an image nobody named, and what it cannot then do is tell the run what executed it.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_SCRATCH

Default: /var/lib/reviewos/microvm, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_VCPUS

Default: 2, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_MEMORY_MIB

Default: 2048, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_DISK_MIB

Default: 2048, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_HOST_IP

Default: 172.20.0.1, and the line is commented out.

The two ends of the tap link - a /30, because it has exactly two things on it and a wider one is addresses a guest could try to reach.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_MICROVM_GUEST_IP

Default: 172.20.0.2, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_EGRESS_ALLOW

Default: registry.npmjs.org:443,proxy.golang.org:443, and the line is commented out.

What a job may connect to, comma separated, as host or host:port. Empty is the default and means nothing at all: usable for a great many workflows and genuinely unusable for the ones that install dependencies. Some destinations are refused however they are written - the cloud metadata endpoint, loopback, and the instance's own addresses below.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_INSTANCE_ADDRESSES

Default: 10.0.0.5,10.0.0.6, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

REVIEWOS_ACTOR_ID

Default: unset.

Read by app/Actions/Git/hooks.ts.

REVIEWOS_RUNNER_TOKEN

Default: unset.

The credential the local runner authenticates with.

Only for single-tenant installs running buddy runner:local, which executes this instance's jobs on this host with no isolation - steps run as the user who started it. Make one with buddy runner:local --register, which prints it once. Nothing runs unless somebody starts the runner, so leaving this unset is the same as having no execution plane, which is the default this instance ships with.

Read by app/Actions/Runner/standalone.ts, app/Commands/RunnerLocal.ts.

REVIEWOS_ACTION_STORE

Default: storage/actions, and the line is commented out.

Where mirrored actions are kept.

storage/actions by default. The mirrors of a busy instance are the one part of storage/ that grows without anybody adding anything, so this exists to put them on a disk with room. Nothing is mirrored at all until the action policy allows a host, which it does not by default.

Read by app/Actions/Actions/store.ts.

REVIEWOS_SECRET_STORES

Default: /etc/reviewos/secret-stores.json, and the line is commented out.

Where the external secret stores are described.

A JSON file naming the stores a secret may reference - a directory your platform mounted, or a Vault address and the file its token is in:

{ "mounted": { "kind": "file", "address": "/run/secrets" }, "prod": { "kind": "vault", "address": "https://vault.internal", "tokenFile": "/run/secrets/vault-token" } }

A secret set with a reference rather than a value is read from one of these when a job claims work, so this instance holds a path rather than a credential. Unset means no stores, and a reference to one fails the job that needed it by name. The stores are named here, by whoever runs the instance, because a reference a repository could point at any URL would be a request this server makes from inside your network on somebody else's say-so.

Read by app/Actions/Workflow/secretStore.ts.

REVIEWOS_CACHE_MAX_BYTES

Default: 10737418240, and the line is commented out.

How much of the dependency cache one repository may keep.

Ten gigabytes by default: a few snapshots of a large repository and many of an ordinary one. Past it, the least recently restored snapshots go first - an entry a hundred runs a day reach for should outlive one written this morning and never read again, and age from the write gets that backwards.

buddy ci:caches prints what the next sweep would remove before it removes anything, through the same function the sweep uses.

Read by app/Actions/Workflow/cacheCollect.ts.

REVIEWOS_CACHE_MAX_IDLE_DAYS

Default: 7, and the line is commented out.

How long a dependency snapshot nobody restores is kept.

Seven days by default: long enough that a branch somebody returns to on Monday still has its cache, short enough that a fork's entry from a merged pull request does not sit there for a quarter. Applied before the size limit above, because idle is its own rule rather than a tiebreak.

Read by app/Actions/Workflow/cacheCollect.ts.

Screenshots

SCREENSHOT_URL

Default: https://reviewos.org, and the line is commented out.

Where buddy screenshot points its browser. Defaults to APP_URL, which is right on the machine the instance runs on and wrong from a laptop taking pictures of staging - that is the whole reason it is separate.

Read by app/Actions/Screenshot/capture.ts.

Exposed to the browser

FRONTEND_APP_ENV

Default: "${APP_ENV}".

FRONTEND_* variables should not contain any sensitive information

No reader in app/, routes/ or resources/: this one is the framework's.

FRONTEND_APP_URL

Default: "${APP_URL}".

No reader in app/, routes/ or resources/: this one is the framework's.

FRONTEND_STRIPE_PUBLIC_KEY

Default: empty.

No reader in app/, routes/ or resources/: this one is the framework's.

Cloud dashboard

TS_CLOUD_UI_PASSWORD

Default: empty.

ts-cloud management dashboard (auto-deployed on every server). Served behind HTTP Basic auth when TS_CLOUD_UI_PASSWORD is set; if left empty the dashboard is served WITHOUT auth (recommended: set a password).

No reader in app/, routes/ or resources/: this one is the framework's.

TS_CLOUD_UI_USERNAME

Default: admin, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

TS_CLOUD_UI_DOMAIN

Default: dashboard.example.com, and the line is commented out.

No reader in app/, routes/ or resources/: this one is the framework's.

TS_CLOUD_UI_DISABLE

Default: unset.

No reader in app/, routes/ or resources/: this one is the framework's.

Webhooks

WEBHOOK_ALLOWED_HOSTS

Default: ci.internal:8080,10.0.0.5:9000, and the line is commented out.

Webhooks

Hosts a webhook may reach that the SSRF policy would otherwise refuse: loopback, link-local, and private ranges. Empty by default, and deliberately - a webhook URL is chosen by whoever configured the webhook, so the only person entitled to vouch for an internal destination is whoever runs the server.

Comma separated, and the port is part of the match, so naming one service does not open every port on that machine. A CI runner on the same LAN is the ordinary reason to set it:

Read by app/Actions/Webhook/ssrf.ts.

WEBHOOK_ALLOWED_HOSTS

Default: unset.

Read by app/Actions/Webhook/ssrf.ts.

Web push

VAPID_PUBLIC_KEY

Default: unset.

Web push

Generated per installation with buddy push:keys, never shipped. A key baked into the source would be shared by every self-hosted instance, so any one of them could send notifications claiming to be another, and one revocation would break all of them.

The public key is safe to expose - the browser needs it to subscribe. The private key never leaves the server. Replacing the pair invalidates every existing subscription, because a browser subscribes TO a public key.

VAPID_SUBJECT is mailto: or https:, and RFC 8292 requires it: it is how a push service reaches whoever runs this server, and the alternative to being reachable is being blocked.

Read by app/Actions/Notification/vapid.ts.

VAPID_PRIVATE_KEY

Default: unset.

Read by app/Actions/Notification/vapid.ts, app/Commands/PushKeys.ts.

VAPID_SUBJECT

Default: mailto:ops@example.com, and the line is commented out.

Read by app/Actions/Notification/vapid.ts.

Single sign-on

SSO_ISSUER

Default: unset.

Single sign-on (OIDC)

Off unless SSO_ISSUER, SSO_CLIENT_ID and SSO_CLIENT_SECRET are all set: a half-configured provider would be a sign-in button that fails after the redirect, which is worse than no button. The issuer is the base URL whose /.well-known/openid-configuration describes the provider.

SSO_REDIRECT_URI defaults to <APP_URL>/api/auth/sso, which is what the provider must be told to allow. Set it only when this instance sits behind something that rewrites the path.

SSO_SCOPES is space or comma separated and adds to the defaults. Ask for the groups claim here when the provider needs to be told to send it.

SSO_TEAM_ORGANIZATION names the one organization whose teams follow the provider's groups. Deliberately a single named organization: group mapping that could add somebody to any organization on the instance is a provider misconfiguration away from being a privilege escalation.

Read by app/Actions/Auth/SsoAction.ts.

SSO_CLIENT_ID

Default: unset.

Read by app/Actions/Auth/SsoAction.ts.

SSO_CLIENT_SECRET

Default: unset.

Read by app/Actions/Auth/SsoAction.ts.

SSO_REDIRECT_URI

Default: unset.

Read by app/Actions/Auth/SsoAction.ts.

SSO_SCOPES

Default: unset.

Read by app/Actions/Auth/SsoAction.ts.

SSO_TEAM_ORGANIZATION

Default: unset.

Read by app/Actions/Auth/provision.ts.

Error reporting

ERROR_REPORTING_URL

Default: unset. Checked at boot, so a wrong value stops the instance with a sentence rather than failing quietly later.

Error reporting

Off unless ERROR_REPORTING_URL is set, and that default is a product decision rather than laziness: self-hosted software that phones home by default is software people stop trusting. A POST to a webhook rather than a vendor SDK, so this is not a dependency or a bet on which service you use.

ERROR_REPORTING_WINDOW_MS is how long the same error stays deduplicated, so a failing job cannot turn into ten thousand identical messages.

Read by app/Ops/reporting.ts.

ERROR_REPORTING_TOKEN

Default: unset.

Read by app/Ops/reporting.ts.

ERROR_REPORTING_TIMEOUT_MS

Default: 3000, and the line is commented out.

Read by app/Ops/reporting.ts.

ERROR_REPORTING_WINDOW_MS

Default: 300000, and the line is commented out.

Read by app/Ops/reporting.ts.

Metrics

METRICS_TOKEN

Default: unset.

The bearer token a metrics scraper presents at /api/metrics.

Without it the endpoint needs an instance administrator's session, which a Prometheus scrape config holds far less comfortably than a token. Leaving it unset does not open the endpoint; it only means a scraper cannot reach it.

Read by app/Actions/Ops/MetricsAction.ts.

SSH

SSH_PORT

Default: 2222, and the line is commented out.

Serving git over SSH: the port buddy git:ssh listens on. 2222 rather than 22 because binding a privileged port needs root, and running the git server as root to save people six characters in a clone URL is not a trade worth making.

Read by app/Actions/Repo/cloneUrl.ts, app/Commands/GitSsh.ts.

SSH_CLONE_HOST

Default: unset.

Serving git over SSH to whoever is looking at a repository page.

The clone box offers an SSH URL only when one of these is set, and that is deliberate: a URL that cannot connect is worse than one fewer, because somebody copies it, waits for a timeout, and concludes the forge is broken rather than that a feature is off. Setting SSH_PORT is enough - the host falls back to the one the page was served from.

SSH_CLONE_USER is the account clients connect as. The user name is ignored on the way in; the key is the identity. git unless you have a reason.

Read by app/Actions/Repo/cloneUrl.ts.

SSH_CLONE_USER

Default: git, and the line is commented out.

Read by app/Actions/Repo/cloneUrl.ts.

Stopping

SHUTDOWN_LEAD_MS

Default: 5000, and the line is commented out.

Stopping without losing work.

SHUTDOWN_LEAD_MS is how long to keep answering after reporting unhealthy, so the load balancer has noticed before the door closes. SHUTDOWN_DRAIN_MS is how long in-flight work then has to finish. The total is deliberately under the 30 seconds Docker, Kubernetes and systemd wait before SIGKILL: a process that exits on its own terms has finished its writes, and one that is killed has not.

Read by app/Ops/shutdown.ts.

SHUTDOWN_DRAIN_MS

Default: 25000, and the line is commented out.

Read by app/Ops/shutdown.ts.

The diff viewer

DIFF_INLINE_ROWS_BUDGET

Default: 8388608, and the line is commented out.

How much of a diff is rendered into the manifest before rows are fetched on demand, in bytes. Bigger is a faster diff on a fast link and a slower first byte on a phone. The benchmark harness pins it so the on-demand path can be measured at all.

Read by app/Actions/Pull/manifest.ts.

Ssr

SSR_DIFF_BYTE_LIMIT

Default: 8388608, and the line is commented out.

How much patch text the server-rendered pull request page will hold, in bytes. The same default as the budget above and for the same reason: past it, a diff is not something a whole-page render serves anybody with, and the virtualized review screen is where the reader belongs - the page says so in a banner rather than rendering part of a change as though it were all of it. Lower it on a small box; a value that is not a number falls back to the default rather than to zero, which would truncate every diff on the site.

Read by app/Actions/Pull/load.ts.

GitHub

GITHUB_TOKEN

Default: unset.

A token for importing and mirroring from GitHub. Read when a mirror has no stored credential of its own, and by buddy import:github. A personal access token with read access to the repositories you intend to import.

Read by app/Actions/Mirror/credentials.ts, app/Actions/Runner/ClaimJobAction.ts, app/Actions/Runner/localExecutor.ts, app/Actions/Workflow/secrets.ts, app/Commands/ImportGitHub.ts, app/Jobs/ImportRepositoryJob.ts.

GITHUB_CLONE_BASE

Default: https://github.com, and the line is commented out.

Where imports clone from. Only ever changed by the tests, which point it at a local repository so a suite does not depend on being able to reach github.com.

Read by app/Jobs/ImportRepositoryJob.ts.

Set by git, not by you

Git hands these to a hook on every push, per repository. A value for one of them in .env would be wrong for every repository but the one it was copied from, and the failure is a push writing objects into somebody else's directory.

  • GIT_ALTERNATE_OBJECT_DIRECTORIES
  • GIT_DIR
  • GIT_OBJECT_DIRECTORY
  • GIT_PUSH_OPTION_COUNT
  • GIT_TERMINAL_PROMPT
  • HOME
  • LANG
  • PATH

Set where the work runs, not on the instance

Two programs read their configuration somewhere else: the runner, compiled into a binary and copied to a machine that is not this one, and buddy tests:report, run by whatever CI a repository already uses. These are set there rather than in this instance's .env. Each has a command-line flag too, and the flag wins.

  • BUILDKITE_BRANCH, read by app/Commands/ReportTests.ts
  • BUILDKITE_BUILD_ID, read by app/Commands/ReportTests.ts
  • BUILDKITE_COMMIT, read by app/Commands/ReportTests.ts
  • CI_COMMIT_REF_NAME, read by app/Commands/ReportTests.ts
  • CI_COMMIT_SHA, read by app/Commands/ReportTests.ts
  • CI_JOB_ID, read by app/Commands/ReportTests.ts
  • GITHUB_REF_NAME, read by app/Commands/ReportTests.ts
  • GITHUB_RUN_ATTEMPT, read by app/Commands/ReportTests.ts
  • GITHUB_RUN_ID, read by app/Commands/ReportTests.ts
  • GITHUB_SHA, read by app/Commands/ReportTests.ts
  • REVIEWOS_ACTION_DEFAULT_HOST, read by app/Actions/Runner/standalone.ts
  • REVIEWOS_ACTION_HOSTS, read by app/Actions/Runner/standalone.ts
  • REVIEWOS_ACTION_PINNED, read by app/Actions/Runner/standalone.ts
  • REVIEWOS_ALLOW_CONTAINERS, read by app/Actions/Runner/standalone.ts
  • REVIEWOS_CONTAINER_RUNTIME, read by app/Actions/Runner/container.ts
  • REVIEWOS_REF_NAME, read by app/Commands/ReportTests.ts
  • REVIEWOS_REGISTRATION_TOKEN, read by app/Actions/Runner/standalone.ts
  • REVIEWOS_REPOSITORY, read by app/Commands/Ci.ts, app/Commands/ReportTests.ts
  • REVIEWOS_SHA, read by app/Commands/ReportTests.ts
  • REVIEWOS_TOKEN, read by app/Commands/Ci.ts, app/Commands/ReportTests.ts
  • REVIEWOS_URL, read by app/Actions/Runner/standalone.ts, app/Commands/Ci.ts, app/Commands/ReportTests.ts
  • REVIEWOS_WORKSPACE, read by app/Actions/Runner/standalone.ts