-
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_URL takes precedence over the POSTGRES_* components. If DATABASE_URL is 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 CIDR ranges for trusted reverse proxies (e.g. 10.0.0.0/8,172.16.0.0/12). When set, X-Forwarded-For headers from these IPs are trusted for rate limiting and request logging. This controls inbound trust only; it is unrelated to outbound SSRF protection (see KNOWN_PROXIES). |
KNOWN_PROXIES |
string (comma-separated CIDR) | (none) | Comma-separated CIDR ranges for internal LLM servers on private networks (e.g. 10.0.0.0/8,192.168.1.0/24). IPs within these CIDRs bypass the SSRF protection (SafeDialer private-IP blocking) so the proxy can reach self-hosted providers like Ollama or KoboldCPP running on private subnets, while still blocking all other private/loopback addresses. Unlike ALLOWED_PROVIDER_HOSTS (which allows by hostname and bypasses all SSRF checks), this operates at the network/CIDR level and only bypasses the private-IP block. |
WEBAUTHN_RP_ID |
string | (empty) | Relying Party ID for WebAuthn/FIDO2 passkey authentication (typically your domain, e.g. example.com). When empty, passkey login is disabled. When set, users can register and log in with passkeys (Touch ID, Windows Hello, YubiKey, etc.) alongside the admin token. |
WEBAUTHN_RP_DISPLAY_NAME |
string | Model Hotel |
Display name for the WebAuthn relying party, shown in the browser's passkey dialog. |
WEBAUTHN_RP_ORIGINS |
string (comma-separated) | (falls back to CORS_ORIGINS) |
Comma-separated list of allowed origins for WebAuthn registration/authentication (e.g. https://example.com). Falls back to CORS_ORIGINS if empty, then to http://localhost:<port>. |
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_PROXIEScontrols which reverse proxies are trusted for inbound request metadata (X-Forwarded-For).KNOWN_PROXIEScontrols which private CIDRs are allowed for outbound connections to self-hosted LLM providers. They serve opposite directions and should not be confused. -
ALLOWED_PROVIDER_HOSTSbypasses all SSRF protections (both URL validation and SafeDialer IP checks). Use it for specific hostnames. -
KNOWN_PROXIESpermits entire CIDR ranges but only bypasses the private-IP block in the SafeDialer (provider URL validation still applies). If your internal LLM server has a stable hostname, useALLOWED_PROVIDER_HOSTS. If it sits on a subnet with dynamic hostnames, useKNOWN_PROXIES. - Self-hosted providers detected via port heuristics (KoboldCPP on 5001, LMStudio on 1234, local Ollama on 11434) are not in the built-in host allowlist; add them to
ALLOWED_PROVIDER_HOSTSorKNOWN_PROXIESas needed. -
WEBAUTHN_RP_IDis empty by default, meaning passkey login is disabled. Set it to your domain to enable FIDO2/WebAuthn passkey authentication.WEBAUTHN_RP_ORIGINSfalls back toCORS_ORIGINSand then tohttp://localhost:<port>. -
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) |
ttft_timeout |
duration string | 1m0s |
Time-to-first-token probe timeout for streaming requests. After the upstream responds 200, the proxy reads ahead to confirm the first token arrives before committing the stream to the client. If the provider fails to produce a token within this timeout, the request fails over to the next provider. Set to 0s to disable (immediate stream commit). |
0s, 30s, 1m0s, etc. |
stream_stall_timeout |
duration string | 30s |
Maximum silence during streaming before the connection is terminated and the circuit breaker records a failure. After 50 chunks the effective timeout is multiplied by 3 to tolerate tool-call pauses and long reasoning chains. Set to 0s to disable the watchdog. |
0s, 10s, 30s, 1m0s, etc. |
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 11 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
Backend settings: ttft_timeout, stream_stall_timeout
-
TTFT Timeout: Duration string (default
1m0s) - time-to-first-token probe timeout for streaming requests. Set to0sto disable. -
Stream Stall Timeout: Duration string (default
30s) - maximum silence during streaming before termination. After 50 chunks the effective timeout is multiplied by 3.
Backend settings: circuit_breaker_threshold, circuit_breaker_cooldown, circuit_breaker_half_open_requests
- Failure Threshold: Number of consecutive failures before circuit opens (default 5).
-
Cooldown Duration: Duration an open circuit stays open before transitioning to half-open (default
60s). - Half-Open Requests: Number of test requests allowed in half-open state (default 1).
- Backup: Download a PostgreSQL dump of the database
- Restore: Upload a previously downloaded backup file to restore
Settings page - showing the 11 collapsible sections: Model Discovery, Appearance, Toast, Sidebar Quotas, Dashboard Refresh, Logging, Data Storage, Proxy, Rate Limiting, Circuit Breaker & Failover, and Database Backup.
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.
Settings page - Proxy section, showing TTFT Timeout and Stream Stall Timeout settings.
Settings page - Circuit Breaker & Failover section, showing cooldown, failure threshold, and half-open request settings.
Settings page - Database Backup section, showing backup and restore controls.
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=
KNOWN_PROXIES=
DATABASE_MAX_CONNS=25
DATABASE_MIN_CONNS=5
MODELSDEV_ENABLED=true
DEBUG_LOG=false
WEBAUTHN_RP_ID=
WEBAUTHN_RP_DISPLAY_NAME=Model Hotel
WEBAUTHN_RP_ORIGINS=| Category | Count | Runtime Changeable |
|---|---|---|
| Environment Variables | 26 | No (restart required) |
| Database Settings | 20 | 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