Configuration

Introduction

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 languages, 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 language - bun reeman exposes each as a menu item that edits the relevant file (.env, the supported-languages 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       # language-prefixed URLs (e.g. /sl/about)
MAX_UPLOAD_SIZE_MB=10
RELOAD_SECRET="my-secret"    # protects the /__reload-translations endpoint

# --- 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)

# --- AI translation providers (pick one; see generator/ai-provider.ts)
# Precedence: Ollama > Gemini > OpenRouter > HF
GEMINI_API_KEY=...
GEMINI_MODEL=gemini-2.5-flash
# OPENROUTER_KEY=...
# OLLAMA_URL="http://localhost:11434"   # local LLM; takes priority if set
# HF_TOKEN="hf_..."                      # Hugging Face inference fallback

The S3 object-storage block (used only when STORAGE="s3") and the MCP and agent-mode ports live further down in the real file - see File Uploads.

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
MCP_SERVER_PORTnoscripts/mcp/project.tsPort the standalone MCP server listens on (it runs on its own port, separate from the app). .env.example ships 2400
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
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
GEMINI_API_KEYnogenerator/ai-provider.ts, generator/gemini.tsGoogle Gemini key for bun generator/resource --translate. The shipped default provider (.env.example sets GEMINI_API_KEY + GEMINI_MODEL)
GEMINI_MODELnogenerator/gemini.tsGemini model id (.env.example uses gemini-2.5-flash)
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_MODELnogenerator/ai-provider.tsModel name for the local Ollama provider (default gemma4:e4b)
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_SECRETnoserver.tsIf set, the /__reload-translations endpoint requires a matching X-Reload-Secret header. Generators and the queue worker use it to hot-reload translations without a restart
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_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_lang.tsSet to true to enable language-prefixed URL routing (e.g. /sl/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": "conc -n tw,dev -c yellow,blue --restart-tries -1 \"bun css:watch\" \"bun --hot --no-clear-screen server.ts --dev\"",
        "start": "bun server.ts --prod"
    }
}

bun dev runs the CSS watcher and the hot-reloading server (bun --hot server.ts --dev) side by side under concurrently. 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; creates a Bun SQL instance from CONNECTION_STRING
├── db_structure.ts           ← generator preferences
├── rate_limit.ts             ← per-scope rate limit rules (see Rate Limiting)
└── supported_languages.ts    ← language list and metadata

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_languages.ts

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

export const languages = ["en", "sl"] as const;
export const active_languages = ["sl", "en"] as const;
export const default_language = "sl";

export const language_names: Record<string, string> = {
    en: "English",
    sl: "Slovenian",
};

export const language_locales: Record<string, string> = {
    en: "en-US",
    sl: "sl-SI",
};

Adding a new language updates this configuration and adds rows to the translations table. The reeman Add language flow handles both steps; see Adding a New Language. The full mechanics are in Languages & 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", "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 = [
    "option_text",
    "search_text",
    "hashed_password",
    "previous_hashed_password",
] as const;
export const IGNORE_ORDER_FIELDS = ["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 devconc "bun css:watch" "bun --hot --no-clear-screen server.ts --dev"Local development with hot reload
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 --verboseWatching 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.

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.