-
Notifications
You must be signed in to change notification settings - Fork 3
Configuration
Model Hotel is configured through environment variables (startup-only) and runtime database settings (changeable without restart).
Environment variables are read once at server startup and cannot be changed at runtime. The application loads them from a .env file (via godotenv) or from the process environment.
| Variable | Type | Default | Description |
|---|---|---|---|
MASTER_KEY |
string | - | Master encryption key for provider API keys. Used as input to Argon2id key derivation before AES-256-GCM encryption. Must be strong and kept secret. Rotating this key invalidates ALL encrypted provider API keys - they must be re-encrypted after rotation. Generate with openssl rand -base64 32. |
POSTGRES_PASSWORD |
string | - | PostgreSQL password. Required if DATABASE_URL is not set. Used to construct the connection string from POSTGRES_USER, POSTGRES_HOST, and POSTGRES_DB. Generate with openssl rand -hex 16. |
Note:
DATABASE_URLtakes precedence over thePOSTGRES_*components. IfDATABASE_URLis set, the other PostgreSQL variables are ignored.
| Variable | Type | Default | Description |
|---|---|---|---|
DATABASE_URL |
string | (constructed) | PostgreSQL connection string. If not set, constructed from POSTGRES_USER:POSTGRES_PASSWORD@POSTGRES_HOST:5432/POSTGRES_DB. E.g. postgres://user:pass@localhost:5432/modelhotel
|
POSTGRES_USER |
string | modelhotel |
PostgreSQL username (used if DATABASE_URL not set). |
POSTGRES_HOST |
string | db |
PostgreSQL host (used if DATABASE_URL not set). In Docker Compose, this is the db service name; use localhost for local dev. |
POSTGRES_DB |
string | modelhotel |
PostgreSQL database name (used if DATABASE_URL not set). |
PORT |
string | :8080 |
Server listen address inside the container. E.g. :8080
|
HOST_PORT |
string | 8081 |
Docker Compose deployment only. Port exposed on the host machine. Maps to container port 8080. Not read by the Go application - used exclusively by docker-compose.yml for port mapping. |
DATA_DIR |
string | ./data |
Directory for persistent data (admin token file, etc.). In Docker, mounted to /data. |
ADMIN_TOKEN |
string | (auto-generated) | Fixed admin token for API authentication. Auto-generated on first run if empty, displayed once in logs, then stored as SHA-256 hash in <DATA_DIR>/admin-token. Regenerate by deleting that file and restarting. Generate with openssl rand -hex 16. |
ALLOW_HTTP_PROVIDERS |
bool | false |
Allow HTTP (non-HTTPS) provider base URLs. Useful for local Ollama instances or testing with mock servers. |
RATE_LIMIT_ENABLED |
bool | true |
Hard kill-switch for rate limiting. When false, the rate-limiting middleware is always mounted but becomes a complete pass-through (no buckets allocated, no headers, no 429 responses). Cannot be overridden at runtime. |
RATE_LIMIT_IP_RPS |
float | 30 |
Per-IP requests per second for the token bucket rate limiter. Clamped to 0β10000. |
RATE_LIMIT_IP_BURST |
int | 60 |
Per-IP maximum burst size for the token bucket. Clamped to 1β10000. |
MAX_REQUEST_SIZE |
int | 10485760 |
Maximum request body size in bytes. Clamped to 1KBβ100MB. Default is 10 MB. |
CORS_ORIGINS |
string (comma-separated) | http://localhost:5173,http://localhost:8081 |
Comma-separated list of allowed CORS origins. Must include the scheme (e.g. http://). Wildcard * is explicitly rejected (incompatible with credentials=true). |
ALLOWED_PROVIDER_HOSTS |
string (comma-separated) | (empty) | Comma-separated list of additional allowed provider hosts. Built-in provider hosts are always allowed regardless of this setting. Hosts listed here bypass loopback blocking, so localhost can be added for local Ollama. E.g. localhost,api.example.com
|
TRUSTED_PROXIES |
string (comma-separated CIDR) | (none) | Comma-separated list of CIDR ranges for correct client IP detection behind reverse proxies. Only needed when running behind nginx, Cloudflare, etc. E.g. 10.0.0.0/8,172.16.0.0/12
|
DATABASE_MAX_CONNS |
int | 25 |
Maximum database connection pool size. Clamped to 1β1000. |
DATABASE_MIN_CONNS |
int | 5 |
Minimum database connection pool size. Clamped to 1β1000. Cannot exceed DATABASE_MAX_CONNS. |
MODELSDEV_ENABLED |
bool | true |
Enable loading models.dev catalogue at startup for model enrichment data. |
DEBUG_LOG |
bool | false |
Enable debug-level structured logging. Logs are written to stdout in JSON format. |
The following provider hosts are always allowed as provider base_url values, regardless of ALLOWED_PROVIDER_HOSTS:
api.openai.comapi.nano-gpt.comapi.z.aiapi.deepseek.comapi.anthropic.comollama.comopencode.aiapi.x.aigenerativelanguage.googleapis.comapi.cohere.comapi.cohere.aiopenrouter.ai
These correspond to the providers detected by DetectProviderType in internal/provider/discovery.go.
-
MASTER_KEYis never used directly as an AES key. It is fed through Argon2id key derivation (per-provider random salt in v2) to produce the 256-bit AES key. See Security for details. -
ADMIN_TOKENis stored as a SHA-256 hash. Legacy plaintext tokens are automatically migrated to hashed format on first validation. -
RATE_LIMIT_ENABLEDis a hard kill-switch - whenfalse, the rate-limiting middleware is always mounted but becomes a complete pass-through (no buckets, no headers, no 429s). The DB settingrate_limit_enabledhas no effect when the env var isfalse. -
ALLOWED_PROVIDER_HOSTSis primarily for permitting non-standard hosts (loopback addresses for Ollama, custom provider endpoints). Built-in provider hosts never need to be listed here. -
TRUSTED_PROXIESis only needed when running behind a reverse proxy (e.g. nginx, Cloudflare). It controls which IPs are trusted to sendX-Forwarded-Forheaders for accurate client IP logging and rate limiting. -
DATABASE_MAX_CONNSandDATABASE_MIN_CONNSare clamped to the range 1β1000.DATABASE_MIN_CONNScannot exceedDATABASE_MAX_CONNS. -
CORS_ORIGINSexplicitly rejects*wildcard - it is incompatible withcredentials=true(CORS spec forbids it) and would silently break auth.
These settings are stored in the settings table and can be changed at runtime via the Settings UI or the PUT /api/settings endpoint - no restart required. Changes take effect immediately (within 30 seconds of cache TTL at most, or instantly via the subscription notification system).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/settings |
Returns all settings as a JSON key-value map. Requires admin token auth. |
PUT |
/api/settings |
Updates one or more settings. Body: {"key": "value", ...}. Max 50 keys per request. Requires admin token auth. |
| Setting | Type | Default | Description | Valid Values / Range |
|---|---|---|---|---|
discovery_interval |
duration string | 6h |
Model auto-discovery interval. Set to 0 to disable periodic discovery entirely. |
30m, 1h, 6h, 24h, 0
|
discovery_on_startup |
bool string | true |
Whether to run model discovery automatically on server startup. Skipped if last discovery was within 5 minutes. |
true, false
|
discovery_on_provider_create |
bool string | true |
Whether to trigger model discovery when a new provider is created via the API. Note: This setting is checked client-side - the frontend reads it and decides whether to trigger discovery after provider creation; it is not enforced server-side. |
true, false
|
log_retention |
duration string | (empty) | How long to keep request logs. Empty or unrecognised values = keep forever. Cleanup runs hourly. |
1h, 1d/24h, 1w/168h, 1m/720h, (empty) |
stale_request_timeout |
duration string | 30m |
Timeout for marking in-progress request logs as failed. Rows stuck in pending or streaming state longer than this are marked failed. |
30m, 1h, etc. |
request_timeout |
duration string | 1m |
Per-request timeout duration. Base timeout for non-streaming requests. Streaming requests use 10x this value. |
30s, 1m, 5m, 10m
|
failover_on_rate_limit |
bool string | true |
Whether to failover to the next provider when an upstream returns HTTP 429. 5xx errors always trigger failover. |
true, false
|
circuit_breaker_enabled |
bool string | true |
Enable per-provider circuit breaker for hotel/ failover routes. When open, the provider is skipped during failover selection. |
true, false
|
circuit_breaker_threshold |
int | 5 |
Consecutive failures before a provider's circuit opens. | 1β100 |
circuit_breaker_cooldown |
duration string | 60s |
Duration an open circuit stays open before transitioning to half-open. |
30s, 60s, 120s, etc. |
rate_limit_enabled |
bool string | true |
Runtime toggle for rate limiting. Overridden by RATE_LIMIT_ENABLED env var - if env var is false, this setting has no effect. |
true, false
|
rate_limit_ip_enabled |
bool string | true |
Runtime toggle for per-IP rate limiting. Only effective when RATE_LIMIT_ENABLED=true. |
true, false
|
rate_limit_ip_rps |
float | 30 |
Per-IP requests per second. Set to 0 for unlimited per-IP rate. |
0β10000 |
rate_limit_ip_burst |
int | 60 |
Per-IP burst size for token bucket. | 1β10000 |
rate_limit_rps |
float | 10 |
Per-virtual-key requests per second. Set to 0 to disable per-key rate limiting (makes every bucket unlimited). |
0β10000 |
rate_limit_burst |
int | 20 |
Maximum burst bucket size per virtual key. | 1β10000 |
rate_limit_max_wait_ms |
int | 200 |
Maximum wait time (ms) in the rate-limiter queue before rejecting with 429. Shared by both per-IP and per-key limiters. | 0β10000 |
key_cache_ttl |
duration string | 10m |
Provider key decryption cache TTL. Controls how long decrypted provider API keys are held in memory before re-derivation. | (varies) |
The rate limiting system has two layers, both using token buckets (backed by golang.org/x/time/rate):
Per-IP rate limiting (DoS protection, always-on when enabled):
- Applied before authentication, before the per-key limiter
-
rate_limit_ip_rpscontrols the per-IP refill rate (default 30) -
rate_limit_ip_burstcontrols the per-IP maximum burst size (default 60) -
rate_limit_ip_enabledtoggles this layer at runtime (requiresRATE_LIMIT_ENABLED=true) - Independent bucket per client IP address
Per-virtual-key rate limiting (usage control):
- Each key gets its own independent bucket
-
rate_limit_rpscontrols the refill rate (tokens per second, default 10) -
rate_limit_burstcontrols the maximum bucket size (default 20) - Setting
rate_limit_rps=0makes every bucket unlimited (no per-key rate limiting)
Shared settings:
-
rate_limit_max_wait_ms(default 200) - maximum time a request waits in the rate-limiter queue before being rejected with 429. Applies to both per-IP and per-key limiters. - The
RATE_LIMIT_ENABLEDenvironment variable is a hard kill-switch - whenfalse, both layers are always mounted but become a complete pass-through (no buckets, no headers, no 429s) - When rate limiting is re-enabled after being disabled, all existing buckets are reset to ensure fresh state
- Unused buckets are cleaned up by a periodic cleanup task that runs every 5 minutes and removes entries that have been idle for more than 10 minutes
When a request is rate-limited, the response includes:
-
Retry-After: <seconds>- When the client can retry -
X-RateLimit-Limit: <rate>- The refill rate -
X-RateLimit-Remaining: <tokens>- Remaining tokens in the bucket -
X-RateLimit-Burst: <burst>- The burst capacity -
X-RateLimit-Scope: <ip|key>- Indicates whether the rate limit applies toip(per-IP) orkey(per-virtual-key)
User preferences are stored in localStorage (client-side only, never sent to the server):
| Key | Description |
|---|---|
adminToken |
Admin authentication token (used for API calls) |
theme |
dark/light |
accentColor |
Hex color string |
uiStyle |
cyber-terminal, glassmorphism-lite, or default |
toastPosition |
Toast notification position |
toastTimeout |
Toast display duration (ms) |
persistChat |
Whether to persist chat state across sessions |
persistConversation |
Whether to persist conversation state |
persistArena |
Whether to persist arena state and history |
sidebarChatSubMode |
chat/conversation |
sidebarArenaSubMode |
competition/compare |
sidebarLogsSubMode |
request/app |
sidebarQuotaDisabled |
Whether to hide the quotas pill in sidebar (inverted: true = hidden) |
sidebarQuotaRefreshMin |
Sidebar quota refresh interval in minutes |
dashboardRefreshSec |
Dashboard refresh interval in seconds |
The Settings page has 8 collapsible sections:
Backend settings: discovery_interval, discovery_on_startup, discovery_on_provider_create
-
UI Style:
cyber-terminal,glassmorphism-lite- stored in localStorageuiStyle(note:clean-saasis not available) -
Theme:
dark/light- stored in localStoragetheme -
Accent Color: 10 preset swatches + custom hex picker - stored in localStorage
accentColor
Note:
theme,ui_style, andaccent_colorare not in the backendAllowedSettings- they are localStorage-only and cannot be set viaPUT /api/settings.
-
Position: 6-position visual picker - stored in localStorage
toastPosition -
Auto-dismiss: 1sβ15s slider - stored in localStorage
timeout
-
Show Quotas Pill: Toggle - stored in localStorage
sidebarQuotaDisabled(inverted:true= hidden) -
Refresh Interval: 1/2/5/10/15/30 minutes or Disabled - stored in localStorage
sidebarQuotaRefreshMin
-
Refresh Interval: 10s/30s/1m/2m/5m/10m/Disabled - stored in localStorage
dashboardRefreshSec
Backend settings: log_retention, stale_request_timeout
Also includes purges for request logs and app logs.
- Session Persistence: Toggle for chat, arena, and conversation state across page reloads
- Arena History: Save match history toggle, limit (10/25/50/100), clear button
- Cache & Resets: Clear provider quota cache, reset dismissed error banners
Backend settings: rate_limit_enabled, rate_limit_rps, rate_limit_burst, rate_limit_ip_enabled, rate_limit_ip_rps, rate_limit_ip_burst, rate_limit_max_wait_ms
Settings page - showing the 8 collapsible sections: Model Discovery, Appearance, Toast, Sidebar Quotas, Dashboard Refresh, Logging, Data Storage, and Rate Limiting.
Settings page - Appearance section expanded, showing UI Style cards (cyber-terminal, glassmorphism-lite), Theme toggle, and Accent Color picker.
Settings page - Rate Limiting section expanded, showing the enable toggle, RPS selector, and Burst selector.
The docker-compose.yml sets up the following services:
| Configuration | Value | Description |
|---|---|---|
| Build | . |
Builds from the root Dockerfile
|
| Ports | ${HOST_PORT:-8081}:8080 |
Maps host port (default 8081) to container port 8080 |
| Environment | See below | Environment variables passed to the container |
| Volumes | ./.data:/data |
Persistent data storage (admin token, etc.) |
| Volumes |
/var/run/docker.sock:/var/run/docker.sock:ro (commented out by default)
|
Read-only Docker socket access for container stats in sidebar |
| Depends on |
db (healthy) |
Waits for PostgreSQL to be ready |
Environment variables (docker-compose.yml):
environment:
- MASTER_KEY=${MASTER_KEY:?MASTER_KEY must be set in .env}
- POSTGRES_USER=${POSTGRES_USER:-modelhotel}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
- POSTGRES_HOST=db
- POSTGRES_DB=${POSTGRES_DB:-modelhotel}
- ADMIN_TOKEN=${ADMIN_TOKEN:-}
- ALLOW_HTTP_PROVIDERS=false
- DATA_DIR=/data
- RATE_LIMIT_ENABLED=true
- DEBUG_LOG=false
- CORS_ORIGINS=http://localhost:5173,http://localhost:${HOST_PORT:-8081}
- ALLOWED_PROVIDER_HOSTS=| Configuration | Value | Description |
|---|---|---|
| Image | postgres:16-alpine |
PostgreSQL 16 on Alpine Linux |
| Ports | 5432:5432 |
Exposed for local development |
| Environment |
POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
|
Database credentials |
| Volumes | ./.data/pgdata:/var/lib/postgresql/data |
Persistent database storage |
| Healthcheck | pg_isready -U ${POSTGRES_USER} |
Checks database readiness every 5s |
git clone https://github.com/hugalafutro/model-hotel.git
cd model-hotel
cp .env.example .env
nano .env # set a strong MASTER_KEY and POSTGRES_PASSWORD
docker compose up --buildGet the admin token:
docker compose logs app | grep "ADMIN_TOKEN="If you lose the token, delete .data/admin-token and restart to generate a new one.
You can also set a fixed admin token via the ADMIN_TOKEN environment variable in your .env file.
The Dockerfile uses a multi-stage build:
-
Base:
node:20-alpine - Package manager: pnpm (via corepack)
-
Working directory:
/app/web -
Build command:
pnpm run build -
Output:
dist/directory (embedded in backend)
-
Base:
golang:1.25-alpine -
Working directory:
/app -
Build command:
go build -o server ./cmd/server/ -
Embeds: Frontend
dist/intocmd/server/static/
-
Base:
alpine:latest -
Dependencies:
ca-certificates,postgresql-client -
Working directory:
/app -
Binary:
./server -
Exposed port:
8080 -
Healthcheck:
wget --spider http://localhost:8080/health(30s interval, 40s start period) -
CMD:
["./server"]
| Path | Source | Purpose |
|---|---|---|
/app/server |
Backend binary | Main application executable |
/app/web/dist/ |
Frontend build | Static assets served by the backend |
/app/migrations/ |
DB migrations | Reference copy (migrations are embedded in binary) |
Template file showing all available environment variables:
# Required: Generate strong secrets before deploying
MASTER_KEY=
POSTGRES_PASSWORD=
# PostgreSQL (used if DATABASE_URL not set)
POSTGRES_USER=modelhotel
POSTGRES_HOST=db
POSTGRES_DB=modelhotel
# Server configuration
PORT=:8080
HOST_PORT=8081 # Docker Compose only - not read by the Go application
# DISCOVERY_INTERVAL=30m # DB setting only - set via PUT /api/settings, not read from env
DATA_DIR=./data
ADMIN_TOKEN=
# Feature flags
ALLOW_HTTP_PROVIDERS=false
RATE_LIMIT_ENABLED=true
RATE_LIMIT_IP_RPS=30
RATE_LIMIT_IP_BURST=60
MAX_REQUEST_SIZE=10485760
CORS_ORIGINS=http://localhost:5173,http://localhost:8081
ALLOWED_PROVIDER_HOSTS=
TRUSTED_PROXIES=
DATABASE_MAX_CONNS=25
DATABASE_MIN_CONNS=5
MODELSDEV_ENABLED=true
DEBUG_LOG=false| Category | Count | Runtime Changeable |
|---|---|---|
| Environment Variables | 22 | No (restart required) |
| Database Settings | 18 | Yes (via API/UI) |
| Frontend localStorage | 14 | Yes (client-side only) |
Key Architecture Points:
-
Environment variables are loaded once at startup via
godotenv.Load()and theconfig.Load()function. -
Database settings use a 30-second cache with change notifications via
Subscribe()for immediate updates. -
Rate limiting has a hard kill-switch (
RATE_LIMIT_ENABLEDenv var) that completely disables the middleware whenfalse. -
Provider host validation always allows built-in providers;
ALLOWED_PROVIDER_HOSTSis only for custom/local providers. - Admin token is auto-generated on first run and stored as a SHA-256 hash.
Last synced from hugalafutro/model-hotel@b0da0d8 on 2026-07-08 23:42 UTC. Edit these pages under wiki/ (and images under docs/screenshots/) in the main repo, not here.
- π¨ Home
- βοΈ Configuration
- π Development
- π Virtual Keys
- π₯ Multi-User
- π API Reference
- π Request Logging
- π Model Discovery
- π Failover and Hotel Routing
- π Alerting
- π¨ High Availability