forked from libredb/libredb-studio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.env.example
More file actions
499 lines (461 loc) · 27.1 KB
/
Copy path.env.example
File metadata and controls
499 lines (461 loc) · 27.1 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
# ============================================
# LibreDB Studio - Environment Configuration
# ============================================
#
# LOCAL DEVELOPMENT:
# Copy this file to .env.local and fill in your values
# cp .env.example .env.local
#
# DOCKER / RENDER DEPLOYMENT:
# Set these variables in your deployment environment
#
# ============================================
# ============================================
# SERVER BIND ADDRESS (Optional)
# ============================================
# Which address the standalone server listens on. The app never reads it - it
# goes straight to the Next.js server - so it applies to every channel that
# starts that server, and has no effect when the npm package is embedded.
# Leave it unset and the channel decides. The Docker image and the Helm chart
# resolve it at container startup and prefer "::" - all addresses of both
# families - falling back to 0.0.0.0 where the namespace has no usable IPv6.
# The native channels (npx, .deb/.rpm, Homebrew, Snap) force 127.0.0.1 and
# treat exposure as an explicit opt-in (--host, or LIBREDB_BIND for the
# packaged wrappers); they run no probe and pick nothing for you.
# IPv6 literals are accepted: "::" listens on every IPv6 address and, in a Node
# server, on every IPv4 address through the same socket - libuv clears
# IPV6_V6ONLY, so this holds even where net.ipv6.bindv6only=1 (measured).
# "::1" is IPv6 loopback, the IPv6 form of the local-first default.
# Setting this in a container overrules the resolver, so it is how you pin the
# container back to IPv4: `docker run -e HOSTNAME=0.0.0.0`. Kubernetes: the
# chart's config.bindAddress (or extraEnv, which renders an explicit env entry
# and so overrides the ConfigMap). A dual-stack cluster needs only
# service.ipFamilyPolicy now - do NOT also pin an IPv4 literal here, or the
# Service advertises an IPv6 address the pod does not listen on.
# Per-channel defaults and the reverse-proxy advice: docs/DISTRIBUTION.md
# (Network exposure).
# HOSTNAME=0.0.0.0
# ============================================
# AUTHENTICATION (Required when AUTH_BOOTSTRAP=off)
# ============================================
# Admin credentials (full access + maintenance tools) — ADMIN_PASSWORD is
# required only when AUTH_BOOTSTRAP=off; otherwise it is auto-generated on
# first start (see ZERO-CONFIG BOOTSTRAP section below).
ADMIN_EMAIL=admin@libredb.org
ADMIN_PASSWORD=your_secure_admin_password
# User credentials (query execution only) — OPTIONAL.
# The lower-privilege user account exists only when USER_PASSWORD is set.
# Leave USER_PASSWORD unset to run admin-only (no default user password is ever assumed).
USER_EMAIL=user@libredb.org
USER_PASSWORD=your_secure_user_password
# JWT Secret for session management (min 32 characters)
# Generate with: openssl rand -base64 32
# A shorter value stops the server at startup (exit code 1) instead of booting
# into a deployment where the health check is green but every login returns 503.
# Leave it unset to have a strong secret generated on first run.
JWT_SECRET=your_32_character_random_string_here
# ============================================
# ZERO-CONFIG BOOTSTRAP (Optional)
# ============================================
# When JWT_SECRET and/or ADMIN_PASSWORD are NOT set, the server generates the
# missing values on first start, persists them to <data dir>/auth-bootstrap.json
# (file mode 0600), and prints the admin password ONCE to stdout. Explicit env
# vars always win. In OIDC mode (NEXT_PUBLIC_AUTH_PROVIDER=oidc) only the JWT
# secret is generated — a password is never generated. Set to "off" (or
# "false"/"0", case-insensitive) to disable bootstrap and require explicit
# configuration (strict mode: missing vars surface as a clear login error).
# Unrecognized values warn and leave bootstrap on.
# AUTH_BOOTSTRAP=on
# ============================================
# COOKIE SECURITY (Optional)
# ============================================
# The Secure flag on the auth cookies (auth-token and the OIDC state cookie).
# Unset (default): on in production, off otherwise, with one exception - a
# request that arrived on a loopback host over plain http never gets it, so the
# desktop shell can keep a session (issue #232).
# Set to "false" ("off"/"0") when the browser itself reaches the app over plain
# HTTP on a host that is not loopback - a LAN or home-server deployment such as
# umbrelOS. The browser would otherwise reject the cookie and login would
# silently bounce back to /login. The session cookie then travels in cleartext,
# so keep it to trusted networks. TLS terminated at an ingress or load balancer
# does NOT need this: the browser still speaks HTTPS and accepts the flag.
# Set to "true" ("on"/"1") to force the flag on. Unrecognized values warn and
# leave the default in place.
# AUTH_COOKIE_SECURE=false
# ============================================
# AUTHENTICATION PROVIDER
# ============================================
# "local" (default) = email/password login (ADMIN_EMAIL/ADMIN_PASSWORD, USER_EMAIL/USER_PASSWORD)
# "oidc" = OpenID Connect SSO (Auth0, Keycloak, Okta, Azure AD, Zitadel, etc.)
NEXT_PUBLIC_AUTH_PROVIDER=local
# ============================================
# OIDC Configuration (required when NEXT_PUBLIC_AUTH_PROVIDER=oidc)
# ============================================
# Issuer URL — must serve /.well-known/openid-configuration
# OIDC_ISSUER=https://dev-xxx.auth0.com
# OIDC_CLIENT_ID=your_client_id
# OIDC_CLIENT_SECRET=your_client_secret
# Scopes to request (default: openid profile email)
# OIDC_SCOPE=openid profile email
# if using Zitadel, add this scope: urn:zitadel:iam:org:project:roles
# Role mapping (optional) — claim path for determining admin vs user role
# Supports dot-notation for nested claims (e.g. "realm_access.roles")
# OIDC_ROLE_CLAIM=
# Comma-separated values that map to admin role (default: admin)
# OIDC_ADMIN_ROLES=admin
# --- Provider-specific role claim examples ---
# Auth0: OIDC_ROLE_CLAIM=https://myapp.com/roles (via Auth0 Actions)
# Keycloak: OIDC_ROLE_CLAIM=realm_access.roles
# Okta: OIDC_ROLE_CLAIM=groups
# Azure AD: OIDC_ROLE_CLAIM=roles
# Zitadel: OIDC_ROLE_CLAIM=urn:zitadel:iam:org:project:roles
# ============================================
# STORAGE PROVIDER (Optional)
# ============================================
# Controls where application data is persisted.
# "local" (default) = browser localStorage only (zero config, great for dev)
# "sqlite" = SQLite file on server (persistent, single-node)
# "postgres" = PostgreSQL database (persistent, multi-node, enterprise)
#
# Note: NOT prefixed with NEXT_PUBLIC_ — server-side only, discovered at runtime
# via GET /api/storage/config endpoint.
STORAGE_PROVIDER=local
# SQLite storage path (required when STORAGE_PROVIDER=sqlite)
# STORAGE_SQLITE_PATH=./data/libredb-storage.db
# PostgreSQL connection URL (required when STORAGE_PROVIDER=postgres)
# Local PostgreSQL without SSL:
# STORAGE_POSTGRES_URL=postgresql://user:pass@localhost:5432/libredb?sslmode=disable
# Cloud PostgreSQL with SSL:
# STORAGE_POSTGRES_URL=postgresql://user:pass@host:5432/libredb?sslmode=require
# Credential encryption key for the SERVER-SIDE store (sqlite/postgres only) — OPTIONAL.
# When STORAGE_PROVIDER is sqlite or postgres, database passwords, connection strings, TLS
# client keys and SSH keys/passphrases are encrypted before they are written, so a stolen
# database file or dump is useless on its own.
# Leave this unset and the key is derived from JWT_SECRET, so there is nothing to configure.
# Set it (at least 32 characters, generate with: openssl rand -base64 32) when you want the
# storage key separated from the session-signing key — for example so JWT_SECRET can be
# rotated without invalidating every saved connection password.
# IMPORTANT: rotating whichever key is in use makes existing stored credentials unreadable.
# They are omitted from the connection, not deleted; the rest of the connection survives and
# you re-enter the password once. Restore the previous key BEFORE the app writes again if you
# want the old values back.
# Browser localStorage is NOT encrypted; this variable does not change that.
# STORAGE_ENCRYPTION_KEY=your_32_character_random_string_here
# ===========================================
# SQLite DB Provider Driver (advanced)
# ===========================================
# The SQLite *target-database* provider picks the runtime's built-in driver
# automatically: bun:sqlite under Bun, node:sqlite under Node (>= 24).
# Set this only to force a specific driver (e.g. deterministic tests).
# Options: bun, node
# LIBREDB_SQLITE_DRIVER=node
# ===========================================
# Oracle Thick-mode Client (advanced)
# ===========================================
# The Oracle DB provider runs in Thin mode (pure JS, no Instant Client) by
# default, which only supports Oracle Database 12.1+. Set this to an installed
# Oracle Instant Client "lib" directory to opt into Thick mode for older
# servers (11.2 and earlier). Leave unset unless you hit driver error NJS-138.
# Version matters: use Instant Client 19c for an Oracle 11.2 (11g) server -
# 21c/23ai cannot reach 11.2. The published image is Thin-only and does not
# bundle the client; see docs/providers/oracle.md section 4.4 to build an
# image with it. A wrong path fails fast with a clear config error.
# ORACLE_CLIENT_LIB_DIR=/opt/oracle/instantclient_19_28
# ===========================================
# LLM Configuration (Strategy Pattern)
# ===========================================
# Provider options: gemini, openai, ollama, custom
# The system uses Strategy Pattern to automatically select
# the appropriate provider based on this configuration.
LLM_PROVIDER=gemini
# API Key for the selected provider
# Required for: gemini, openai
# Optional for: ollama, custom (depends on endpoint)
#
# Get API keys from:
# - Gemini: https://aistudio.google.com/
# - OpenAI: https://platform.openai.com/
LLM_API_KEY=your_api_key_here
# Model name (optional - auto-defaults based on provider)
# Default models:
# - Gemini: gemini-2.5-flash
# - OpenAI: gpt-4o
# - Ollama: llama3.2
# - Custom: gpt-3.5-turbo
#
# Popular options:
# Gemini: gemini-2.5-flash, gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
# OpenAI: gpt-4o, gpt-4-turbo, gpt-3.5-turbo, gpt-4o-mini
# Ollama: llama3.2, mistral, codellama, deepseek-coder
LLM_MODEL=gemini-2.5-flash
# API URL (optional - only needed for ollama or custom providers)
# Default URLs:
# - Ollama: http://localhost:11434/v1
# - OpenAI: https://api.openai.com/v1
#
# Custom provider examples:
# - LiteLLM: http://localhost:4000/v1
# - LMStudio: http://localhost:1234/v1
# - vLLM: http://localhost:8000/v1
# - LocalAI: http://localhost:8080/v1
#LLM_API_URL=http://localhost:11434/v1
# ===========================================
# Provider Configuration Examples
# ===========================================
# --- Gemini (Default) ---
# LLM_PROVIDER=gemini
# LLM_API_KEY=AIzaSy...
# LLM_MODEL=gemini-2.5-flash
# --- OpenAI ---
# LLM_PROVIDER=openai
# LLM_API_KEY=sk-...
# LLM_MODEL=gpt-4o
# --- Ollama (Local) ---
# LLM_PROVIDER=ollama
# LLM_MODEL=llama3.2
# LLM_API_URL=http://localhost:11434/v1
# AGENT MODE additionally needs a model that CALLS TOOLS, and on Ollama the model
# decides that rather than the endpoint: Ollama documents tool_choice as unsupported,
# so nothing can force the call. Establish it with a probe rather than from a model
# card - docs/AGENT_GUIDE.md has the measurement and how to repeat it.
# PLAN MODE needs no tools and is never probed (src/lib/agent/capability-gate.ts:74),
# so a model refused for Agent mode can still be used in Plan mode - which is what
# the rail offers when it reports the refusal.
# --- LiteLLM Proxy ---
# LLM_PROVIDER=custom
# LLM_API_KEY=your_litellm_key # optional
# LLM_MODEL=gpt-4o
# LLM_API_URL=http://localhost:4000/v1
# --- LMStudio (Local) ---
# LLM_PROVIDER=custom
# LLM_MODEL=local-model
# LLM_API_URL=http://localhost:1234/v1
# ─── Agent Runtime (available when AI is configured) ─────────────────────────
# The agent rail drives a read-only investigation over a connected database:
# it drafts statements, repairs them against the schema, and composes a report
# whose claims carry evidence references. Every database reach goes through the
# AGENT'S OWN audited operation pipeline - a policy decision, an audit event and
# budget accounting before the driver is touched (executeAuditedOperation,
# src/lib/db/operations/execution.ts:129) - so the agent can never exceed what
# that policy allows. It is the agent's pipeline and not one shared with the
# editor: a statement you run yourself calls the provider directly
# (src/app/api/db/query/route.ts:44) and gets neither check.
#
# AGENT MODE READS POSTGRESQL AND SQLITE ONLY. The read-only execution profile
# is database-native, so it exists only where the provider implements it
# (queryReadOnly: postgres.ts, sqlite.ts). On any other engine an Agent-mode run
# ends "engine-unsupported". Plan mode is toolless and opens on every connection,
# but it is GROUNDED — given the schema and the engine's own size estimates before
# its first turn — on PostgreSQL and SQLite only; elsewhere it runs ungrounded and
# says so. It runs no statement of yours on any engine, writes nothing, and hands
# every statement it drafts to you to run yourself.
#
# THERE IS NO FLAG TO TURN IT ON. Availability is derived from what is actually
# true on this server: the agent appears when a model is configured through the
# LLM_* settings above - there is no second place to enter an API key - AND the
# durable ledger below has a writable path. Configuring a model IS the opt-in,
# so a deployment that never sets LLM_API_KEY never sees an agent, and a
# deployment that sets one is not offered a Start that must fail.
# It is standalone-only either way: no agent surface appears when the npm
# package is embedded in libredb-platform.
#
# UPGRADING FROM 0.11 OR EARLIER: if LLM_API_KEY is already set - it powered the
# NL2SQL and Autopilot panels, which this release removes - the agent appears
# without you asking for one. The line below is how you decline it.
#
# The variable survives as the explicit OFF-switch. "false"/"off"/"0" mean no
# agent even with AI configured; "true"/"on"/"1" are still accepted and mean the
# default, but cannot conjure a model; unset means derive; an unrecognized value
# warns and is ignored.
# LIBREDB_AGENT_ENABLED=false
#
# Durable-execution backend for agent runs. Exactly two values are accepted; an
# unrecognized one is refused rather than silently defaulted, because the
# workflow runtime otherwise treats this variable as a module to load.
# local (default) zero-config, keeps run state on disk.
# SINGLE INSTANCE ONLY - it takes file locks, so do
# not point more than one replica at it.
# @workflow/world-postgres opt-in, required for more than one replica. Point
# WORKFLOW_POSTGRES_URL at its own PostgreSQL
# database (not one of your connected databases).
# Leaving it unset selects "local", except on a hosting platform that sets
# VERCEL_DEPLOYMENT_ID: there the workflow runtime would pick its own hosted
# backend instead, so the agent refuses to start until you set this explicitly.
# WORKFLOW_TARGET_WORLD=local
#
# Where the "local" backend keeps run state, and the second half of the
# availability answer above: if this path cannot be created and written, the
# agent reports itself absent and GET /api/agent/config says which condition
# failed. Its own variable, not one of ours: the SDK's fallback is
# ".workflow-data" resolved against the working directory, which is fine for a
# dev checkout and wrong in a container in two different ways. Under a read-only
# root filesystem the write fails outright unless the path is inside a mounted
# volume. In plain Docker /app IS writable, so nothing fails - the ledger simply
# lands outside the mounted volume and every run is lost the next time the
# container is recreated. Neither applies to the published artifacts: the image
# sets this to /app/data/workflow itself (Dockerfile, runtime stage) and
# docker-compose.yml restates it, so a bare `docker run` lands inside the data
# directory too - mount a volume on /app/data if the history should outlive the
# container. The npx launcher needs nothing here either: it defaults this to a
# per-user directory beside its payload cache (~/.libredb-studio/workflow-data),
# so run history follows the user rather than the folder they happened to start
# Studio from. Set it below only to put the ledger somewhere else.
# WORKFLOW_LOCAL_DATA_DIR=/app/data/workflow
#
# Full behaviour, what bounds a run, and the limitations it does NOT hide:
# docs/AGENT.md
# ─── LibreDB Embedded Sample ─────────────────────────────────────────────────
# LibreDB embedded sample: on first standalone startup, auto-provide an editable
# "Sample (LibreDB)" connection seeded with example data (one per lens). Default on.
# Set to "false" to disable. Has no effect when embedded in libredb-platform.
# LIBREDB_EMBEDDED_SAMPLE=true
# Optional file path override (default: <data dir>/sample.libredb):
# LIBREDB_EMBEDDED_SAMPLE_PATH=/app/data/sample.libredb
# ─── SQLite Embedded Sample ──────────────────────────────────────────────────
# SQLite embedded sample: on first standalone startup, copy the vendored
# employees database (seed-assets/sqlite/employee.db) into the data dir and
# auto-provide an editable "Sample (Employees)" connection. Seeded
# asynchronously (never blocks boot); default on. Set to "false" to disable.
# Has no effect when embedded in libredb-platform.
# SQLITE_EMBEDDED_SAMPLE=true
# Optional runtime file path override (default: <data dir>/sample-employees.db):
# SQLITE_EMBEDDED_SAMPLE_PATH=/app/data/sample-employees.db
# Optional template override (default: <cwd>/seed-assets/sqlite/employee.db):
# SQLITE_EMBEDDED_SAMPLE_TEMPLATE=/app/seed-assets/sqlite/employee.db
# ─── Startup Banner ──────────────────────────────────────────────────────────
# On standalone startup the server prints one short block naming the version,
# the local URL and the project repository. It is plain stdout for whoever reads
# `docker logs` - nothing is sent anywhere. Set to "1" or "true" to print
# nothing. Has no effect when embedded in libredb-platform.
# LIBREDB_NO_BANNER=1
# ─── Seed Connections (pre-configured databases) ─────────────────────────────
# SEED_CONFIG_PATH=/app/config/seed-connections.yaml # Path to seed config file
# SEED_CACHE_TTL_MS=60000 # Cache TTL in ms (default: 60s)
# Credential env vars referenced in seed config (e.g., ${MY_DB_PASSWORD}):
# MY_DB_PASSWORD=secret
# ─── Monaco Editor Assets ────────────────────────────────────────────────────
# The SQL editor is served from this origin: `bun run build` stages the Monaco AMD
# bundle from node_modules into public/monaco/vs, so no CDN is contacted at runtime
# and the app works air-gapped. Override only when the assets live elsewhere —
# a sub-path mount, a CDN of your own, or libredb-platform embedding the npm
# package (see .claude/rules/platform-integration.md). When this is an ABSOLUTE
# URL, its origin is added to the Content-Security-Policy's script-src and
# worker-src automatically; a same-origin path (the default) needs no CSP change.
# NEXT_PUBLIC_MONACO_VS_PATH=/monaco/vs
# ─── Security Headers ────────────────────────────────────────────────────────
# Every response that passes through the app's request middleware carries
# X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options,
# Strict-Transport-Security and a Content-Security-Policy. Nothing needs to be set
# for that to happen. (Static assets and the two load-balancer/bootstrap paths,
# /api/db/health and GET /api/storage/config, are excluded from the middleware and
# carry none of these — see docs/BACKLOG.md. The health exclusion is by PATH, not
# method: POST /api/db/health is also a database-reaching route and also gets no
# Origin check and no security headers from the middleware this way. It still
# requires a session — it checks one itself, the same as every other guarded
# route — so this is a headers/CSRF gap, not an auth gap.)
#
# The CSP is enforced, not merely reported. If a deployment of yours breaks in a
# way you can trace to a blocked resource — most likely something served from an
# origin other than this app's own, such as a custom-hosted Monaco bundle or a CDN
# in front of static assets — set this to "true" to downgrade it to report-only:
# the browser then logs the same violation to its console instead of blocking the
# resource, with no server-side trace (the policy carries no report-uri). No
# rebuild is required; this is a plain runtime environment variable. Please also
# open an issue naming the violated directive - a prebuilt image cannot be
# rebuilt by the person whose channel broke, which is why this hatch exists.
# CSP_REPORT_ONLY=false
#
# HSTS is sent unconditionally with a 180-day max-age and no way to turn it off:
# an escape hatch that merely stops SENDING the header would be useless, because a
# browser that already cached the pin keeps enforcing HTTPS-only for the rest of
# the 180 days regardless of what the server does next, and a server that has
# already dropped TLS may not even be reachable to serve a corrective response.
# Browsers ignore the header entirely when it arrives over plain HTTP (RFC 6797),
# so plain-HTTP deployments are unaffected by any of this.
# HSTS is opt-in only for includeSubDomains: on studio.example.com it would also
# upgrade every unrelated sibling host to HTTPS-only.
# HSTS_INCLUDE_SUBDOMAINS=false
# ─── CSRF: Origin Check ──────────────────────────────────────────────────────
# Every POST, PUT, PATCH and DELETE must carry an Origin (or, failing that, a
# Referer) whose HOST matches this deployment's own host. Schemes are ignored
# deliberately, so a TLS-terminating proxy that forwards plain HTTP does not lock
# you out. This is a second layer behind the session cookie's SameSite=Lax.
#
# SET THIS if a reverse proxy rewrites the Host header to an internal name (a
# Kubernetes service name, a Docker network alias) and does not set
# x-forwarded-host. The symptom is a page that loads correctly and then refuses
# every action, including login, with a 403 whose body names this variable.
# Comma-separated; full origins or bare hosts both work.
# ALLOWED_ORIGINS=https://db.example.com,studio.internal:8443
#
# Non-browser callers (scripts, automation) that do not send a JSON body must
# send Origin: <your public origin> instead. There is no switch to disable the
# check entirely.
# ─── Rate Limiting ───────────────────────────────────────────────────────────
# Counters live in the app process, so the limits are PER REPLICA. The default
# deployment runs one replica. If you run more than one, enforce the same budgets
# at your ingress instead (nginx limit_req, Traefik rateLimit) - see
# charts/libredb-studio/README.md. Setting any *_MAX to 0 disables that bucket.
#
# Failed logins per client address per window. The sixth returns 429.
# RATE_LIMIT_LOGIN_MAX=5
# RATE_LIMIT_LOGIN_WINDOW_SEC=300
#
# Failed logins per submitted account per window. This one is keyed on a hash of
# the submitted email and is unaffected by a forged X-Forwarded-For, which is
# what keeps per-account brute force capped. A successful login clears it, so a
# typo run is not a lockout - but this is inherently a denial-of-login handle on
# a known account: whoever trips it (a stranger needs only the address, and the
# published default admin@libredb.org counts) locks the real owner out for the
# rest of the window, renewable indefinitely afterwards at roughly one wrong
# guess per window. That is the accepted trade for bounding brute force against
# an operator-set password, not an oversight - there is no design that removes
# this residual without also removing the bound. Set this to 0 to disable the
# bucket entirely (verified: every request is then allowed unconditionally, not
# blocked) if that trade is unacceptable for your deployment.
# RATE_LIMIT_LOGIN_ACCOUNT_MAX=20
# RATE_LIMIT_LOGIN_ACCOUNT_WINDOW_SEC=300
#
# Shared, per signed-in user, by every route that reaches an LLM provider or
# touches an agent run: the /api/ai/* routes, and every /api/agent/* route apart
# from GET /api/agent/config, which reaches no provider and only reports whether
# the agent exists. They share the bucket so that rotating between them cannot
# multiply the budget.
#
# It bounds how often LLM work can be STARTED, not how much that work costs. One
# agent run takes a single slot and then makes many model calls of its own, so
# this cap alone does not bound the bill on your LLM API key. What bounds a
# single run is the agent's own budget (see docs/AGENT.md).
# RATE_LIMIT_AI_MAX=20
# RATE_LIMIT_AI_WINDOW_SEC=60
#
# Shared across every database-reaching route under /api/db/* plus
# /api/admin/fleet-health (seventeen routes today - query, multi-query,
# transaction, disconnect, cancel, maintenance and the rest - see
# src/lib/api/rate-limit.ts for the current list), per signed-in user, so
# routing the same workload through a different endpoint does not multiply
# the budget.
# RATE_LIMIT_QUERY_MAX=120
# RATE_LIMIT_QUERY_WINDOW_SEC=60
#
# Bounds how many "permission denied" audit lines an unauthenticated scanner can
# produce. It never affects whether a request is refused, only how often the
# refusal is logged.
# RATE_LIMIT_ANON_MAX=5
# RATE_LIMIT_ANON_WINDOW_SEC=300
# ─── Forwarded Headers ───────────────────────────────────────────────────────
# The client address the rate limiter buckets on, and the "ip" field in the
# audit log, are derived from X-Forwarded-For (falling back to X-Real-IP). Both
# headers are attacker-controlled: the derived value is a HINT for bucketing and
# for reading logs, never an identity, and nothing in this product makes an
# authorization decision from it.
#
# 0 (default) takes the leftmost X-Forwarded-For entry. Set it to the number of
# proxies in front of Studio to take the entry that proxy appended instead.
# TRUSTED_PROXY_HOPS=0
#
# Set to "false" to ignore forwarded headers entirely. Be aware of what that
# costs: with no address signal at all, every anonymous caller shares one
# bucket, so a single attacker tripping the login limiter locks everyone out
# until the window closes.
# TRUST_PROXY_HEADERS=true