Configuration

Reepolee has very little to configure. Everything that varies between environments - database connection, SMTP credentials, the port, the timezone - lives in .env and is read directly from Bun.env. Everything that varies between projects - supported locales, generator preferences, the database driver - lives in TypeScript files under config/ that you edit like any other source file.

There is no JSON config file, no YAML, no dotenv package to install (Bun loads .env automatically), no environment-resolution layer. This page covers what each piece is for and where the runtime reads it.

For the common toggles - switching database, switching session backend, adding a locale - bun reeman exposes each as a menu item that edits the relevant file (.env, the supported-locales config, etc.) so you don't have to remember which knob lives where.

The .env File

.env is gitignored and lives at the project root. The .env.example file in the repository shows the full set of variables a Reepolee project expects:

# --- Core: database & server (required to boot) ----------------------
DEV_CONNECTION_STRING=sqlite:app.db
# DEV_CONNECTION_STRING="mysql://login:pass@localhost/reepolee_dev"
# PROD_CONNECTION_STRING=sqlite:app.db
# PROD_CONNECTION_STRING="mysql://login:pass@localhost/reepolee"
TEST_CONNECTION_STRING=sqlite:test.db
TIME_ZONE=Europe/London
PORT=2338
SERVER_NAME=localhost

# --- Quick Start admin defaults --------------------------------------
ADMIN_USERNAME=admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=N/A

# --- Sessions / CSRF --------------------------------------------------
SESSION_STORE="sql"          # "sql" (default) or "redis" (needs Redis)
CSRF_SECRET=N/A              # required in production; openssl rand -base64 48

# --- Storage ----------------------------------------------------------
STORAGE="local"              # "local" keeps uploads on disk; "s3" uses S3
LOCAL_STORAGE_DIR="../storage"

# --- Email (SMTP) -----------------------------------------------------
SMTP_ENABLED=false           # the switch; "true" enables email delivery
SMTP_HOST=N/A
SMTP_PORT=N/A
SMTP_USERNAME=N/A
SMTP_PASSWORD=N/A
SMTP_FROM=N/A

# --- Common app toggles ----------------------------------------------
SQL_LOGGING=false
MAX_UPLOAD_SIZE_MB=10
LOCALIZE_CONTENT=false
GROUP_JS=true
BUNDLE_JS=false

# --- Redis (optional) -------------------------------------------------
REDIS_ENABLED=false          # the switch; REDIS_URL alone no longer enables it
REDIS_URL=N/A
CACHE_ENABLED=false

The S3 object-storage block (used only when STORAGE="s3"), the AI-translation provider block, and the agent-mode, MCP, and rate-limit settings live further down in the real file. See File Uploads, Dynamic Translations, and Rate Limiting.

Every variable is read via Bun.env.<NAME> from somewhere in the codebase. If you add a new one, you can read it the same way - no helper or registration step. The framework also keeps a committed inventory of the known variables in config/env_vars.ts (KNOWN_ENV_VARS) plus human-readable descriptions in config/env_var_descriptions.ts; the reeman /environment page reads those descriptions. When you add a variable, update .env.example and those two files - a test fails if the inventory and the descriptions drift.

Environment Variable Reference

The full set of variables Reepolee reads, grouped by purpose:

