Configuration
Complete reference for what.toml. All settings, defaults, and options in one place.
[server]
Server binding and request handling.
| Key | Type | Default | Description |
|---|---|---|---|
port | integer | 8085 | Port to listen on |
host | string | 127.0.0.1 | Host to bind to |
max_body_size | string | 10mb | Maximum request body size (kb, mb, gb) |
fetch_timeout | integer | 10 | External fetch timeout in seconds |
css | string | full | Framework stylesheet: full, minimal, or none |
[server]
port = 3000
host = "0.0.0.0"
max_body_size = "25mb"
fetch_timeout = 15
Framework CSS modes
css controls how much of what.css your app serves:
full(default) — the whole design system: reset, theme variables, utilities, and component styles. About 14 KB gzipped.minimal— reset, theme variables, and utility classes only. Component styles (buttons, cards, forms, navigation) are cut — roughly half the file. For when you style components yourself but want the utility layer.none— what.css is not injected at all. Bring your own CSS. The file stays available at/static/what.cssfor manual linking.
minimal and none are about shipping fewer bytes, not escaping specificity fights.
[session]
Server-side session storage and cookie settings.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable sessions |
store | string | sqlite | Storage backend: sqlite or cloudflare-kv |
cookie_name | string | w_session | Session cookie name |
max_age | integer | 604800 | Session lifetime in seconds (7 days) |
secure | boolean | true | Set Secure flag on cookie |
database | string | sessions.db | SQLite database file for sessions |
[session]
enabled = true
cookie_name = "my_session"
max_age = 86400 # 1 day
secure = false # set false for local HTTP development
database = "data/sessions.db"
[cache]
In-memory page and data cache.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable caching |
ttl | integer | 300 | Default TTL in seconds (5 minutes) |
[cache]
enabled = true
ttl = 60
[auth]
JWT-based authentication and route protection.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable auth middleware |
login_endpoint | string | — | Backend API URL for login |
logout_endpoint | string | — | Backend API URL for logout |
jwt_cookie_name | string | w_token | Cookie name for JWT |
jwt_secret | string | auto-generated | Secret for JWT validation |
login_path | string | /login | Redirect path for unauthenticated users |
after_login | string | / | Redirect path after login |
protected_paths | array | [] | Glob patterns requiring auth |
jwt_claims | array | ["id", "user_id", "email", "full_name"] | Claims to extract from JWT |
[auth]
enabled = true
login_endpoint = "https://api.example.com/auth/login"
logout_endpoint = "https://api.example.com/auth/logout"
jwt_secret = "${JWT_SECRET}"
login_path = "/login"
after_login = "/dashboard"
protected_paths = ["/dashboard/*", "/admin/**"]
jwt_claims = ["id", "email", "full_name", "role"]
See Authentication for usage details.
[database]
Database backend selection. When absent, SQLite at data/app.db is used automatically. Tables are created on first write — no schema definition needed.
| Key | Type | Default | Description |
|---|---|---|---|
type | string | sqlite | Database type: sqlite, d1, or supabase |
path | string | data/app.db | SQLite database file path (SQLite only) |
[database]
type = "sqlite" # or "d1", "supabase"
path = "data/app.db" # SQLite only
See Database for backend-specific setup (D1, Supabase).
[datasources.*]
Named datasources connect multiple external backends simultaneously. Each is accessed via the dsn: fetch prefix. See Data Sources for full usage.
| Key | Type | Description |
|---|---|---|
type | string | Required. One of: d1, supabase, sqlite, api |
url | string | Base URL (API type only, must start with http:// or https://) |
headers | table | HTTP headers (API type only) |
account_id | string | Cloudflare account ID (D1 type) |
api_token | string | Cloudflare API token (D1 type) |
d1_database_id | string | D1 database ID (D1 type) |
project_url | string | Supabase project URL (Supabase type) |
api_key | string | Supabase service role key (Supabase type) |
path | string | SQLite file path (SQLite type) |
[datasources.cloud]
type = "d1"
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "${CF_D1_DATABASE_ID}"
[datasources.cms]
type = "supabase"
project_url = "${SUPABASE_URL}"
api_key = "${SUPABASE_KEY}"
[datasources.inventory]
type = "api"
url = "https://api.example.com"
headers = { Authorization = "Bearer ${API_TOKEN}" }
${ENV_VAR} references in datasource config will cause the server to fail at startup. Make sure all referenced environment variables are set (or defined in .env).
[supabase]
Supabase configuration. Required when [database] type = "supabase".
| Key | Type | Description |
|---|---|---|
project_url | string | Supabase project URL (e.g., https://xxx.supabase.co) |
api_key | string | Supabase service_role key (NOT the anon key) |
[database]
type = "supabase"
[supabase]
project_url = "${SUPABASE_URL}"
api_key = "${SUPABASE_SERVICE_KEY}"
See Database for step-by-step Supabase setup.
[uploads]
File upload handling and storage.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable the upload endpoint |
provider | string | local | Storage backend: local or r2 |
directory | string | uploads | Upload directory (local only) |
max_size | string | 10mb | Maximum file size |
allowed_types | array | [] (all) | Allowed MIME types/extensions |
[uploads]
enabled = true
provider = "local"
directory = "uploads"
max_size = "25mb"
allowed_types = ["image/*", ".pdf", ".csv"]
See File Uploads for usage details.
[email]
Transactional email configuration. Choose SMTP or Resend API.
| Key | Type | Default | Description |
|---|---|---|---|
from | string | — | Sender email address (required) |
from_name | string | — | Sender display name |
template_dir | string | emails | Email template directory |
[email.smtp]
| Key | Type | Default | Description |
|---|---|---|---|
host | string | — | SMTP server hostname (required) |
port | integer | 587 | SMTP port |
username | string | — | SMTP username (supports ${ENV_VAR}) |
password | string | — | SMTP password (supports ${ENV_VAR}) |
[email.api]
| Key | Type | Default | Description |
|---|---|---|---|
provider | string | — | API provider: resend |
api_key | string | — | API key (supports ${ENV_VAR}) |
[email]
from = "[email protected]"
from_name = "My App"
[email.smtp]
host = "smtp.mailgun.org"
port = 587
username = "${SMTP_USER}"
password = "${SMTP_PASS}"
See Email for template syntax and form integration.
[cloudflare]
Shared credentials for Cloudflare services (D1, R2, Turnstile).
| Key | Type | Description |
|---|---|---|
account_id | string | Cloudflare account ID |
api_token | string | API token (default for all services) |
d1_database_id | string | D1 database ID |
r2_bucket | string | R2 bucket name |
r2_public_url | string | R2 public URL prefix |
turnstile_site_key | string | Turnstile site key (public) |
turnstile_secret_key | string | Turnstile secret key (server-side) |
[cloudflare]
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "your-d1-database-id"
r2_bucket = "my-uploads"
r2_public_url = "https://pub-abc123.r2.dev"
turnstile_site_key = "0x..."
turnstile_secret_key = "${CF_TURNSTILE_SECRET}"
[rate_limit]
Rate limiting for sensitive endpoints. Format: "requests/seconds".
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable rate limiting |
login | string | 5/60 | Login endpoint (5 per 60s) |
upload | string | 10/60 | Upload endpoint (10 per 60s) |
action | string | 30/60 | Form action endpoints (30 per 60s) |
[rate_limit]
enabled = true
login = "3/60" # 3 login attempts per minute
upload = "20/60" # 20 uploads per minute
action = "60/60" # 60 form submissions per minute
[redirects]
URL redirect rules. Supports exact matches and wildcard prefixes.
[redirects]
"/old-page" = "/new-page"
"/blog/2023/*" = "/archive"
"/legacy/**" = "/modern"
Redirects are applied before route matching. Wildcard patterns follow the same rules as protected_paths: * matches one level, ** matches all depths.
[deploy]
Deployment configuration, saved automatically by run-what deploy.
[deploy.ssh]
host = "77.42.22.50"
user = "root"
remote_dir = "/opt/what"
[deploy.docker]
image = "my-app"
registry = "ghcr.io/username"
Full Example
A complete what.toml showing all sections:
[server]
port = 8085
host = "127.0.0.1"
max_body_size = "10mb"
fetch_timeout = 10
[session]
enabled = true
cookie_name = "w_session"
max_age = 604800
secure = true
database = "sessions.db"
[cache]
enabled = true
ttl = 300
[auth]
enabled = true
login_endpoint = "https://api.example.com/auth/login"
logout_endpoint = "https://api.example.com/auth/logout"
jwt_secret = "${JWT_SECRET}"
jwt_cookie_name = "w_token"
login_path = "/login"
after_login = "/dashboard"
protected_paths = ["/dashboard/*", "/admin/**"]
jwt_claims = ["id", "email", "full_name", "role"]
[database]
type = "sqlite"
path = "data/app.db"
[uploads]
enabled = true
provider = "local"
directory = "uploads"
max_size = "10mb"
allowed_types = ["image/*", ".pdf"]
[email]
from = "[email protected]"
from_name = "My App"
[email.smtp]
host = "smtp.mailgun.org"
port = 587
username = "${SMTP_USER}"
password = "${SMTP_PASS}"
[cloudflare]
account_id = "${CF_ACCOUNT_ID}"
api_token = "${CF_API_TOKEN}"
d1_database_id = "your-d1-database-id"
r2_bucket = "my-uploads"
r2_public_url = "https://pub-abc123.r2.dev"
turnstile_site_key = "0x..."
turnstile_secret_key = "${CF_TURNSTILE_SECRET}"
# Supabase (alternative to D1)
# [supabase]
# project_url = "${SUPABASE_URL}"
# api_key = "${SUPABASE_SERVICE_KEY}"
# Named datasources (dsn: prefix in fetch directives)
# [datasources.external_api]
# type = "api"
# url = "https://api.example.com"
# headers = { Authorization = "Bearer ${API_TOKEN}" }
[rate_limit]
enabled = true
login = "5/60"
upload = "10/60"
action = "30/60"
[redirects]
"/old-blog" = "/blog"
"/legacy/*" = "/modern"
[deploy.ssh]
host = "77.42.22.50"
user = "root"
remote_dir = "/opt/what"
what.toml (or no file at all) uses sensible defaults for everything. Add sections only when you need to override the defaults.