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:

VariableRequiredRead byPurpose
PORTyesserver.tsThe port the HTTP server listens on
TEST_PORTnoserver.tsOverrides 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_USERNAMEnoroutes/system/auth/middleware.tsDefault 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_PORTnoserver.tsPort 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_SECRETnoroutes/system/auth/middleware.tsOptional 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_RENDERnoscripts/mcp/index.tsSet 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_MUTATIONSnoscripts/mcp/capabilities.tsSet 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_STRINGMySQL MCP inspectionscripts/mcp/db.tsA 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_ENDPOINTSnolib/admin/require_admin_auth.tsSet to true to enable the protected translation-reload and rate-limit diagnostic endpoints. They remain absent (404) by default; see Internal Admin Endpoints
TIME_ZONEyesconfig/db.tsTimezone 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_STRINGyesconfig/db.ts (dispatches to the SQLite or MySQL driver based on prefix)Database connection string
SESSION_STOREnoroutes/system/auth/session_store.tsredis backs sessions with Redis (requires REDIS_URL). Otherwise (default sql) sessions use the database store matching your driver - SQLite or MySQL
SMTP_HOSTfor emaillib/smtp.tsSMTP server hostname
SMTP_PORTfor emaillib/smtp.ts587 for STARTTLS, 465 for implicit TLS
SMTP_USERNAMEfor emaillib/smtp.tsSMTP auth username
SMTP_PASSWORDfor emaillib/smtp.tsSMTP auth password / API key
SMTP_FROMfor emaillib/smtp.tsDefault From: address
SQL_LOGGINGnoserver.tsSet to "true" to log every query to logs/sql.ndjson
OPENROUTER_KEYnogenerator/openrouter.tsOpenRouter key for AI-translated language files. Used when set and no higher-precedence provider is
OPENROUTER_MODELnogenerator/openrouter.tsOverride the OpenRouter model (e.g. deepseek/deepseek-chat)
OLLAMA_URLnogenerator/ai-provider.tsLocal LLM endpoint for AI translation. Takes priority over all other providers when set
OLLAMA_MODELwith Ollamagenerator/ai-provider.tsModel name for the local Ollama provider. No default - the provider throws if it's unset
HF_TOKENnogenerator/ai-provider.tsHugging Face inference token; used when set and OPENROUTER_KEY is not
REDIS_URLfor Redis featureslib/cache.ts, lib/middleware/rate_limit.ts, routes/system/auth/session_store_redis.tsRedis connection string; defaults to redis://localhost:6379. Required when SESSION_STORE=redis, CACHE_ENABLED=true, or RATE_LIMITING=true
CACHE_ENABLEDnolib/cache.tsSet 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_BYTESnolib/cache.tsMax serialized size of a single cached value. Larger results are skipped rather than cached. Defaults to 524288 (512 KB)
CACHE_MAX_RECORDSnolib/cache.tsMax record count for a cached query result; larger result sets are not cached. Defaults to 500
RATE_LIMITINGnolib/middleware/rate_limit.tsSet to "true" to enable sliding-window rate limiting (requires REDIS_URL - fails loud if missing). Disabled by default. See Rate Limiting
RELOAD_SECRETwhen internal endpoints are enabledlib/admin/require_admin_auth.tsA 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_URLnolib/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_MBnolib/middlewareMaximum upload size in megabytes. Defaults to 10
S3_ACCESS_KEY_IDfor uploadslib/s3.tsAccess key for S3-compatible storage. Also accepts AWS_ACCESS_KEY_ID
S3_SECRET_ACCESS_KEYfor uploadslib/s3.tsSecret key. Also accepts AWS_SECRET_ACCESS_KEY
S3_ENDPOINTfor uploadslib/s3.tsEndpoint URL, when not using the split-style S3_HOSTNAME/S3_PORT/S3_PROTOCOL connection. Also accepts AWS_ENDPOINT
S3_HOSTNAMEfor uploadslib/s3.tsHostname for split-style S3 connection (e.g. localhost). Takes precedence over S3_ENDPOINT when set
S3_PORTfor uploadslib/s3.tsPort for split-style S3 connection (e.g. 8333 for MinIO)
S3_PROTOCOLfor uploadslib/s3.tsProtocol for split-style S3 connection: "http" or "https"
S3_IMAGE_BUCKETfor uploadslib/s3.tsBucket name used for uploaded images via the image editor. Defaults to "images"
S3_FILE_BUCKETfor uploadslib/file_processor/, lib/bootstrap.tsBucket name used for uploaded documents via <file-upload> and the /system/files library; also the /files/ serving mount. Defaults to "files"
S3_REGIONoptionallib/s3.tsRegion string; some providers require it. Also accepts AWS_REGION
SERVER_NAMEyesserver.tsThe hostname of this server (e.g. localhost). Used for generating absolute URLs and links
SITE_URLyesserver.tsThe full base URL of the site (e.g. http://localhost:2338). Used wherever an absolute URL is needed
STORAGEnolib/local_storage.ts, lib/server_helpers.tsStorage backend: "local" to write files to disk, "s3" to use S3-compatible object storage. Defaults to "local"
LOCAL_STORAGE_DIRwhen STORAGE=locallib/local_storage.tsDirectory path for local file storage (e.g. "../storage"). Sub-directories mirror S3 bucket names
TRANSLATED_ROUTESnolib/middleware/set_locale.tsSet 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.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",
        "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_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 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:

ScriptRunsPurpose
bun devbun scripts/dev_run.tsLocal development with hot reload (dev orchestrator)
bun startbun server.ts --prodProduction server (also called by systemd)
bun css:buildtailwindcss -i ./css/app.css -o ./static/app.css --minifyProduction CSS build
bun css:watchtailwindcss -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 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": ["routes"],
    "component_roots": ["components"],
    "translation_provider": "db-export",
    "translation_root": ".reepolee/i18n"
  }
}
FieldValuePurpose
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:

  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.

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.