VariableRequiredPurpose
DEV_CONNECTION_STRINGyes (dev)Database used by bun dev, reeman, the generators, and every script under scripts/. Development tooling only ever touches this one.
PROD_CONNECTION_STRINGyes (prod)Database used only when the server is started with --prod (bun start). Required to boot in production.
TEST_CONNECTION_STRINGyes (tests)Database used by bun test and bun run db:clone-test. Must contain "test" in the DB name; the safety guard refuses non-test DBs.
TIME_ZONEyesIANA time zone (e.g. Europe/Ljubljana) used for date, time and timestamp columns. Read unconditionally by config/db.ts.
PORTnoPort the main app binds to. Defaults to 2338.
TEST_PORTnoOverrides PORT under the --test flag (binds to 127.0.0.1 only). Falls back to PORT then 2338. "N/A" means no override.
REEMAN_PORTnoPort for the reeman app (apps/reeman/server.ts). Defaults to 2339.
REEQA_PORTyesPort for the ReeQA app (apps/reeqa/server.ts) and its development app-switcher link. It must be an explicit valid TCP port; there is no fallback. The shipped .env.example uses 2340. See ReeQA.
MAIN_APP_URLfor reemanBase URL of the main app, used by the reeman process to reload the main app's translations across the two-app split.
SERVER_NAMEnoHostname used when building self-referencing URLs (reload notifications, agent mode). Defaults to localhost.
ADMIN_USERNAMEnoUsername for the seeded admin account (Quick Start).
ADMIN_EMAILnoEmail address for the seeded admin account.
ADMIN_PASSWORDnoPassword for the seeded admin account. "N/A" leaves it unset - you are prompted to type one.
SESSION_STOREno"sql" (default) or "redis" (requires REDIS_ENABLED=true and REDIS_URL).
CSRF_SECRETprodHMAC key for signing CSRF tokens. Required in production (--prod) - the server refuses to boot without it. Must be >= 32 chars. Generate with openssl rand -base64 48.
STORAGEno"local" writes files to disk, "s3" uses S3-compatible storage. "N/A"/unset auto-detects from the S3 block.
LOCAL_STORAGE_DIRwith STORAGE=localDirectory for uploads. Relative paths resolve from the project root.
SMTP_ENABLEDnoSwitch for email delivery. Off by default; only "true"/"on" enables sending. Once on, every SMTP field must hold a real value or boot fails.
SMTP_HOSTwith SMTPSMTP server hostname.
SMTP_PORTwith SMTPSMTP server port (587 STARTTLS, 465 implicit TLS).
SMTP_USERNAMEwith SMTPSMTP account username.
SMTP_PASSWORDwith SMTPSMTP account password / API key.
SMTP_FROMwith SMTPDefault From: address.
SQL_LOGGINGnoSet to "true" to log every SQL statement to logs/sql.ndjson.
MAX_UPLOAD_SIZE_MByesUpload size limit in MB, shared by image uploads and the data-to-sql tool. No fallback - missing, blank, or "N/A" fails loudly at the upload point.
LOCALIZE_CONTENTnoWhen "true", reeman marks every text/textarea/markdown field localized: true in newly generated schema columns.
GROUP_JSno"true"/"on" groups a page's script tags into one immediate and one deferred file, cached to disk. Off by default in code, on in the shipped .env.example.
BUNDLE_JSnoSet to "true" to additionally minify grouped output via Bun.build. No effect when GROUP_JS=false.
RATE_LIMITINGprodBrute-force protection on /login, /password, /register. Required in production; off unless "true"/"on".
TRUST_PROXYprodHow the limiter finds the real client IP: "cloudflare" (trusts CF-Connecting-IP) or "direct" (socket peer address). Production requires one of the two.
REDIS_ENABLEDnoSwitch for Redis. Off by default; only "true"/"on" enables it. REDIS_URL alone no longer enables anything.
REDIS_URLfor RedisRedis/Valkey connection URL, shared by sessions, cache, queue, and rate limiting. Takes effect only when REDIS_ENABLED=true.
TEST_REDIS_URLnoReal Redis for Redis-backed tests (use a different DB number, e.g. /1). "N/A" skips those tests.
CACHE_ENABLEDno"true" enables Redis-backed caching of search_records queries. Requires REDIS_ENABLED=true and REDIS_URL.
CACHE_MAX_BYTESnoMax serialized bytes for a cached value. Default 524288 (512 KB).
CACHE_MAX_RECORDSnoMax record count for a cached query result. Default 500.
S3_HOSTNAMEfor uploadsS3 endpoint hostname. Endpoint is built as S3_PROTOCOL://S3_HOSTNAME:S3_PORT.
S3_PORTfor uploadsS3 endpoint port (e.g. 8333 for MinIO).
S3_PROTOCOLfor uploads"http" or "https".
S3_ACCESS_KEY_IDfor uploadsS3 access key ID.
S3_SECRET_ACCESS_KEYfor uploadsS3 secret access key.
S3_REGIONoptionalRegion string; some providers require it.
S3_IMAGE_BUCKETfor uploadsBucket for images uploaded through the image editor. Defaults to "images".
S3_FILE_BUCKETfor uploadsBucket for documents uploaded through the file library. Defaults to "files".
OPEN_IDEnoEditor launched by the inspector's "open in editor" action: vscode, zed, nvim, sublime, idea. See Dev Inspector.
OPENROUTER_KEY / OPENROUTER_MODELnoOpenRouter credentials for AI translation.
GEMINI_API_KEY / GEMINI_MODELnoGoogle Gemini credentials for AI translation.
OPENAI_API_KEY / OPENAI_MODELnoOpenAI credentials for AI translation.
CLAUDE_API_KEY / CLAUDE_MODELnoAnthropic Claude credentials for AI translation.
XAI_API_KEY / XAI_MODELnoxAI Grok credentials for AI translation.
OLLAMA_URL / OLLAMA_MODELnoLocal Ollama server for offline translation; takes priority over all other providers when set.
HF_URL / HF_MODEL / HF_TOKENnoHugging Face inference endpoint, model prefix, and token. The language pair is appended to HF_MODEL.
MCP_ENABLE_TEMPLATE_RENDERno"true" lets the MCP server render .ree templates (executes local code). Off by default.
MCP_ENABLE_MUTATIONSno"true" allows MCP tools that write (generators, translation edits). Off by default.
MCP_READONLY_CONNECTION_STRINGMySQL MCP inspectionA separate SELECT-only MySQL account for safe inspection. "N/A" means SQLite inspection uses its own read-only URL.
MCP_SERVER_PORTnoPort reported to MCP clients by the project tool. Defaults to 2400.
INTERNAL_ADMIN_ENDPOINTSno"true" exposes the internal translation-reload and rate-limit endpoints. Off by default.
RELOAD_SECRETwith internal endpointsShared secret for those endpoints, sent as X-Reload-Secret. Must be >= 32 chars.
AGENT_SERVER_PORTfor agent modeDedicated port for the main app's agent mode. Required by bun run agent.
AGENT_REEMAN_SERVER_PORTfor Reeman agent modeDedicated port required by bun run agent:reeman.
AGENT_REEQA_SERVER_PORTfor ReeQA agent modeDedicated port required by bun run agent:reeqa.
AGENT_SECRETnoOptional shared secret for X-Agent-User-Username requests. When set, the request must also send the matching X-Agent-Secret.
AGENT_USER_USERNAMEnoDefault username used when an agent request does not supply X-Agent-User-Username.
CF_API_TOKEN / CF_ACCOUNT_ID / CF_D1_DATABASE_IDnoCloudflare D1 credentials for pulling edge applications into origin tables. "N/A" disables the D1 client.
REEWEB_PUBLISHER_ENABLEDnoExplicit opt-in for publisher signals. The URL alone is not enough - both must be set.
REEWEB_PUBLISHER_URLnoSignal endpoint of a paired ReeWeb Publisher (e.g. http://localhost:3011/api/render-signal).

is_s3_configured() in lib/s3/core.ts checks STORAGE together with the access key, secret, and endpoint (S3_HOSTNAME, with optional S3_PORT/S3_PROTOCOL). With STORAGE="s3" and an incomplete block, the process exits at startup; with STORAGE unset or "N/A" it auto-detects and falls back to local disk when the block is incomplete. See File Uploads for the full flow.

For environment-specific overrides - different SMTP credentials in staging vs production, a different PORT for a second instance - keep multiple .env files (.env.staging, .env.production) and copy the right one to .env during deploy. Bun reads only .env at startup.

The --dev / --prod Flag

The mode the application runs in is determined by a command-line flag, not an environment variable. The server reads:

const is_dev = Bun.argv.includes("--dev");

That single line drives every dev-vs-prod branch in the codebase:

  • Template caching (off in dev, on in prod)
  • The dev-only props.toJSON and props.toPrettyJSON injections
  • The choice of static/app-dev.css vs static/app.css in the layout
  • The live-reload WebSocket endpoint (only in dev)
  • The static-file Cache-Control header (no-store in dev, one-year immutable in prod)

The two scripts in package.json choose the flag:

{
    "scripts": {
        "dev": "bun scripts/dev_run.ts --app",
        "start": "bun apps/main/server.ts --prod"
    }
}

bun dev runs the dev orchestrator (scripts/dev_run.ts), which builds CSS once, then runs the Tailwind watcher (--watch=always) and the hot-reloading server (bun --hot apps/main/server.ts --dev) side by side. bun start runs the production server (bun apps/main/server.ts --prod); in production the systemd unit runs the same apps/main/server.ts --prod entry with --hot added so a git pull auto-reloads - see systemd. NODE_ENV=production from the systemd unit is included for any third-party library that reads it but isn't load-bearing for Reepolee itself.

The config/ Folder

Compile-time configuration lives in config/ as TypeScript files. Each one exports the values the rest of the codebase imports.

config/
├── db.ts                     ← database connection from DEV/PROD_CONNECTION_STRING
├── db_structure.ts           ← generator preferences (ignore tables/fields, boolean/date conventions)
├── domain_types/             ← canonical column types for MySQL and SQLite
│   ├── mysql.ts
│   └── sqlite.ts
├── rate_limit.ts             ← per-scope rate limit rules
├── supported_locales.ts      ← locale list and metadata
├── apps.ts                   ← the three dev apps (main, reeman, reeqa) and their ports
├── paths.ts                  ← app tree locations (apps/main, apps/reeman, apps/reeqa, platform)
├── env_vars.ts               ← KNOWN_ENV_VARS inventory and presence/enum checks
├── env_var_descriptions.ts   ← human-readable descriptions for the reeman /environment page
├── test_db.ts                ← test database safety guard and connection helper
├── db_cli.ts                 ← shared CLI flag parsing for database tools
├── api_blocklist.ts          ← columns excluded from API serialisation
└── excluded_translations.ts  ← translation keys exempt from AI re-translation

Every file in config/ is in version control - there's nothing to copy or generate. Which database you connect to is decided at startup by reading DEV_CONNECTION_STRING or PROD_CONNECTION_STRING from .env.

config/supported_locales.ts

Declares the locales your application supports and the metadata used by the picker and the formatters:

export const locales = ["en-us", "sl-si"] as const;
export const active_locales = ["en-us"] as const;
export const default_locale = "en-us";

export const locale_names: Record<string, string> = {
    "en-us": "English",
    "sl-si": "Slovenščina",
};

export const locale_aliases: Record<string, string> = {};

Adding a new locale updates this configuration and adds {locale}.json translation files. The reeman Add locale flow handles both steps; see Adding a New Locale. The full mechanics are in Locales.

config/db_structure.ts

Generator preferences - which tables to skip, which columns to ignore, how to recognise booleans, dates, images, and files:

export const INTERNAL_TABLE_PREFIX = "_" as const;
export const IGNORE_TABLES = ["modules", "sessions", "email", "images", "files", "users", "translations", "db_tables", "db_routes"] as const;
export const MAINTENANCE_FIELDS = ["created_at", "updated_at", "archived_at", "archived_by_user_id"] as const;
export const DATE_SUFFIXES = ["_on", "_by"] as const;
export const DATETIME_SUFFIXES = ["_at"] as const;
export const IMAGE_SUFFIXES = ["_image"] as const;
export const FILE_SUFFIXES = ["_file"] as const;
export const IGNORE_INDEX_FIELDS = ["display", "option_display", "option_text", "search_text", "hashed_password", "previous_hashed_password", "archived_by_user_display"] as const;
export const IGNORE_ORDER_FIELDS = ["option_display", "search_text", "hashed_password", "previous_hashed_password", "archived_by_user_display"] as const;
export const BOOLEAN_PREFIXES = ["is_", "has_", "can_"] as const;
export const MIN_PASSWORD_LENGTH = Bun.argv.includes("--dev") ? 1 : 8;

The archive (soft delete) fields - archived_at / archived_by_user_id - are declared here too: a table carrying archived_at is archivable, its generated archive_record writes those columns instead of issuing a DELETE, and every generated read filters archived_at IS NULL. What each constant does - and how to extend the lists for your project's conventions - is on the Generators page.

config/db.ts

Creates a Bun SQL connection from the active connection string and exports it as db. lib/env.ts picks the variable: PROD_CONNECTION_STRING when the server is started with --prod (bun start), DEV_CONNECTION_STRING in every other mode. Development tooling (reeman, the generators, scripts/) reads DEV_CONNECTION_STRING directly and never resolves through here, so a generator can never be pointed at the production database by accident.

config/db.ts also reads TIME_ZONE unconditionally. The prefix of the connection string determines which timezone configuration is applied:

  • sqlite: → uses UTC for date/time/datetime, TIME_ZONE for timestamps. Logs Using DB SQLITE.
  • mysql: → uses TIME_ZONE for all column types. Logs Using DB MYSQL.
  • Anything else → exits with an error.

You don't edit this file to switch databases - change DEV_CONNECTION_STRING (or PROD_CONNECTION_STRING) in .env. The reeman's "Set database type" option (bun reeman) flips both for you. See Database - Getting Started.

package.json Scripts

The scripts in package.json are the day-to-day commands. The ones you'll use most:

ScriptRunsPurpose
bun devbun scripts/dev_run.ts --appApp server + Tailwind watcher with hot reload (dev orchestrator)
bun dev:workerbun scripts/dev_run.ts --app --workerApp server plus the queue worker (worker.ts)
bun dev:reemanbun scripts/dev_run.ts --reemanReeman app only (apps/reeman/server.ts, port from REEMAN_PORT)
bun dev:reeqabun scripts/dev_run.ts --reeqaReeQA app only (apps/reeqa/server.ts, port from REEQA_PORT)
bun dev:allbun scripts/dev_run.ts --app --reeman --reeqa --workerApp server, reeman, reeqa, and the queue worker together
bun startbun apps/main/server.ts --prodProduction server (also called by systemd)
bun css:buildtw -i ./css/app.css -o ./static/app.css --minifyProduction CSS build
bun css:watchtw -i ./css/app.css -o ./static/app-dev.css --watch=alwaysWatching CSS build for development
bun testbun test --parallelRun the test suite in parallel
bun run git:productiongit push origin main:production --forceForce-pushes main to the production branch (your server pulls from production)
bun run service:installsudo journalctl --rotate ... && sudo cp ./operations/reepolee.service /etc/systemd/system/reepolee.serviceOne-time systemd install on the server
bun run service:logsjournalctl -u reepolee -fFollow the production log live

Add your own scripts as you need them. There's no Reepolee-specific structure - package.json scripts are plain shell commands.

The ree Section — Editor & LSP Integration

package.json contains a ree object that tells the Reepolee language server where to find templates, components, and translations in your project. This is read by the .ree LSP for syntax highlighting, go-to-definition, autocompletion of translation keys and template helpers, and component discovery. There is nothing to install or configure — the starter ships with the correct values and the LSP picks them up automatically.

{
  "ree": {
    "project_family": "reepolee",
    "template_roots": ["apps/main", "apps/reeman", "apps/reeqa", "platform"],
    "component_roots": ["components"],
    "translation_provider": "route-json",
    "translation_roots": ["apps/main", "apps/reeman", "apps/reeqa", "platform"],
    "issue_repo": "reepolee/reepolee-dev"
  }
}
FieldValuePurpose
project_family"reepolee"Selects the Reepolee project profile — determines how includes, components, and translations are resolved
template_roots["apps/main", "apps/reeman", "apps/reeqa", "platform"]Directories the LSP scans for .ree templates. Supports multiple roots for projects with multiple source trees
component_roots["components"]Directories the LSP scans for .ree components (ReeTags). <app-banner> resolves to components/app-banner.ree
translation_provider"route-json"Tells the LSP that translations are co-located {locale}.json files next to each route directory — the same files the server loads
translation_roots["apps/main", "apps/reeman", "apps/reeqa", "platform"]Where the LSP discovers {locale}.json files for autocompletion and go-to-definition of {_ } / {- } / {@ } keys
issue_repo"reepolee/reepolee-dev"Repository the dev issue reporter (Ctrl+Shift+I) files issues against. See Dev Inspector.

All paths are relative to the project root and must not traverse outside it — the LSP validates this at startup and ignores the section if any path escapes.

When the ree section is absent, the LSP falls back to heuristics: it looks for apps/main/ and lib/template/compiler.ts to detect a Reepolee project. The explicit section is faster and unambiguous — the starter ships with it already in place.

How Bun Loads .env

Bun reads .env files automatically at startup, with this priority:

  1. .env.local (if present, gitignored - for personal-machine overrides)
  2. .env.{NODE_ENV} (if NODE_ENV is set - .env.production etc.)
  3. .env

Later files override earlier ones. The variables become available on Bun.env.NAME and process.env.NAME (for compatibility with code that expects the Node-style API).

There's no dotenv package to import and no dotenv.config() call to make. If you've used Node's ecosystem before, this is one fewer initialisation step than you're used to.

Secrets in Production

.env files in production should be readable only by the deploy user - chmod 600 .env makes it rw-------. The systemd unit runs as the deploy user, which can read its own .env; nobody else on the system can.

Two variables matter especially in production: CSRF_SECRET (must be >= 32 chars and stable across deploys, or every open form invalidates) and TRUST_PROXY (must be cloudflare or direct). The server refuses to boot without them under --prod.

For deployments that source secrets from a vault (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager), the integration is whatever fetches the secrets and writes them to .env before the systemd service starts. Reepolee reads Bun.env.NAME regardless of how the variable got there.

For the simplest production setup - a single VPS with .env on disk and chmod 600 - that's enough. Vaults add value when you have multiple servers, an audit requirement for who-accessed-what, or rotation that has to happen without redeployment. For most projects, the simpler approach is fine until it isn't.