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) ----------------------
CONNECTION_STRING=sqlite:app.db
# CONNECTION_STRING="mysql://login:pass@localhost/reepolee_dev"
TIME_ZONE=Europe/London
PORT=2338
SERVER_NAME=localhost
SITE_URL=http://localhost:2338
# --- Sessions --------------------------------------------------------
SESSION_STORE="sql" # "sql" (default) or "redis" (needs REDIS_URL)
# --- Storage ---------------------------------------------------------
STORAGE="local" # "local" keeps uploads on disk; "s3" uses S3
LOCAL_STORAGE_DIR="../storage"
# --- Email (SMTP) ----------------------------------------------------
SMTP_HOST="sandbox.smtp.mailtrap.io"
SMTP_PORT=587
SMTP_USERNAME="..."
SMTP_PASSWORD="..."
SMTP_FROM="hello@yourdomain.com"
# --- Common app toggles ----------------------------------------------
SQL_LOGGING=false
TRANSLATED_ROUTES=true # locale-prefixed URLs (e.g. /sl-si/about)
MAX_UPLOAD_SIZE_MB=10
# Internal endpoints are disabled by default. Enable only for controlled operations.
# INTERNAL_ADMIN_ENDPOINTS=true
# RELOAD_SECRET="at-least-32-random-characters"
# --- Redis (optional): sessions, SQL cache, queue, rate limiting -----
# REDIS_URL="redis://localhost:6379"
CACHE_ENABLED=false # cache search_records queries (needs REDIS_URL)
# RATE_LIMITING=true # sliding-window rate limiting (needs REDIS_URL)
The S3 object-storage block (used only when STORAGE="s3") and agent-mode
settings live further down in the real file - see File Uploads
and Agent Mode. MCP is a
local stdio process, so it does not use a network port; see MCP Server.
AI provider variables are documented in the canonical Dynamic Translations - Choosing a Provider procedure. Keep provider setup there so precedence and model requirements do not drift across pages.
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.
Environment Variable Reference
The full set of variables Reepolee reads, with the file that consumes each:
| Variable | Required | Read by | Purpose |
|---|---|---|---|
PORT | yes | server.ts | The port the HTTP server listens on |
TEST_PORT | no | server.ts | Overrides PORT when the server runs with the --test flag (binds to 127.0.0.1 only). Falls back to PORT, then 2338. The smoke-test script defaults to 2600 |
AGENT_USER_USERNAME | no | routes/system/auth/middleware.ts | Default authenticated user for agent mode (bun run agent). The X-Agent-User-Username header overrides it per-request (guarded by X-Agent-Secret / AGENT_SECRET) |
AGENT_SERVER_PORT | no | server.ts | Port for the agent-mode server (avoids clashing with dev on 2338). Agent mode binds to 127.0.0.1 only and is allowed only alongside --dev |
AGENT_SECRET | no | routes/system/auth/middleware.ts | Optional shared secret for agent-mode identity headers. When set, a request that uses X-Agent-User-Username must also supply matching X-Agent-Secret |
MCP_ENABLE_TEMPLATE_RENDER | no | scripts/mcp/index.ts | Set to true only when a local MCP client may execute Ree template code. Template inspection remains available without it; rendering is otherwise rejected |
MCP_ENABLE_MUTATIONS | no | scripts/mcp/capabilities.ts | Set to true only when a local MCP client may run generators or other project/database mutation tools. The default MCP surface is inspection-only |
MCP_READONLY_CONNECTION_STRING | MySQL MCP inspection | scripts/mcp/db.ts | A separate MySQL account with SELECT-only and no file privileges. Required for safe MySQL database-inspection tools; not used by the application runtime |
INTERNAL_ADMIN_ENDPOINTS | no | lib/admin/require_admin_auth.ts | Set to true to enable the protected translation-reload and rate-limit diagnostic endpoints. They remain absent (404) by default; see Internal Admin Endpoints |
TIME_ZONE | yes | config/db.ts | Timezone used by the database connection for date/time column handling. Required for all database types - config/db.ts calls require_env("TIME_ZONE") unconditionally |
CONNECTION_STRING | yes | config/db.ts (dispatches to the SQLite or MySQL driver based on prefix) | Database connection string |
SESSION_STORE | no | routes/system/auth/session_store.ts | redis backs sessions with Redis (requires REDIS_URL). Otherwise (default sql) sessions use the database store matching your driver - SQLite or MySQL |
SMTP_HOST | for email | lib/smtp.ts | SMTP server hostname |
SMTP_PORT | for email | lib/smtp.ts | 587 for STARTTLS, 465 for implicit TLS |
SMTP_USERNAME | for email | lib/smtp.ts | SMTP auth username |
SMTP_PASSWORD | for email | lib/smtp.ts | SMTP auth password / API key |
SMTP_FROM | for email | lib/smtp.ts | Default From: address |
SQL_LOGGING | no | server.ts | Set to "true" to log every query to logs/sql.ndjson |
OPENROUTER_KEY | no | generator/openrouter.ts | OpenRouter key for AI-translated language files. Used when set and no higher-precedence provider is |
OPENROUTER_MODEL | no | generator/openrouter.ts | Override the OpenRouter model (e.g. deepseek/deepseek-chat) |
OLLAMA_URL | no | generator/ai-provider.ts | Local LLM endpoint for AI translation. Takes priority over all other providers when set |
OLLAMA_MODEL | with Ollama | generator/ai-provider.ts | Model name for the local Ollama provider. No default - the provider throws if it's unset |
HF_TOKEN | no | generator/ai-provider.ts | Hugging Face inference token; used when set and OPENROUTER_KEY is not |
REDIS_URL | for Redis features | lib/cache.ts, lib/middleware/rate_limit.ts, routes/system/auth/session_store_redis.ts | Redis connection string; defaults to redis://localhost:6379. Required when SESSION_STORE=redis, CACHE_ENABLED=true, or RATE_LIMITING=true |
CACHE_ENABLED | no | lib/cache.ts | Set to "true" to cache search_records query results in Redis (requires REDIS_URL - fails loud if missing). Silent no-op when unset. See Caching |
CACHE_MAX_BYTES | no | lib/cache.ts | Max serialized size of a single cached value. Larger results are skipped rather than cached. Defaults to 524288 (512 KB) |
CACHE_MAX_RECORDS | no | lib/cache.ts | Max record count for a cached query result; larger result sets are not cached. Defaults to 500 |
RATE_LIMITING | no | lib/middleware/rate_limit.ts | Set to "true" to enable sliding-window rate limiting (requires REDIS_URL - fails loud if missing). Disabled by default. See Rate Limiting |
RELOAD_SECRET | when internal endpoints are enabled | lib/admin/require_admin_auth.ts | A 32+-character secret required with INTERNAL_ADMIN_ENDPOINTS=true. Clients must send it in X-Reload-Secret for translation reload and rate-limit diagnostic endpoints |
REEWEB_PUBLISHER_URL | no | lib/publisher_signal/ | Signal endpoint of a paired ReeWeb Publisher (e.g. http://localhost:3011/api/render-signal). When set, successful mutations fire a best-effort re-render signal; unset disables it. See the ReeWeb Publisher |
MAX_UPLOAD_SIZE_MB | no | lib/middleware | Maximum upload size in megabytes. Defaults to 10 |
S3_ACCESS_KEY_ID | for uploads | lib/s3.ts | Access key for S3-compatible storage. Also accepts AWS_ACCESS_KEY_ID |
S3_SECRET_ACCESS_KEY | for uploads | lib/s3.ts | Secret key. Also accepts AWS_SECRET_ACCESS_KEY |
S3_ENDPOINT | for uploads | lib/s3.ts | Endpoint URL, when not using the split-style S3_HOSTNAME/S3_PORT/S3_PROTOCOL connection. Also accepts AWS_ENDPOINT |
S3_HOSTNAME | for uploads | lib/s3.ts | Hostname for split-style S3 connection (e.g. localhost). Takes precedence over S3_ENDPOINT when set |
S3_PORT | for uploads | lib/s3.ts | Port for split-style S3 connection (e.g. 8333 for MinIO) |
S3_PROTOCOL | for uploads | lib/s3.ts | Protocol for split-style S3 connection: "http" or "https" |
S3_IMAGE_BUCKET | for uploads | lib/s3.ts | Bucket name used for uploaded images via the image editor. Defaults to "images" |
S3_FILE_BUCKET | for uploads | lib/file_processor/, lib/bootstrap.ts | Bucket name used for uploaded documents via <file-upload> and the /system/files library; also the /files/ serving mount. Defaults to "files" |
S3_REGION | optional | lib/s3.ts | Region string; some providers require it. Also accepts AWS_REGION |
SERVER_NAME | yes | server.ts | The hostname of this server (e.g. localhost). Used for generating absolute URLs and links |
SITE_URL | yes | server.ts | The full base URL of the site (e.g. http://localhost:2338). Used wherever an absolute URL is needed |
STORAGE | no | lib/local_storage.ts, lib/server_helpers.ts | Storage backend: "local" to write files to disk, "s3" to use S3-compatible object storage. Defaults to "local" |
LOCAL_STORAGE_DIR | when STORAGE=local | lib/local_storage.ts | Directory path for local file storage (e.g. "../storage"). Sub-directories mirror S3 bucket names |
TRANSLATED_ROUTES | no | lib/middleware/set_locale.ts | Set to true to enable locale-prefixed URL routing (e.g. /sl-si/about). Enabled in the shipped .env.example |
is_s3_configured() in lib/s3.ts checks for the access key, secret, and endpoint together - leave any of the three blank and uploads fall back to writing under static/avatars/ instead. 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 render layer 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",
"start": "bun 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 server.ts --dev) side by side. bun start runs the production server (bun server.ts --prod); in production the systemd unit runs the same 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 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
├── 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 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 = ["sl-si", "en-us"] as const;
export const default_locale = "en-us";
export const locale_names: Record<string, string> = {
"en-us": "English",
"sl-si": "Slovenian",
};
export const locale_aliases: Record<string, string> = {};
Adding a new locale updates this configuration and adds rows to the translations table. 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 and dates:
export const IGNORE_TABLES = ["modules", "sessions", "email", "images", "files", "users", "translations"] as const;
export const MAINTENANCE_FIELDS = ["created_at", "updated_at"] as const;
export const DATE_SUFIXES = ["_on", "_by"] as const;
export const DATETIME_SUFIXES = ["_at"] as const;
export const IGNORE_INDEX_FIELDS = [
"display",
"option_display",
"option_text",
"search_text",
"hashed_password",
"previous_hashed_password",
] as const;
export const IGNORE_ORDER_FIELDS = ["option_display", "search_text", "hashed_password", "previous_hashed_password"] as const;
export const BOOLEAN_PREFIXES = ["is_", "has_", "can_"] as const;
export const MIN_PASSWORD_LENGTH = Bun.argv.includes("--dev") ? 1 : 8;
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 CONNECTION_STRING and exports it as db. It also reads TIME_ZONE unconditionally to configure timezone constants used by both SQLite and MySQL. The prefix of CONNECTION_STRING determines which timezone configuration is applied:
sqlite:→ uses UTC for dates,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 CONNECTION_STRING in .env. The reeman's "Set database type" option (bun reeman) flips it 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 | Local development with hot reload (dev orchestrator) |
bun start | bun server.ts --prod | Production server (also called by systemd) |
bun css:build | tailwindcss -i ./css/app.css -o ./static/app.css --minify | Production CSS build |
bun css:watch | tailwindcss -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 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": ["routes"],
"component_roots": ["components"],
"translation_provider": "db-export",
"translation_root": ".reepolee/i18n"
}
}
| Field | Value | Purpose |
|---|---|---|
project_family | "reepolee" | Selects the Reepolee project profile — determines how includes, components, and translations are resolved |
template_roots | ["routes"] | 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 | "db-export" | Tells the LSP that translations are exported from the database to JSON. Reepolee writes them to .reepolee/i18n/ at dev-server startup and on translation reload |
translation_root | ".reepolee/i18n" | Where the LSP reads per-locale JSON translation indexes for autocompletion and go-to-definition of {_ } / {- } / {@ } keys |
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 routes/ 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.
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.