Repository navigation
Expand file tree
/
Copy path.env.example
More file actions
759 lines (721 loc) · 42.5 KB
/
Copy path.env.example
File metadata and controls
759 lines (721 loc) · 42.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
# bex environment — the one checked-in, value-less mirror of .env. Copy it, fill
# it in, never commit the filled copy (.env is gitignored — never commit secrets).
#
# One file, two uses:
# • Local runtime: cp .env.example .env then fill in for a local / self-hosted bex.
# • CI secrets: cp .env.example .env, fill in, then `bash scripts/gh-secrets.sh`
# to push them into this repo's GitHub Actions secrets — the
# workflows (infra.yml / app-cluster.yml / deploy.yml) read them
# via ${{ secrets.* }}. Or set them by hand: GitHub → repo
# Settings → Secrets and variables → Actions.
#
# The example values below are non-secret illustrations (endpoints/paths); replace
# them with your own. Secret keys are left blank — fill them in your local .env only.
# --- Hetzner Cloud API token (Terraform provisions infra) ---
HCLOUD_TOKEN=
# --- onbex.co wildcard fallback TLS (production deploy only) ---
# Paths to an externally issued full-chain certificate and its unencrypted
# private key. bex never calls the DNS provider: scripts/gh-secrets.sh uploads
# the file contents into the protected production-deploy environment, and
# deploy.yml validates/installs traefik/onbex-default-wildcard-tls over stdin.
# Rotate the files, re-run scripts/gh-secrets.sh, then dispatch deploy.yml.
BEX_ONBEX_TLS_CERT_FILE=
BEX_ONBEX_TLS_KEY_FILE=
# --- SSH key for the infra node (give the FILE PATHS, not the key material) ---
# Create once: ssh-keygen -t ed25519 -f ~/.ssh/bex -C bex
BEX_SSH_PUBLIC_KEY_FILE=$HOME/.ssh/bex.pub
BEX_SSH_PRIVATE_KEY_FILE=$HOME/.ssh/bex
# --- Control-plane SSH host key (w1/m66 F7, docs/ADR019-infra-credentials.md) ---
# The AUTHORITATIVE known_hosts entries for the app cluster's control-plane
# nodes. scripts/fetch-app-kubeconfig.sh reads /etc/kubernetes/admin.conf (full
# cluster-admin) over SSH; with this set it authenticates the server and fails
# closed on an unknown or changed host key instead of trusting it on first use.
# Unset => trust-on-first-use, with a notice (the pre-m66 behavior).
#
# Capture out of band, from a host you already trust, and eyeball the fingerprint
# against the node's console before committing it to the secret:
# ssh-keyscan -t ed25519 <cp-ip> # repeat per control-plane node
# gh-secrets.sh pushes the value; the workflows materialize it to a file and set
# BEX_SSH_KNOWN_HOSTS_FILE. Multi-line value: keep the entries newline-separated.
BEX_SSH_KNOWN_HOSTS=
# Local override: point the scripts at an existing known-hosts file instead.
BEX_SSH_KNOWN_HOSTS_FILE=
# --- App SSH gateway (docs/ADR035-ssh.md) ---
# Public gateway hostname advertised to Render-compatible clients. Production
# must route this host's TCP/22 to the gateway before enabling it.
BEX_SSH_HOST=
# Stable gateway host private-key FILE PATH (not key material). Create once:
# ssh-keygen -t ed25519 -N '' -f ~/.ssh/bex_gateway_host -C bex-ssh-gateway
# scripts/ssh-host-key-secret.sh installs it out of band as a Kubernetes Secret.
BEX_SSH_HOST_KEY_FILE=$HOME/.ssh/bex_gateway_host
# Gateway listeners. Blank uses :2222 for SSH and :9090 for internal
# health/Prometheus.
BEX_SSH_ADDR=
BEX_SSH_METRICS_ADDR=
# Path mounted inside the gateway process. The production Deployment sets this
# to the host-key Secret mount; local runs must set it explicitly.
BEX_SSH_HOST_KEY_PATH=
# Gateway resource bounds. Blank uses 10s / 4h / 100 / 5.
BEX_SSH_HANDSHAKE_TIMEOUT=
BEX_SSH_SESSION_TIMEOUT=
BEX_SSH_MAX_SESSIONS=
BEX_SSH_MAX_SESSIONS_PER_IDENTITY=
# Pre-authentication connection cap: how many SSH handshakes may be in flight at
# once before the post-handshake session limiter applies. Bounds an anonymous
# connection flood's goroutine/descriptor/DB-lookup cost. Blank uses 256.
BEX_SSH_MAX_PREAUTH_CONNS=
# Per-source share of that pre-auth pool (round-11 #2): how many in-flight
# handshakes ONE resolved client address may hold, so a single silent source
# cannot park every global slot for the handshake deadline. Blank uses 32;
# negative restores global-only admission.
BEX_SSH_MAX_PREAUTH_CONNS_PER_SOURCE=
# Trusted immediate peer (Traefik's pod network) allowed to assert a PROXY
# protocol v1/v2 original-client address on the ssh entrypoint's forwarded
# connection, so ssh_sessions.remote_address records the real client instead
# of Traefik's own pod IP (w4/029.md #10). Blank disables PROXY-header trust.
# SECURITY: keep this to the immediate-peer address only (the edge/LB, e.g. a
# single /32). Whoever this trusts can assert an arbitrary client IP; widening
# it to a shared range (e.g. the tenant pod CIDR) lets a tenant workload spoof
# its source address and defeat any IP-allowlist that keys on it.
BEX_SSH_PROXY_PROTOCOL_TRUSTED_CIDRS=
# "Open in Zed" per-connection channel cap for agent-session sandbox targets
# (w2/m65, docs/ADR054-open-in-zed.md D3). Blank uses 16; 0 disables the
# multi-channel exception (single-channel for sandbox targets too).
BEX_SSH_MAX_CHANNELS_PER_CONN=
# Exec-stream (channel) caps (round-8 #7): the session caps above bound
# transports, but one multiplexed connection holds many pods/exec streams.
# Every accepted session channel claims a slot here. Blank uses 512 / 32.
BEX_SSH_MAX_CHANNELS=
BEX_SSH_MAX_CHANNELS_PER_IDENTITY=
# Live-stream revalidation cadence (round-9 #6): every established gateway
# stream (native-SSH exec, web shell, agent attach) re-runs its fresh
# authorization on this interval, so a revocation ends LIVE streams too, not
# just the next admission. Go duration; blank uses 1m; negative disables
# (admission-only, the pre-round-9 behavior).
BEX_SSH_REVALIDATE_INTERVAL=
# Agent Git smart-HTTP proxy bounds (round-9 #4): concurrent in-flight proxy
# requests, global and per source sandbox Pod, acquired before the Pod lookup
# and credential mint. Blank uses 64 / 4. The listener's whole-request read
# deadline (BEX_AGENT_GIT_READ_TIMEOUT, blank 10m) bounds a dripped body's
# duration.
BEX_AGENT_GIT_MAX_CONNS=
BEX_AGENT_GIT_MAX_CONNS_PER_POD=
BEX_AGENT_GIT_READ_TIMEOUT=
# Total Git smart-HTTP exchange lifetime and cumulative process-local request
# budgets. Blank uses 10m / 1000 per session / 5000 per workspace; 0 disables
# either request-count dimension.
BEX_AGENT_GIT_MAX_DURATION=
BEX_AGENT_GIT_MAX_REQUESTS_PER_SESSION=
BEX_AGENT_GIT_MAX_REQUESTS_PER_WORKSPACE=
# Browser Web Shell (w2/m55, docs/ADR035-ssh.md § Browser Web Shell). The HMAC
# key is shared between bex-api and the gateway; set the SAME value in both.
# Blank disables the in-dashboard terminal (native ssh still works). The gateway
# WebSocket listen address defaults to :8080 (path /shell); BEX_SHELL_WS_URL is
# the browser-reachable origin bex-api hands the terminal (e.g. wss://ssh.bex.co/shell).
BEX_SHELL_TICKET_SECRET=
BEX_SHELL_WS_URL=
BEX_SHELL_WS_ADDR=
# Cloud coding-agent session attach origin (ADR047 D3 / w3/m39), for example
# the browser-reachable origin of the agent-session conversation stream. Per
# ADR047 D9 the endpoint publishes under the PRIMARY API origin
# (https://api.bex.co/v1/agent-sessions), which the edge path-routes to the
# gateway process (ingressroute-agent-attach.yaml) — one origin, no CORS. A
# dedicated origin (e.g. wss://attach.bex.co) is the fallback. Agent-session
# tickets reuse BEX_SHELL_TICKET_SECRET's HMAC + shared-DB nonce trust design but
# carry their own subject/session/sandbox-pod/workspace claims. Blank makes
# session create/resume/steer/attach-ticket return 503; list/get remain available
# when their control plane + OpenSandbox dependencies are configured.
BEX_AGENT_SESSION_GATEWAY_URL=
# Platform-registered sandbox image containing the session driver, ACP agent,
# git credential helper, and language toolchains. Production pins the dedicated
# ghcr.io/bex-co/bex-agent-sandbox image by digest.
BEX_AGENT_SESSION_IMAGE=
# ADR059 D2 Active-tier idle grace (w2/m67): a finished agent-session sandbox
# stays alive until idle > this, where idle = now − max(last turn end, last
# editor SSH disconnect); an open Open-in-Zed session pins it. Go duration,
# default 30m; 0 = reap as soon as no editor is connected (ADR054 D6 behavior).
BEX_AGENT_SANDBOX_IDLE_TTL=
# w5/m80 t002: wall-clock bound on one agent turn inside the sandbox, injected as
# BEX_AGENT_TURN_TIMEOUT_MS. A hung model call or runaway tool loop converges to
# failed at this bound instead of the driver's 4h fallback. Go duration, default
# 30m; a 0/invalid value falls back to 30m (the bound is never disabled).
BEX_AGENT_TURN_TIMEOUT=
# ADR059 D6 per-workspace concurrent live-sandbox cap (w2/m67): max agent-session
# sandboxes in a live phase one workspace may hold; create/steer/resume beyond it
# is refused with AGENT_SESSION_LIVE_LIMIT. Default 5; 0 = uncapped.
BEX_AGENT_MAX_LIVE_SANDBOXES_PER_WORKSPACE=
# Per-workspace env-group quota (round-11 #3; default 100, 0 disables): bounds
# one tenant's share of the shared env-group index every list sweep walks;
# beyond it create is refused with ENV_GROUP_LIMIT.
BEX_MAX_ENV_GROUPS_PER_WORKSPACE=
# Per-workspace cap on durable Blueprint projects+environments (w8/m20; default 1000, 0 disables)
BEX_MAX_BLUEPRINT_GROUPINGS=
# Per-workspace GitHub-connection quota (ADR075 §2; default 10, 0 disables): caps
# how many GitHub App installations one workspace may connect; beyond it connect
# is refused with GIT_CONNECTION_LIMIT.
BEX_MAX_GIT_CONNECTIONS_PER_WORKSPACE=
# Its N:N mirror (ADR078 §2; default 10, 0 disables): how many workspaces ONE
# GitHub App installation may serve — the same GitHub account can back several
# workspaces, each binding separately proved — and therefore how wide a single
# push delivery fans out (§4a). Beyond it, binding is refused with
# GIT_INSTALLATION_WORKSPACE_LIMIT.
BEX_MAX_WORKSPACES_PER_GIT_INSTALLATION=
# Per-workspace registry-credential quota (default 50, 0 disables); beyond it
# create is refused with REGISTRY_CREDENTIAL_LIMIT.
BEX_MAX_REGISTRY_CREDS_PER_WORKSPACE=
# Custom-domain cardinality quotas (codex-security round 18; defaults 100 per
# service — the routes/headers scale — and 500 per workspace; 0 disables):
# every verified host fans out into an Ingress rule + cert-manager TLS entry +
# host-cache entries, so beyond either cap the claim is refused with
# CUSTOM_DOMAIN_LIMIT.
BEX_MAX_CUSTOM_DOMAINS_PER_SERVICE=
BEX_MAX_CUSTOM_DOMAINS_PER_WORKSPACE=
# ADR082 D5 persistent-disk snapshots (w1/m87). Hetzner has no volume snapshots
# at any level, so each disk-bearing App gets a nightly CronJob that streams its
# volume (tar | gzip | age, one pass) into a DEDICATED bucket — NEVER
# bex-tfstate; production name bex-disk-snapshots — with 7-day retention and a
# purge Job on detach. Provision with scripts/disk-snapshot-secret.sh.
#
# TWO identities on purpose: the operator writes and deletes; bex-api only
# LISTS (the READ_ pair), so it can never write or delete a tenant's backups and
# never holds the age key at all.
#
# The age pair is DEDICATED to disks, not ADR050's AGE_BACKUP_* platform key: a
# restore must decrypt inside the cluster, and ADR050 exists to keep the
# platform key out of it. Only the PUBLIC half belongs here — the private half
# lives solely in the bex-disk-snapshot-age Secret.
#
# FAIL-CLOSED: endpoint + bucket + S3 credential + age public key must ALL be
# set or NO snapshot is taken. A disk snapshot is a full copy of a tenant
# filesystem leaving the cluster, so bex takes none rather than an unencrypted
# one. Any unset => snapshots off, disks otherwise unaffected.
BEX_DISK_SNAPSHOT_ENDPOINT=
BEX_DISK_SNAPSHOT_BUCKET=
BEX_DISK_SNAPSHOT_REGION=
BEX_DISK_SNAPSHOT_PREFIX=
BEX_DISK_SNAPSHOT_ACCESS_KEY=
BEX_DISK_SNAPSHOT_SECRET_KEY=
BEX_DISK_SNAPSHOT_READ_ACCESS_KEY=
BEX_DISK_SNAPSHOT_READ_SECRET_KEY=
BEX_DISK_SNAPSHOT_AGE_PUBLIC_KEY=
# ADR059 D3 agent-session hibernation object store (w2/m68, armed w2/m77):
# dedicated SSE-enabled bucket (NEVER bex-tfstate; production name
# bex-agent-snapshots) + per-workspace prefix agent-snapshots/<ws>/… + the
# durable credentials bex-api holds to mint short-lived presigned PUT/GET URLs.
# Provision with scripts/agent-snapshot-secret.sh. All of endpoint+bucket+access
# +secret set => Completer hibernates idle finished sessions; any unset =>
# Hibernated tier off (reclaim = Terminate); a partial set fails bex-api startup.
BEX_AGENT_SNAPSHOT_S3_ENDPOINT=
BEX_AGENT_SNAPSHOT_S3_BUCKET=
BEX_AGENT_SNAPSHOT_S3_REGION=
BEX_AGENT_SNAPSHOT_S3_PREFIX=
BEX_AGENT_SNAPSHOT_S3_ACCESS_KEY=
BEX_AGENT_SNAPSHOT_S3_SECRET_KEY=
# ADR059 D5 hibernation retention window before an unpinned snapshot+row is
# deleted (Go duration, default 168h/7d; doubled when the git tree was dirty at
# hibernation), and the per-workspace pinned (never-expire) session quota (a pin
# beyond it is refused with AGENT_SESSION_PIN_LIMIT; default 10, 0 = uncapped).
BEX_AGENT_SNAPSHOT_RETENTION=
BEX_AGENT_MAX_PINNED_SANDBOXES_PER_WORKSPACE=
# `render ea sandbox exec` (w3/m33): the HMAC key shared between bex-api and the
# isolated SSH gateway. bex-api authorizes can_operate + signs a per-exec ticket;
# the gateway (which alone holds pods/exec) verifies it, runs the command in the
# sandbox pod, and streams stdout/stderr as SSE. Set the SAME value in both; blank
# leaves the exec verb 503 (create/list/stop unaffected). BEX_SANDBOX_EXEC_URL is
# the gateway's internal exec endpoint bex-api reverse-proxies to;
# BEX_SANDBOX_EXEC_ADDR is the gateway's internal listen address (default :8081).
BEX_SANDBOX_EXEC_SECRET=
BEX_SANDBOX_EXEC_URL=
BEX_SANDBOX_EXEC_ADDR=
# ADR047 D2 agent-session Git credential broker. The gateway listener defaults
# to :8082 and HMAC-proxies to bex-api's internal :8091 mint route. It reuses the
# domain-separated BEX_SANDBOX_EXEC_SECRET; unset secret disables both paths.
BEX_AGENT_CREDENTIAL_ADDR=
BEX_AGENT_CREDENTIAL_API_URL=
# ADR062 + security round 10 agent-session model-credential proxy. Set
# BEX_AGENT_MODEL_PROXY_URL (bex-api) to the internal gateway origin (e.g.
# http://bex-ssh-gateway.bex-system.svc.cluster.local:8084) to keep the BYO model
# key out of the sandbox: the sandbox gets a placeholder + this per-session base
# URL, and the gateway injects the real key on the vendor hop. The gateway's model
# listener defaults to :8084 and HMAC-mints from bex-api's internal :8091 (reusing
# BEX_SANDBOX_EXEC_SECRET). Unset disables create/steer/rehydrate; there is no
# direct-key fallback. The listener admits only registered inference operations,
# caps request/response bytes, and re-mints on every exchange. Concurrency is
# global/per source pod (defaults 32/2); read and total lifetimes default 2m/2h.
BEX_AGENT_MODEL_PROXY_URL=
BEX_AGENT_MODEL_PROXY_ADDR=
BEX_AGENT_MODEL_CREDENTIAL_API_URL=
BEX_AGENT_MODEL_MAX_CONNS=
BEX_AGENT_MODEL_MAX_CONNS_PER_POD=
BEX_AGENT_MODEL_READ_TIMEOUT=
BEX_AGENT_MODEL_MAX_DURATION=
# Security round 13 cumulative exchange budgets (request counts, process-local):
# every per-exchange bound resets on completion, so without these a live
# sandbox's tenant code could loop billable inference for the session's whole
# lifetime. Defaults 1000 per session / 5000 per workspace; 0 disables a
# dimension.
BEX_AGENT_MODEL_MAX_REQUESTS_PER_SESSION=
BEX_AGENT_MODEL_MAX_REQUESTS_PER_WORKSPACE=
# LOCAL-DEV ONLY: the agent-session model key (docs/runbooks/agent-sessions-local-dev.md).
# In PRODUCTION the BYO model key is per-workspace in OpenBao (ADR047 D7 / ADR062),
# NOT a platform env var — the gateway model proxy injects it on the vendor hop and
# the sandbox only ever sees a placeholder. But a local run has no per-tenant
# provisioning UI, so the dev tooling reads this one key and seeds it into the local
# OpenBao path the mint reads (tenants/data/<default>/agent-sessions/<ws>/model-key,
# field BEX_AGENT_MODEL_API_KEY), which lets the in-sandbox agent (claude-code-acp)
# actually call the model. LEAVE BLANK in any real deployment.
ANTHROPIC_API_KEY=
# ADR047 D9 agent-session conversation transport (w3/m43). The gateway's
# browser-facing SSE listen address (default :8083; the edge terminates TLS and
# path-routes api.bex.co/v1/agent-sessions/{id}/stream here) and the in-sandbox
# driver stream port the gateway dials directly (default 8787, must match the
# driver's BEX_AGENT_LISTEN_PORT). The listener starts only when the shared
# BEX_SHELL_TICKET_SECRET is set; the gateway verifies the agent-session ticket,
# replays the durable transcript, then splices + tees the live driver stream.
BEX_AGENT_ATTACH_ADDR=
BEX_AGENT_SESSION_DRIVER_PORT=
# Optional override of the in-cluster gateway git-credential broker URL that
# bex-api injects into each agent sandbox (ADR047 D2); the sandbox's
# git-credential-bex helper POSTs here to mint a scoped GitHub token for the
# setup-phase clone/push. Unset => the default
# http://bex-ssh-gateway.bex-system.svc:8082/session-credential.
BEX_AGENT_CREDENTIAL_URL=
# Control-plane internal API bearer (w1/m53). REQUIRED when BEX_CP_DB_URI is set:
# the :8091 tenant API grants workspace-admin and cross-tenant writes, so bex-api
# refuses to start with this empty (BEX_CP_INSECURE=1 overrides in local dev only).
BEX_CP_TOKEN=
# Control-plane instance identity (w6/m39, docs/ADR043 D9). Stamped on every
# tenant namespace AND every projected App CR this control plane owns, and the
# scope both of its cluster-scoped prunes delete within. Without it, every
# bex-api holding a BEX_CP_DB_URI deletes every managed object absent from ITS
# OWN database — which is how two `dev-N` harnesses sharing the CAPD mock
# cluster deleted each other's tenants. Set it per dev harness (e.g. dev-6);
# unset => "production", which also still reclaims pre-m39 objects carrying no
# identity label. Must be a valid Kubernetes label value or startup fails.
BEX_CP_IDENTITY=
# Authorization fail-closed override (w1/m65 F16). When BEX_CP_DB_URI is set but
# BEX_OPENFGA_URL is unset, bex-api REFUSES to start: authorization would be
# fail-open (every workspace member admin-equivalent, and explicit-workspace
# verbs bypass membership isolation, so cross-tenant isolation would not hold).
# Set to 1 to override in a single-member workspace / local dev only.
BEX_ALLOW_INSECURE_AUTHZ=
# Hosted agent sandboxes (pillar 5, ADR042/w3/m32). Set BEX_OPENSANDBOX_URL =>
# bex-api serves the Render-compatible /v1/sandboxes* surface over the
# OpenSandbox lifecycle client (render ea sandbox create/list/stop). Unset =>
# the sandbox verbs 503 (byte-identical). BEX_SANDBOX_IMAGE overrides the base
# template image. (BEX_OPENSANDBOX_URL is also read by the operator's opensandbox
# runtime.)
BEX_OPENSANDBOX_URL=
BEX_SANDBOX_IMAGE=
# --- Terraform remote state: Hetzner Object Storage (S3-compatible) ---
TF_STATE_BUCKET=bex-tfstate
TF_STATE_ENDPOINT=https://s3.eu-central-2.wasabisys.com
TF_STATE_REGION=eu-central-2
TF_STATE_ACCESS_KEY=
TF_STATE_SECRET_KEY=
# --- Paid KeyValue off-cluster backups (docs/ADR021-keyvalue-management.md) ---
# Non-secret operator contract. All three must be set or the feature is disabled.
# The named apps-namespace Secret contains AWS_ACCESS_KEY_ID and
# AWS_SECRET_ACCESS_KEY and is provisioned out of band; never put its values here.
BEX_KV_BACKUP_DESTINATION=s3://bex-tfstate/keyvalue
BEX_KV_BACKUP_ENDPOINT=https://s3.eu-central-2.wasabisys.com
BEX_KV_BACKUP_S3_SECRET=bex-kv-backup-s3
# --- Persistent service disk snapshots (docs/ADR082-persistent-disks.md D5) ---
# Non-secret operator contract for the nightly per-disk snapshot. Render takes a
# daily block snapshot of every disk and keeps it 7 days; Hetzner has no volume
# snapshots at all, so bex streams the volume (tar | gzip | age) into an
# S3-compatible bucket instead. ALL of endpoint+bucket+s3-secret+age-public-key
# must be set or NO disk is backed up — a half-configured store would either
# write nowhere or write a tenant's whole filesystem unencrypted to a third
# party. Use a bucket dedicated to backups, never bex-tfstate.
BEX_DISK_SNAPSHOT_ENDPOINT=
BEX_DISK_SNAPSHOT_BUCKET=
BEX_DISK_SNAPSHOT_PREFIX=
BEX_DISK_SNAPSHOT_REGION=
# Apps-namespace Secret with AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.
BEX_DISK_SNAPSHOT_S3_SECRET=
# age recipient for disk snapshots — a keypair DEDICATED to disks, not the
# platform AGE_BACKUP_* pair. Restoring a disk needs the decrypt half inside the
# cluster, and ADR050 deliberately keeps the platform key out of it; a separate
# pair confines that exposure to data already sitting on the volume in the same
# cluster. The public half is safe to set here.
BEX_DISK_SNAPSHOT_AGE_PUBLIC_KEY=
# Apps-namespace Secret holding the matching private key under "private". Only
# restore Jobs mount it; a nightly backup never needs it.
BEX_DISK_SNAPSHOT_AGE_SECRET=
# bex-api reads the same bucket to LIST a disk's snapshots (it never writes or
# decrypts one), so it needs its own read credentials. The 24-hour snapshotKey
# a listing hands out is signed with BEX_SHELL_TICKET_SECRET — it signs a
# reference to an object, never its contents, so bex-api never holds the age
# key that could decrypt a snapshot.
BEX_DISK_SNAPSHOT_ACCESS_KEY=
BEX_DISK_SNAPSHOT_SECRET_KEY=
# --- Encrypted platform backups (docs/ADR050-encrypted-platform-backups.md) ---
# One age keypair (age-keygen). The PUBLIC key is not secret: it is the recipient
# for Tier A client-side encryption (etcd/OpenBao/KeyValue). The operator consumes
# it as BEX_BACKUP_AGE_PUBLIC_KEY for the KeyValue backup Job; the two static
# CronJobs read it from the OPTIONAL bex-backup-age ConfigMap in their namespace
# (kube-system, secrets) — created out of band from this same value. Empty ⇒ Tier A
# encryption disabled (plain .gz upload, byte-identical to pre-ADR050).
BEX_BACKUP_AGE_PUBLIC_KEY=
AGE_BACKUP_PUBLIC_KEY=
# The PRIVATE half is a high-trust secret in the same custody tier as the OpenBao
# unseal keys (never committed, never printed): needed only to DECRYPT during
# restore (scripts/restore-*.sh source it here). scripts/gh-secrets.sh pushes it to
# GitHub Actions. Retain the previous key past the backup retention window on
# rotation (see ADR050 §1). Fill the real value in .env only, never here.
AGE_BACKUP_PRIVATE_KEY=
# Optional rotation overlap identity; remove only after the old backup inventory
# has expired (docs/runbooks/backup-age-key-rotation.md).
AGE_BACKUP_PRIVATE_KEY_PREVIOUS=
# --- In-cluster Zot registry credentials (w7/m8+m36, docs/ADR022-tenant-isolation.md
# § Registry access control) ---
# Zot denies anonymous catalog/pull/push. scripts/registry-secrets.sh reads these
# and creates the out-of-band Secrets (bex-registry/zot-htpasswd htpasswd, the
# bex-registry-push docker-config in the build ns) — no secret material in git.
# Generate each with: openssl rand -hex 16
BEX_REGISTRY_BUILDER_PASSWORD=
# Optional kpack alias of the same internal Zot endpoint. The checked-in
# deployment defaults to zot.local:5000 for upstream kpack's HTTP detection.
BEX_KPACK_REGISTRY=
# Per-App pull credentials (w7/m36): set BEX_REGISTRY_NS to the Zot namespace
# (typically bex-registry) to activate per-App Zot credentials — the operator
# then mints a unique app-<name> htpasswd user + per-repo Zot ACL for each App
# and stores the pull credential in reg-pull-<name> in the App namespace.
# Unset (default) → shared bex-registry-pull path (byte-identical, backward-compat).
BEX_REGISTRY_NS=
BEX_ZOT_HTPASSWD_SECRET=
BEX_ZOT_CONFIG_SECRET=
# Build-namespace pull credential for the static-site publish Job's extract
# initContainer (pulls the just-built tenant image from Zot). Distinct from the
# apps-namespace tenant pull secret, which is unreachable when BEX_BUILD_NAMESPACE
# differs from the App namespace. Defaults to bex-registry-pull (scripts/registry-secrets.sh)
# when a push credential is set; unset → anonymous pull (dev's unauthenticated Zot).
BEX_REGISTRY_BUILD_PULL_SECRET=
# Tag-retention count for Zot per-App repositories (mostRecentlyPushedCount;
# default 5). Increase to retain more historical image tags.
BEX_ZOT_RETENTION_COUNT=
# --- bex-api bootstrap credential (docs/ADR012-auth.md, docs/ADR006-bex-api.md#auth) ---
# bex-api has no shared static token: callers hold OAuth2 clients (API keys).
# This is the secret of the seeded `bex-bootstrap` client — the platform
# operator's/CI's own key, and the one used to mint more via /v1/api-keys.
# Generate: openssl rand -hex 16 Seed/rotate: scripts/auth-bootstrap-client.sh
BEX_BOOTSTRAP_CLIENT_SECRET=
# --- Platform auth: Ory Kratos + Hydra (docs/ADR012-auth.md) ---
# scripts/auth-secrets.sh reads these and creates the auth/kratos + auth/hydra
# k8s Secrets out-of-band of GitOps. Generate each with: openssl rand -hex 16
# (32 hex chars — exactly the length KRATOS_SECRETS_CIPHER requires).
KRATOS_SECRETS_DEFAULT=
KRATOS_SECRETS_COOKIE=
KRATOS_SECRETS_CIPHER=
HYDRA_SECRETS_SYSTEM=
HYDRA_SECRETS_COOKIE=
HYDRA_OIDC_PAIRWISE_SALT=
# OpenFGA API preshared key (authorization service, cluster-internal).
OPENFGA_PRESHARED_KEY=
# Sign in with GitHub via Kratos oidc (docs/ADR012-auth.md § Social login). Optional:
# BOTH set => auth-secrets.sh enables the GitHub provider button on the login/
# sign-up pages; unset => social login stays off (email+password only). Create
# your own GitHub OAuth app (Settings → Developer settings → OAuth Apps),
# callback URL https://auth.<base-domain>/self-service/methods/oidc/callback/github.
BEX_GITHUB_OIDC_CLIENT_ID=
BEX_GITHUB_OIDC_CLIENT_SECRET=
# --- Platform observability UI: Grafana at obs.bex.co (docs/ADR088-platform-observability-ui.md) ---
# Hydra client secret for the first-party `bex-obs` OIDC client (Grafana's
# generic_oauth). scripts/auth-bootstrap-client.sh refuses to provision the obs
# client while this is unset; the same value is installed out of band into the
# monitoring/grafana-oauth Secret (key client-secret) that the Grafana values
# reference. Never a value in git.
BEX_OBS_OAUTH_CLIENT_SECRET=
# Ops-workspace gate (bex-api + dashboard consent, docs/ADR088 §4): the
# designated tea-* workspace whose membership is the Grafana ACL, and the static
# bearer protecting bex-api's internal GET /internal/ops-role verb
# (machine-to-machine from dashboard SSR only). Either unset => the verb is
# absent, the workspace guards are inert, and any OAUTH_OPS_CLIENTS-listed
# client is rejected at consent (fail closed).
BEX_OPS_WORKSPACE=
BEX_OPS_ROLE_TOKEN=
# Grafana break-glass local-admin password — installed out of band as the
# monitoring/grafana-admin Secret (key admin-password); OIDC is the normal
# sign-in path, this is recovery only.
GRAFANA_ADMIN_PASSWORD=
# --- QA account for live product hunts (/qa-find-bugs) ---
# Sign-in credentials for a dedicated QA user on https://dashboard.bex.co. The
# /qa-find-bugs skill reads these from .env inside the Playwright MCP process so
# the password never reaches the agent transcript; nothing else consumes them.
# Use a real but disposable account whose resources are safe to create/delete.
QA_EMAIL=
QA_PASSWORD=
# --- Production canary probes (w3/m83, docs/ADR088-platform-observability-ui.md §6) ---
# One workspace-scoped API key for the first-party `bex-canary` workspace, in
# `<key-id>:<key-secret>` form (a bex API key is a Hydra client_credentials
# pair, docs/ADR012-auth.md §API keys — not a static token, so the probes
# exchange it at the issuer before every run). It authorizes exactly the canary
# workspace: scripts/tenant-view-liveness.sh reads that workspace's one canary
# service through /v1/logs, /v1/metrics, and /v1/services/{id}/events, and
# scripts/deploy-canary.sh creates and deletes a fixture inside it. Both refuse
# to touch anything else. scripts/gh-secrets.sh pushes it; while unset, both
# scheduled probes soft-skip with a notice instead of failing.
# The fixture's non-secret ids (BEX_CANARY_WORKSPACE_ID, BEX_CANARY_SERVICE_ID,
# BEX_CANARY_URL, and the optional BEX_CANARY_STATIC_REPO) are repository
# VARIABLES, not secrets, so a wrong value is readable in the run log.
BEX_CANARY_API_KEY=
# --- Platform email: one SMTP relay for Kratos's courier + bex-api's mailer (docs/ADR012-auth.md §11) ---
# Prod = SendGrid (integrated as a plain SMTP relay, no SDK — point it anywhere);
# local = Mailpit (the mock cluster's catcher). auth-secrets.sh writes the courier
# URI into the kratos Secret (key smtpConnectionURI) and the BEX_SMTP_USERNAME/PASSWORD
# pair into bex-system/bex-smtp.
# Kratos courier connection URI (REQUIRED — the courier is enabled in the values).
# SendGrid: smtp://apikey:<sendgrid-api-key>@smtp.sendgrid.net:587 (username is literally "apikey")
# Port 587 + STARTTLS, NOT 465: Hetzner blocks outbound 25/465 (implicit TLS),
# while 587/2525 are open. With :465 the flow still reports "sent_email" but the
# courier loops on `dial tcp …:465: i/o timeout` and no mail lands (docs/ADR012-auth.md §11).
# Mailpit: smtp://mailpit.auth.svc:1025/?disable_starttls=true (no TLS locally)
KRATOS_COURIER_SMTP_URI=
# bex-api workspace-invite mailer (w4/m12, docs/ADR024-members.md). Same relay as the
# courier. ADDR/FROM reach the pod as non-secret values (baked into the deployment
# for prod); USERNAME/PASSWORD ride the out-of-band bex-smtp Secret. All unset =>
# invites are recorded but not emailed (mailer nil). Locally point a host-run
# bex-api at Mailpit: BEX_SMTP_ADDR=localhost:1025 after a port-forward.
BEX_SMTP_ADDR=
BEX_SMTP_FROM=
BEX_SMTP_USERNAME=
BEX_SMTP_PASSWORD=
# --- Native mobile push: Expo Push Service (docs/ADR052-notifications.md, docs/runbooks/mobile-push.md) ---
# The third notification channel (email above, outbound webhooks, and this).
# Optional: PROVIDER blank => bex-api constructs no transport, makes no Expo
# network calls, and the dashboard push panel honestly reports "not configured"
# (policy still saves; nothing is delivered). Email + webhooks are unaffected.
# Both values ride the out-of-band bex-system/bex-push Secret — install/rotate
# them with scripts/push-secret.sh, never by editing the Deployment (its two
# secretKeyRefs are optional:true, which is what makes "absent" the clean
# disabled state). The access token is a SERVER credential: keep it out of
# EXPO_PUBLIC_*, app.json, Git, and any mobile binary. The EAS project id is
# public build-time config and lives in the mobile env file instead.
# expo is the only supported provider (device_push_subscriptions CHECK-constrains
# it); a non-empty unsupported value fails bex-api startup rather than degrading.
BEX_PUSH_PROVIDER=
BEX_EXPO_PUSH_ACCESS_TOKEN=
# Provider API base override. DO NOT SET IN PRODUCTION — it exists only to point
# a local bex-api at a loopback fake Expo for end-to-end verification. Blank =>
# https://exp.host/--/api/v2/push. Startup rejects any http:// value that is not
# loopback, so this cannot silently downgrade a real deployment to cleartext.
BEX_EXPO_PUSH_URL=
# --- Browser web push: VAPID (docs/ADR052-notifications.md, docs/runbooks/web-push.md) ---
# Optional third notification transport beside Expo. All three blank => bex-api
# constructs no VAPID sender and makes no push-service network calls; native
# Expo availability is unchanged. A partial set fails bex-api startup.
# Values ride the out-of-band bex-system/bex-webpush Secret — mint/install with
# scripts/webpush-secret.sh (prints only the public key). The Deployment's three
# secretKeyRefs are optional:true, so an absent Secret is the honest disabled
# state. The private key is a SERVER credential: keep it out of Git, issues,
# and any dashboard/mobile binary. The public key is handed to browsers as
# applicationServerKey.
BEX_WEBPUSH_VAPID_PUBLIC_KEY=
BEX_WEBPUSH_VAPID_PRIVATE_KEY=
# RFC 8292 `sub` claim: mailto: contact or https: origin. Blank in .env is
# filled by the installer to mailto:webpush@bex.local.
BEX_WEBPUSH_SUBSCRIBER=
# --- bex-api runtime knobs (docs/ADR023-usage-metering.md) ---
# Usage hot window: calendar months (current included) kept at hourly detail
# before compaction folds older months into monthly aggregates. Blank => 3.
BEX_USAGE_RETENTION_MONTHS=
# --- bex-api runtime knobs (audit log, w4/m10) ---
# Audit retention: days audit_events and SSH-session metadata survive before
# the daily sweep purges them. Blank => 90.
BEX_AUDIT_RETENTION_DAYS=
# Outbound-webhook delivery retention (w1/m67 F3): webhook_deliveries is both the
# durable queue and the dashboard's history view, so terminal (delivered or
# retries-exhausted) rows need a lifetime. Days a terminal row survives, and how
# many an endpoint keeps regardless of age. Blank => 90 days / 1000 rows. A
# pending or retryable delivery is never purged.
BEX_WEBHOOK_RETENTION_DAYS=
BEX_WEBHOOK_RETENTION_KEEP=
# Workspace-wide ceiling on open (pending or retrying) outbound webhook
# notifications. Admission and the source-event watermark commit together, so
# pressure drops only the webhook projection and never fails/replays the source
# deploy or resource mutation. Blank => 10000; 0 deliberately disables.
BEX_MAX_WEBHOOK_DELIVERIES_PER_WORKSPACE=
# Narrow the empty-audience OAuth token exception to bex-provisioned clients
# (w1/m67 F1, docs/ADR012-auth.md §7). 1 enables. Configure the operator-owned
# client-ID registry below first, or platform audience-less logins are refused.
BEX_OAUTH_REQUIRE_AUDIENCE=
# Comma-separated platform OAuth client IDs. Never derive this list from Hydra
# client metadata; trust must not depend on upstream DCR field filtering.
BEX_OAUTH_PLATFORM_CLIENTS=
# Control-plane OAuth capability vocabulary is closed (w8/m27,
# docs/ADR012-auth.md §7): third-party human tokens must carry bex.read /
# bex.write / bex.sensitive. This rule is independent of BEX_OAUTH_RESOURCE
# (an audience-less third-party human token is 401 even when discovery is
# off). This variable is ignored as a second semantic matrix (a custom value
# cannot invent a fourth capability). The name is retained so existing
# deployments and dashboard comments do not drift.
BEX_OAUTH_API_SCOPE=
# --- bex-api Stripe Billing (docs/ADR040-billing-metronome.md, w7/m50) ---
# Restricted runtime key that enables customer/subscription provisioning, sealed
# meter-event export, and real invoice reads. Blank => no Stripe client/network,
# emitter, billing object, or webhook route: estimate-only behavior is unchanged.
# Out-of-band secret; never commit. Requires BEX_CP_DB_URI.
BEX_STRIPE_SECRET_KEY=
# Stripe.js key for the workspace-create Payment Element. Safe for the browser
# but kept beside the runtime Stripe config so test/live mode rotates atomically.
# Required when BEX_REQUIRE_PAYMENT_METHOD is 1 or all.
BEX_STRIPE_PUBLISHABLE_KEY=
# Require a webhook-stamped payment method before billable creates/plan changes.
# 1 => paid-intent-only (ADR046: non-free Service/Postgres/Key Value/Blueprint);
# all => every plan, free tier included, plus agent-session dispatch (ADR075 D7,
# w6/m42). Either mode requires both BEX_STRIPE_SECRET_KEY and BEX_CP_DB_URI;
# any other value fails startup. Blank/0 => disabled.
BEX_REQUIRE_PAYMENT_METHOD=
# Test/stub API override only; production blank => https://api.stripe.com.
BEX_STRIPE_API_URL=
# Rewrite horizon (hours) before an hour's usage is final/exportable; blank => 48.
BEX_STRIPE_SEAL_HOURS=
# RFC3339 "billing starts here" floor (e.g. 2026-07-01T00:00:00Z). Blank while
# Stripe is enabled => now − 34d at startup, the bounded first-enable backfill.
BEX_STRIPE_EPOCH=
# Signing secret for POST /v1/webhooks/stripe. Blank => route not mounted.
# Out-of-band secret distinct from the API key; never commit.
BEX_STRIPE_WEBHOOK_SECRET=
# Stable perpetual 100%-off coupon used by Mode-B comping; blank =>
# bex-comp-100, provisioned by scripts/stripe-billing-setup.py.
BEX_STRIPE_COMP_COUPON_ID=
# Operator-owned Stripe Customer Portal bpc_* configuration id (optional).
BEX_STRIPE_PORTAL_CONFIGURATION_ID=
# Fail-closed Stripe Tax gate (both required to collect tax): canonical txcd_*
# Product tax code, and exclusive|inclusive Price tax behavior.
BEX_STRIPE_TAX_CODE=
BEX_STRIPE_TAX_BEHAVIOR=
# Dunning lifecycle knobs — TEST MODE ONLY (m52; live-mode dunning is refused at
# startup). Read by scripts/stripe-billing-secret.sh. Blank => disabled/defaults.
BEX_STRIPE_DUNNING_ENABLED=
BEX_STRIPE_GRACE_PERIOD=
BEX_STRIPE_RECONCILE_INTERVAL=
# --- GitHub App integration (docs/ADR026-github-integration.md) ---
# bex-api connects a workspace's GitHub via a self-hosted GitHub App (private-
# repo deploys + zero-config push-to-deploy). Create your own app via GitHub's
# app-manifest flow, then fill these. Any of the three unset => every git-connect
# verb returns 503. Private key is PEM (out-of-band secret; never commit).
BEX_GITHUB_APP_ID=
BEX_GITHUB_APP_PRIVATE_KEY=
BEX_GITHUB_APP_SLUG=
# The app's required OAuth credentials. The App must enable "Request user
# authorization (OAuth) during installation"; otherwise installation binding
# fails closed because bex cannot prove the callback user administers it.
# Client secret is out-of-band (never commit).
BEX_GITHUB_APP_CLIENT_ID=
BEX_GITHUB_APP_CLIENT_SECRET=
# The app's webhook HMAC key (w2/m9): a second accepted key on /v1/webhooks/git
# so GitHub-App-signed pushes redeploy without a per-repo webhook. Unset => only
# BEX_WEBHOOK_SECRET is accepted.
BEX_GITHUB_WEBHOOK_SECRET=
# --- Platform secrets: OpenBao (docs/ADR013-secrets.md) ---
# Leave these BLANK — scripts/bao-init.sh generates them on first run (Shamir
# unseal keys + root token from `bao operator init`) and writes them back here
# itself. Every later run reads them back out to unseal. Never printed.
BAO_UNSEAL_KEY_1=
BAO_UNSEAL_KEY_2=
BAO_UNSEAL_KEY_3=
BAO_ROOT_TOKEN=
# --- bex-api rate limits + request caps (docs/ADR006-bex-api.md#rate-limits) ---
# Defaults apply when unset; 0 disables the respective cap entirely.
BEX_RATE_LIMIT=
BEX_RATE_BURST=
BEX_DEVICE_RATE_LIMIT=
BEX_DEVICE_RATE_BURST=
# Webhook-intake IP-keyed limiter (w7/m60): meters POST /v1/webhooks/git and
# /v1/webhooks/stripe before the body read + HMAC. Default 600 req/min per IP.
BEX_WEBHOOK_RATE_LIMIT=
BEX_WEBHOOK_RATE_BURST=
# Deploy-hook IP limiter, applied before token lookup. Defaults to 60/min with a
# burst of 10; the valid-token limiter remains independently fixed at 6/min.
BEX_DEPLOY_HOOK_LOOKUP_RATE_LIMIT=
BEX_DEPLOY_HOOK_LOOKUP_RATE_BURST=
# Pre-auth admission (w1/m67 F1): the limiters above run INSIDE the auth gate,
# keyed on the resolved identity, so they cannot bound the work of resolving one
# — every unique invalid bearer/session costs a Hydra or Kratos round trip. This
# budget charges only credentials that come back INVALID (a successful auth is
# never charged, so the dashboard's one-pod-IP SSR traffic is never throttled).
# Blank => 60 failures/min per client IP, burst = limit; 0 disables.
BEX_AUTH_FAILURE_LIMIT=
BEX_AUTH_FAILURE_BURST=
# Process-wide cap on concurrent upstream auth calls. Blank => 64; 0 disables.
BEX_AUTH_MAX_INFLIGHT=
# Per-credential concurrent upstream auth calls (independent of the process-
# wide cap). Session whoami is not positively cached, so a dashboard page load
# can hold many in-flight slots for one cookie. Blank => 64; 0 ⇒ unbounded
# per credential (global BEX_AUTH_MAX_INFLIGHT still applies).
BEX_AUTH_MAX_INFLIGHT_PER_CREDENTIAL=
# Trusted-proxy CIDRs for rate-limit identity (w4/m33): comma-separated CIDRs of
# the edge proxies (the Traefik pod network). When a request's immediate peer is
# inside one, every IP-keyed limiter above derives the real client IP from
# X-Forwarded-For / X-Real-IP; unset or untrusted peer ⇒ peer IP only, headers
# ignored. Malformed value ⇒ bex-api refuses to start.
# NOT optional behind a reverse proxy (w1/m68 F3): unset, every external client
# collapses onto the proxy's own IP and SHARES one pre-auth failure bucket, so a
# single anonymous flood 429s everyone's login. In-cluster prod sets the pod CIDR
# in lego/operator/config/api/deployment.yaml; set it here for a local run that
# puts bex-api behind a proxy.
BEX_TRUSTED_PROXY_CIDRS=
BEX_MAX_BODY_BYTES=
BEX_MAX_QUERY_HOURS=
BEX_MAX_SSE_CONNS=
# Live log-tail authorization watchdog (w4/034): every established
# /v1/logs/subscribe stream (SSE/WebSocket/NDJSON) re-runs a FRESH
# can_view_logs check on this interval, so a membership/key revocation or App
# deletion ends the tail within one interval instead of at the next admission.
# Go duration; blank uses 1m; negative disables (admission-only).
BEX_LOG_STREAM_REVALIDATE_INTERVAL=
# Public bex-api origin used to build copy-ready Deploy Hook URLs. This is not a
# secret. Blank => authenticated surfaces return a relative /v1/deploy-hooks path.
BEX_API_PUBLIC_URL=
# Explicit platform placement reported by Service/Postgres/Key Value metadata.
# Blank => omitted; never inferred from TF_STATE_REGION or BEX_STATIC_S3_REGION.
BEX_REGION=
# --- build concurrency (docs/ADR034-scalable-build-pipeline.md, w7/m9) ---
# App reconcile worker count defaults to 1. Post-D1 (docs/ADR060) this is a pure
# responsiveness knob; the two caps below carry the concurrency policy.
BEX_APP_RECONCILE_WORKERS=
# Per-workspace concurrent build cap: 0 (unset) = unlimited.
BEX_MAX_CONCURRENT_BUILDS=
# Cluster-wide active-build ceiling (docs/ADR060 D6): 0 (unset) = unlimited.
BEX_MAX_ACTIVE_BUILDS=
# --- build layer cache (docs/ADR060 D3, w7/m86) ---
# "registry" => each App's Dockerfile/native build reuses layers through its own
# <app-repo>-cache repository in Zot. Anything else (including blank) leaves the
# build Job byte-identical to before the feature existed.
BEX_BUILD_CACHE=
# --- SNI proxy connection admission + copy-loop bounds (F6, docs/ADR009 + ADR021) ---
# Postgres front door: global concurrent-connection cap (default 1024), per-source
# (client IP) cap (default 128), routed-copy idle timeout (default 1h) and max
# lifetime (default 24h). 0 disables that dimension.
BEX_PROXY_MAX_CONNS=
BEX_PROXY_MAX_CONNS_PER_SOURCE=
BEX_PROXY_IDLE_TIMEOUT=
BEX_PROXY_MAX_LIFETIME=
# Valkey (key-value) front door: same knobs, same defaults.
BEX_KV_PROXY_MAX_CONNS=
BEX_KV_PROXY_MAX_CONNS_PER_SOURCE=
BEX_KV_PROXY_IDLE_TIMEOUT=
BEX_KV_PROXY_MAX_LIFETIME=
# ── bex CLI distribution (cli-release.yml → bex-co/homebrew-tap) ────────────────
# Path to the PRIVATE half of the bex-co/homebrew-tap write deploy key
# (public half registered as that repo's deploy key). gh-secrets.sh pushes it
# as the BEX_TAP_PUSH_KEY Actions secret, which the release workflow uses to
# push the rendered Homebrew formula; while unset the step skips cleanly.
BEX_TAP_PUSH_KEY_FILE=$HOME/.ssh/bex-tap-deploy
BEX_ROUTER_URL=
BEX_ROUTER_ASSERTION_SECRET=
# --- Mobile store releases (.github/workflows/mobile-release.yml, mobile/AGENTS.md) ---
# All optional: blank => gh-secrets.sh skips them and the release workflow fails
# closed on the missing EXPO_TOKEN. EXPO_TOKEN is an Expo access token
# (expo.dev → Account settings → Access tokens) that lets CI start EAS builds
# and submissions. The three EXPO_ASC_* values are an App Store Connect API key
# (Users and Access → Integrations; role App Manager) and only matter when the
# iOS provisioning profile must be regenerated; until then EAS signs with the
# credentials it already stores. EXPO_ASC_API_KEY_P8_FILE is a path to the
# downloaded AuthKey_<id>.p8, never its contents.
EXPO_TOKEN=
EXPO_ASC_API_KEY_P8_FILE=
EXPO_ASC_KEY_ID=
EXPO_ASC_ISSUER_ID=