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:
| Variable | Required | Purpose |
|---|---|---|
DEV_CONNECTION_STRING | yes (dev) | Database used by bun dev, reeman, the generators, and every script under scripts/. Development tooling only ever touches this one. |
PROD_CONNECTION_STRING | yes (prod) | Database used only when the server is started with --prod (bun start). Required to boot in production. |
TEST_CONNECTION_STRING | yes (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_ZONE | yes | IANA time zone (e.g. Europe/Ljubljana) used for date, time and timestamp columns. Read unconditionally by config/db.ts. |
PORT | no | Port the main app binds to. Defaults to 2338. |
TEST_PORT | no | Overrides PORT under the --test flag (binds to 127.0.0.1 only). Falls back to PORT then 2338. "N/A" means no override. |
REEMAN_PORT | no | Port for the reeman app (apps/reeman/server.ts). Defaults to 2339. |
REEQA_PORT | yes | Port 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_URL | for reeman | Base URL of the main app, used by the reeman process to reload the main app's translations across the two-app split. |
SERVER_NAME | no | Hostname used when building self-referencing URLs (reload notifications, agent mode). Defaults to localhost. |
ADMIN_USERNAME | no | Username for the seeded admin account (Quick Start). |
ADMIN_EMAIL | no | Email address for the seeded admin account. |
ADMIN_PASSWORD | no | Password for the seeded admin account. "N/A" leaves it unset - you are prompted to type one. |
SESSION_STORE | no | "sql" (default) or "redis" (requires REDIS_ENABLED=true and REDIS_URL). |
CSRF_SECRET | prod | HMAC 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. |
STORAGE | no | "local" writes files to disk, "s3" uses S3-compatible storage. "N/A"/unset auto-detects from the S3 block. |
LOCAL_STORAGE_DIR | with STORAGE=local | Directory for uploads. Relative paths resolve from the project root. |
SMTP_ENABLED | no | Switch 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_HOST | with SMTP | SMTP server hostname. |
SMTP_PORT | with SMTP | SMTP server port (587 STARTTLS, 465 implicit TLS). |
SMTP_USERNAME | with SMTP | SMTP account username. |
SMTP_PASSWORD | with SMTP | SMTP account password / API key. |
SMTP_FROM | with SMTP | Default From: address. |
SQL_LOGGING | no | Set to "true" to log every SQL statement to logs/sql.ndjson. |
MAX_UPLOAD_SIZE_MB | yes | Upload 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_CONTENT | no | When "true", reeman marks every text/textarea/markdown field localized: true in newly generated schema columns. |
GROUP_JS | no | "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_JS | no | Set to "true" to additionally minify grouped output via Bun.build. No effect when GROUP_JS=false. |
RATE_LIMITING | prod | Brute-force protection on /login, /password, /register. Required in production; off unless "true"/"on". |
TRUST_PROXY | prod | How the limiter finds the real client IP: "cloudflare" (trusts CF-Connecting-IP) or "direct" (socket peer address). Production requires one of the two. |
REDIS_ENABLED | no | Switch for Redis. Off by default; only "true"/"on" enables it. REDIS_URL alone no longer enables anything. |
REDIS_URL | for Redis | Redis/Valkey connection URL, shared by sessions, cache, queue, and rate limiting. Takes effect only when REDIS_ENABLED=true. |
TEST_REDIS_URL | no | Real Redis for Redis-backed tests (use a different DB number, e.g. /1). "N/A" skips those tests. |
CACHE_ENABLED | no | "true" enables Redis-backed caching of search_records queries. Requires REDIS_ENABLED=true and REDIS_URL. |
CACHE_MAX_BYTES | no | Max serialized bytes for a cached value. Default 524288 (512 KB). |
CACHE_MAX_RECORDS | no | Max record count for a cached query result. Default 500. |
S3_HOSTNAME | for uploads | S3 endpoint hostname. Endpoint is built as S3_PROTOCOL://S3_HOSTNAME:S3_PORT. |
S3_PORT | for uploads | S3 endpoint port (e.g. 8333 for MinIO). |
S3_PROTOCOL | for uploads | "http" or "https". |
S3_ACCESS_KEY_ID | for uploads | S3 access key ID. |
S3_SECRET_ACCESS_KEY | for uploads | S3 secret access key. |
S3_REGION | optional | Region string; some providers require it. |
S3_IMAGE_BUCKET | for uploads | Bucket for images uploaded through the image editor. Defaults to "images". |
S3_FILE_BUCKET | for uploads | Bucket for documents uploaded through the file library. Defaults to "files". |
OPEN_IDE | no | Editor launched by the inspector's "open in editor" action: vscode, zed, nvim, sublime, idea. See Dev Inspector. |
OPENROUTER_KEY / OPENROUTER_MODEL | no | OpenRouter credentials for AI translation. |
GEMINI_API_KEY / GEMINI_MODEL | no | Google Gemini credentials for AI translation. |
OPENAI_API_KEY / OPENAI_MODEL | no | OpenAI credentials for AI translation. |
CLAUDE_API_KEY / CLAUDE_MODEL | no | Anthropic Claude credentials for AI translation. |
XAI_API_KEY / XAI_MODEL | no | xAI Grok credentials for AI translation. |
OLLAMA_URL / OLLAMA_MODEL | no | Local Ollama server for offline translation; takes priority over all other providers when set. |
HF_URL / HF_MODEL / HF_TOKEN | no | Hugging Face inference endpoint, model prefix, and token. The language pair is appended to HF_MODEL. |
MCP_ENABLE_TEMPLATE_RENDER | no | "true" lets the MCP server render .ree templates (executes local code). Off by default. |
MCP_ENABLE_MUTATIONS | no | "true" allows MCP tools that write (generators, translation edits). Off by default. |
MCP_READONLY_CONNECTION_STRING | MySQL MCP inspection | A separate SELECT-only MySQL account for safe inspection. "N/A" means SQLite inspection uses its own read-only URL. |
MCP_SERVER_PORT | no | Port reported to MCP clients by the project tool. Defaults to 2400. |
INTERNAL_ADMIN_ENDPOINTS | no | "true" exposes the internal translation-reload and rate-limit endpoints. Off by default. |
RELOAD_SECRET | with internal endpoints | Shared secret for those endpoints, sent as X-Reload-Secret. Must be >= 32 chars. |
AGENT_SERVER_PORT | for agent mode | Dedicated port for the main app's agent mode. Required by bun run agent. |
AGENT_REEMAN_SERVER_PORT | for Reeman agent mode | Dedicated port required by bun run agent:reeman. |
AGENT_REEQA_SERVER_PORT | for ReeQA agent mode | Dedicated port required by bun run agent:reeqa. |
AGENT_SECRET | no | Optional shared secret for X-Agent-User-Username requests. When set, the request must also send the matching X-Agent-Secret. |
AGENT_USER_USERNAME | no | Default username used when an agent request does not supply X-Agent-User-Username. |
CF_API_TOKEN / CF_ACCOUNT_ID / CF_D1_DATABASE_ID | no | Cloudflare D1 credentials for pulling edge applications into origin tables. "N/A" disables the D1 client. |
REEWEB_PUBLISHER_ENABLED | no | Explicit opt-in for publisher signals. The URL alone is not enough - both must be set. |
REEWEB_PUBLISHER_URL | no | Signal 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.toJSONandprops.toPrettyJSONinjections - The choice of
static/app-dev.cssvsstatic/app.cssin the layout - The live-reload WebSocket endpoint (only in dev)
- The static-file
Cache-Controlheader (no-storein 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_ZONEfor timestamps. LogsUsing DB SQLITE.mysql:→ usesTIME_ZONEfor all column types. LogsUsing 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:
| Script | Runs | Purpose |
|---|---|---|
bun dev | bun scripts/dev_run.ts --app | App server + Tailwind watcher with hot reload (dev orchestrator) |
bun dev:worker | bun scripts/dev_run.ts --app --worker | App server plus the queue worker (worker.ts) |
bun dev:reeman | bun scripts/dev_run.ts --reeman | Reeman app only (apps/reeman/server.ts, port from REEMAN_PORT) |
bun dev:reeqa | bun scripts/dev_run.ts --reeqa | ReeQA app only (apps/reeqa/server.ts, port from REEQA_PORT) |
bun dev:all | bun scripts/dev_run.ts --app --reeman --reeqa --worker | App server, reeman, reeqa, and the queue worker together |
bun start | bun apps/main/server.ts --prod | Production server (also called by systemd) |
bun css:build | tw -i ./css/app.css -o ./static/app.css --minify | Production CSS build |
bun css:watch | tw -i ./css/app.css -o ./static/app-dev.css --watch=always | Watching CSS build for development |
bun test | bun test --parallel | Run the test suite in parallel |
bun run git:production | git push origin main:production --force | Force-pushes main to the production branch (your server pulls from production) |
bun run service:install | sudo journalctl --rotate ... && sudo cp ./operations/reepolee.service /etc/systemd/system/reepolee.service | One-time systemd install on the server |
bun run service:logs | journalctl -u reepolee -f | Follow 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"
}
}
| Field | Value | Purpose |
|---|---|---|
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:
.env.local(if present, gitignored - for personal-machine overrides).env.{NODE_ENV}(ifNODE_ENVis set -.env.productionetc.).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.