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.
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
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_DIRECTORIESGIT_DIRGIT_OBJECT_DIRECTORYGIT_PUSH_OPTION_COUNTGIT_TERMINAL_PROMPTHOMELANGPATH
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 byapp/Commands/ReportTests.tsBUILDKITE_BUILD_ID, read byapp/Commands/ReportTests.tsBUILDKITE_COMMIT, read byapp/Commands/ReportTests.tsCI_COMMIT_REF_NAME, read byapp/Commands/ReportTests.tsCI_COMMIT_SHA, read byapp/Commands/ReportTests.tsCI_JOB_ID, read byapp/Commands/ReportTests.tsGITHUB_REF_NAME, read byapp/Commands/ReportTests.tsGITHUB_RUN_ATTEMPT, read byapp/Commands/ReportTests.tsGITHUB_RUN_ID, read byapp/Commands/ReportTests.tsGITHUB_SHA, read byapp/Commands/ReportTests.tsREVIEWOS_ACTION_DEFAULT_HOST, read byapp/Actions/Runner/standalone.tsREVIEWOS_ACTION_HOSTS, read byapp/Actions/Runner/standalone.tsREVIEWOS_ACTION_PINNED, read byapp/Actions/Runner/standalone.tsREVIEWOS_ALLOW_CONTAINERS, read byapp/Actions/Runner/standalone.tsREVIEWOS_CONTAINER_RUNTIME, read byapp/Actions/Runner/container.tsREVIEWOS_REF_NAME, read byapp/Commands/ReportTests.tsREVIEWOS_REGISTRATION_TOKEN, read byapp/Actions/Runner/standalone.tsREVIEWOS_REPOSITORY, read byapp/Commands/Ci.ts,app/Commands/ReportTests.tsREVIEWOS_SHA, read byapp/Commands/ReportTests.tsREVIEWOS_TOKEN, read byapp/Commands/Ci.ts,app/Commands/ReportTests.tsREVIEWOS_URL, read byapp/Actions/Runner/standalone.ts,app/Commands/Ci.ts,app/Commands/ReportTests.tsREVIEWOS_WORKSPACE, read byapp/Actions/Runner/standalone.ts