3.1 Configuration File Format
Kalos reads its configuration from a JSON file. Pass the path with -c/--config
(canonical), or as a positional argument:
kalosd --config config.json # canonical
kalosd config.json # positional (equivalent)
If both are given, --config wins. The path must exist, or kalosd exits with a
clear error before booting.
The configuration file MUST be valid JSON. Comments are not supported. All paths in the configuration file MUST be absolute paths. Relative paths are resolved relative to the working directory, which is unreliable across deployment environments.
The configuration tree is organized into top-level sections, each consumed by a specific plugin or the kernel itself. A plugin reads its own section and ignores all others. The kernel does not validate section contents — each plugin is responsible for validating its own configuration and logging errors for missing or invalid keys.
3.2 Logging Configuration
The logging section configures the kernel’s logging facility (spdlog).
{
"logging": {
"level": "info",
"format": "text",
"file": {
"path": "/var/log/kalosd/server.log",
"max_size_mb": 10,
"max_files": 3
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
level | string | "info" | Minimum log level: trace, debug, info, warn, error, critical |
format | string | "text" | Output format: "text" (human-readable) or "json" (structured) |
file.path | string | none | Path to the log file. If omitted, logs are written to stdout only. |
file.max_size_mb | integer | 10 | Maximum log file size in megabytes before rotation. |
file.max_files | integer | 3 | Number of rotated log files to retain. |
When file.path is specified, the logger writes to both stdout and the log file. Log rotation occurs when the file exceeds max_size_mb. Rotated files are named {path}.1, {path}.2, etc., up to max_files.
The level setting is global. All plugins share the same log level. A plugin SHOULD prefix its log messages with its name (e.g., kalos_adl: domain bootstrapped).
3.3 Transport Configuration
The transport.rest section configures the HTTP/HTTPS server.
3.3.1 Basic HTTP
{
"transport": {
"rest": {
"host": "127.0.0.1",
"port": 8080
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
host | string | "127.0.0.1" | Bind address. Use "0.0.0.0" to accept connections from all interfaces. |
port | integer | 8080 | TCP port. |
3.3.2 TLS Configuration
{
"transport": {
"rest": {
"host": "127.0.0.1",
"port": 8443,
"tls": {
"cert_file": "/path/to/server.crt",
"key_file": "/path/to/server.key",
"redirect_http": true,
"redirect_http_port": 8080
}
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
tls.cert_file | string | required | Path to the TLS certificate file (PEM format). |
tls.key_file | string | required | Path to the TLS private key file (PEM format). |
tls.redirect_http | boolean | false | If true, start an HTTP listener that returns 301 redirects to the HTTPS endpoint. |
tls.redirect_http_port | integer | 8080 | Port for the HTTP redirect listener. Only used when redirect_http is true. |
When tls is present, the transport listens on HTTPS. The cert_file and key_file MUST be readable by the server process. Self-signed certificates are accepted — the server does not validate its own certificate. Client certificate validation is not supported in this version.
If redirect_http is true, the transport opens a second listener on redirect_http_port that responds to all requests with HTTP 301 redirecting to https://{host}:{port}{path}.
3.3.3 CORS Configuration
{
"transport": {
"rest": {
"cors": {
"origins": "*"
}
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
cors.origins | string | "*" | Allowed origins for CORS preflight responses. Use "*" to allow all origins, or a comma-separated list of specific origins. |
When a CORS preflight request (OPTIONS) is received, the transport responds with the configured Access-Control-Allow-Origin header. The Access-Control-Allow-Methods header includes all methods the route supports. The Access-Control-Allow-Headers header includes Authorization, Content-Type, and any custom headers declared by plugins.
3.3.4 Security Headers
{
"transport": {
"rest": {
"security": {
"hsts_max_age": 31536000,
"x_frame_options": "DENY"
}
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
security.hsts_max_age | integer | 0 | Strict-Transport-Security max-age in seconds. Set to 0 to omit the header. 31536000 is one year. |
security.x_frame_options | string | "DENY" | X-Frame-Options header value: "DENY", "SAMEORIGIN", or omit to disable. |
When TLS is enabled and hsts_max_age is greater than zero, the transport adds the Strict-Transport-Security header to all HTTPS responses. The X-Content-Type-Options: nosniff header is always included on HTTPS responses.
3.4 Authentication Configuration
The authd section configures the authentication plugin.
{
"authd": {
"jwt": {
"algorithm": "HS256",
"secret": "your-secret-key-CHANGE-IN-PRODUCTION",
"issuer": "kalos",
"access_token_ttl_seconds": 900,
"refresh_token_ttl_seconds": 604800
},
"password": {
"algorithm": "argon2id",
"disable_checking": false
},
"store": "default",
"prefix": "/api/v1"
}
}
3.4.1 JWT Configuration
| Key | Type | Default | Description |
|---|---|---|---|
jwt.algorithm | string | "HS256" | JWT signing algorithm. Only "HS256" is supported in this version. |
jwt.secret | string | required | HMAC secret key. MUST be at least 32 characters in production. |
jwt.issuer | string | "kalos" | Value of the iss claim in generated tokens. |
jwt.access_token_ttl_seconds | integer | 900 | Access token lifetime (default: 15 minutes). |
jwt.refresh_token_ttl_seconds | integer | 604800 | Refresh token lifetime (default: 7 days). |
A legacy format is also accepted for backward compatibility:
{
"auth": {
"jwt_secret": "your-secret-key",
"token_expiry_minutes": 60,
"refresh_expiry_days": 7
}
}
When both authd.jwt and auth sections are present, authd.jwt takes precedence. The legacy format SHOULD NOT be used for new configurations.
3.4.2 Password Policy
| Key | Type | Default | Description |
|---|---|---|---|
password.algorithm | string | "argon2id" | Password hashing algorithm. Only "argon2id" is supported. |
password.disable_checking | boolean | false | If true, skip password complexity validation. For development only. |
When disable_checking is false, passwords MUST meet the following requirements: minimum 8 characters, at least one uppercase letter, one lowercase letter, one digit, and one special character. Passwords that fail these requirements are rejected with HTTP 422.
3.4.3 Auth Plugin Settings
| Key | Type | Default | Description |
|---|---|---|---|
store | string | "default" | Name of the store plugin instance to use for user/session data. |
prefix | string | "/api/v1" | URL prefix for auth endpoints. Auth routes are mounted at {prefix}/auth/*. |
3.5 Store Configuration
3.5.1 SQLite
{
"store": {
"sqlite": {
"path": "/path/to/database.db",
"name": "default",
"pool_size": 4
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
path | string | required | Path to the SQLite database file. Created if it does not exist. |
name | string | "default" | Store instance name, referenced by other plugins via ICore::get_store(name). |
pool_size | integer | 4 | Number of connection pool entries. |
The SQLite store applies the following PRAGMAs on connection:
journal_mode = WAL(write-ahead logging for concurrent reads)foreign_keys = ON(enforce foreign key constraints)busy_timeout = 5000(5-second wait on lock contention)
3.5.2 PostgreSQL
{
"store": {
"postgres": {
"conninfo": "host=localhost port=5432 dbname=myapp user=postgres password=postgres",
"name": "default",
"pool_size": 4
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
conninfo | string | required | PostgreSQL connection string (libpq format). |
name | string | "default" | Store instance name. |
pool_size | integer | 4 | Number of connections in the pool. |
The conninfo string supports all libpq connection parameters, including sslmode=require for TLS connections.
Only one store section (sqlite or postgres) SHOULD be present. If both are present, the behavior is implementation-defined.
3.6 ADL Configuration
The adl section configures the ADL engine plugin.
{
"adl": {
"domain_file": "/path/to/domain.yaml",
"db_path": "/path/to/adl-registry.db",
"app_db_path": "/path/to/app-data.db"
}
}
| Key | Type | Default | Description |
|---|---|---|---|
domain_file | string | none | Path to the ADL domain YAML file. If present, the domain is bootstrapped from this file on startup. If omitted, domains must be submitted via the POST /api/v1/domains endpoint. |
db_path | string | auto | Path to the ADL registry database (domain metadata, migration history, snapshots). When using SQLite, defaults to a file in the same directory as the store database. When using PostgreSQL, registry tables are created in the same database. |
app_db_path | string | auto | Path to the application data database (domain resource tables). When using SQLite, defaults to a file in the same directory as the store database. When using PostgreSQL, resource tables are created in the same database. |
When using PostgreSQL, db_path and app_db_path are ignored — all tables are created in the database specified by the store configuration. The separate path settings exist for SQLite, where the registry, the application data, and the authd user store MAY reside in separate database files.
3.7 Seeder Configuration
The seeder section configures the data seeding plugin.
{
"seeder": {
"enabled": true,
"run_on_start": true,
"seed_file": "/path/to/seed.yaml"
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | If false, the seeder plugin is loaded but does not execute. |
run_on_start | boolean | false | If true, execute the seed manifest during the seeder’s on_start() phase. |
seed_file | string | required | Path to the seed YAML manifest. |
When run_on_start is true, the seeder executes every time the server starts. Seed execution is idempotent — records that already exist (409 responses from create actions) are logged as “skipped” and do not cause errors. See §32 for the seed file format.
3.8 Rate Limiting Configuration
The rate_limit section configures request rate limiting at the transport level.
{
"rate_limit": {
"enabled": true,
"max_requests": 60,
"window_seconds": 60,
"auth_endpoints": {
"max_attempts": 10,
"window_seconds": 60
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | If true, enforce rate limits on incoming requests. |
max_requests | integer | 60 | Maximum number of requests per client per window. |
window_seconds | integer | 60 | Duration of the rate limiting window in seconds. |
auth_endpoints.max_attempts | integer | 10 | Maximum authentication attempts (login, register) per client per window. |
auth_endpoints.window_seconds | integer | 60 | Window duration for auth endpoint rate limiting. |
Clients are identified by IP address. When a client exceeds the rate limit, the transport returns HTTP 429 with the following headers:
| Header | Value |
|---|---|
X-RateLimit-Limit | The configured max_requests value |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until the window resets |
Auth endpoints (/auth/login, /auth/register) have a separate, typically stricter, rate limit to mitigate brute-force attacks.
3.9 Email Configuration
The email section configures SMTP for outbound email delivery (verification emails, password resets, notifications).
{
"email": {
"enabled": true,
"smtp_host": "127.0.0.1",
"smtp_port": 1025,
"smtp_user": "",
"smtp_password": "",
"from_address": "noreply@example.com",
"use_tls": false,
"use_smtps": false
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | If true, enable outbound email delivery. |
smtp_host | string | "localhost" | SMTP server hostname. |
smtp_port | integer | 25 | SMTP server port. Common values: 25 (unencrypted), 587 (STARTTLS), 465 (SMTPS), 1025 (MailHog). |
smtp_user | string | "" | SMTP authentication username. Empty for unauthenticated relay. |
smtp_password | string | "" | SMTP authentication password. |
from_address | string | required | Sender address for all outbound emails. |
use_tls | boolean | false | If true, use STARTTLS to upgrade the connection after initial handshake. |
use_smtps | boolean | false | If true, connect directly over TLS (implicit TLS, port 465). Mutually exclusive with use_tls. |
When enabled is false, features that require email delivery (email verification, password reset) are disabled. The auth plugin will still accept registration requests but will not send verification emails.
The notifications.driver key selects the delivery mechanism for the notification engine:
{
"notifications": {
"driver": "smtp"
}
}
Currently only "smtp" is supported. Future versions MAY add additional drivers.
3.10 RBAC Configuration
The rbac section configures role-based access control for admin endpoints.
{
"rbac": {
"enabled": true,
"default_role": "user",
"cache_ttl_seconds": 5,
"action_permissions": {
"admin.users.list": "users.list",
"admin.users.get": "users.list",
"admin.roles.create": "roles.manage",
"admin.roles.list": "roles.list",
"admin.users.set_roles": "roles.manage",
"admin.users.verify": "roles.manage",
"admin.suspend": "users.manage",
"admin.unsuspend": "users.manage",
"admin.audit_log": "audit.view",
"admin.permissions.list": "roles.manage",
"admin.roles.permissions.set": "roles.manage"
}
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | If true, enforce RBAC on admin endpoints. |
default_role | string | "user" | Role assigned to newly registered users. |
cache_ttl_seconds | integer | 5 | How long permission lookups are cached in memory. |
action_permissions | object | {} | Map from bus action names to required permission strings. |
The action_permissions map determines which permission a user must have to invoke each admin action. Permissions are assigned to roles, and roles are assigned to users. The admin role has all permissions by default.
ADL domain resource permissions are configured in the domain YAML (§11), not in the RBAC configuration. This section governs only authd’s own admin endpoints.
3.11 Core Configuration
The core section configures the kernel itself.
{
"core": {
"management_socket": "/var/run/kalosd.sock",
"plugin_dirs": [
"/usr/lib/kalos/plugins"
],
"plugins": [
{
"name": "transport.rest",
"path": "kalos_rest.so",
"enabled": true
},
{
"name": "store.sqlite",
"path": "kalos_sqlite.so",
"enabled": true
},
{
"name": "handler.authd",
"path": "kalos_authd.so",
"enabled": true
},
{
"name": "adl",
"path": "kalos_adl.so",
"enabled": true
},
{
"name": "seeder",
"path": "kalos_seeder.so",
"enabled": true
},
{
"name": "scripting.lua",
"path": "kalos_lua.so",
"enabled": true
}
]
}
}
| Key | Type | Default | Description |
|---|---|---|---|
management_socket | string | platform-dependent | Path to the management socket. On POSIX: a Unix domain socket path (e.g., /var/run/kalosd.sock). On Windows: a named pipe (e.g., \\.\pipe\kalosd). |
plugin_dirs | string[] | ["."] | Directories to search for plugin shared libraries. Paths in the plugins array are resolved relative to these directories. |
plugins | object[] | [] | Ordered list of plugins to load. See below. |
Plugin Entry
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Plugin instance name. Must be unique. Used for logging and ICore::get_plugin(name). |
path | string | required | Shared library filename. Resolved against plugin_dirs. Platform-specific extensions (.so, .dll, .dylib) SHOULD be included. |
enabled | boolean | true | If false, the plugin is skipped during loading. |
Plugins are loaded and initialized in the order they appear in the plugins array. The order MUST respect dependencies:
- Transport plugins first (they register the HTTP listener)
- Store plugins second (they provide data persistence)
- Auth plugins third (they register authentication actions)
- ADL plugin fourth (it registers domain actions, depends on store and auth)
- Seeder plugin fifth (it dispatches to ADL and auth actions)
- Scripting plugins last (they subscribe to ADL events)
Misordering plugins results in missing action errors during on_start().
3.12 Application Configuration
The app_base_url key configures the base URL used in generated links (email verification URLs, password reset URLs, webhook callback URLs).
{
"app_base_url": "https://myapp.example.com/api/v1"
}
| Key | Type | Default | Description |
|---|---|---|---|
app_base_url | string | none | Base URL for generated links. If omitted, link generation uses the transport’s configured host and port. |
This value SHOULD be set in production to the externally-accessible URL, which may differ from the transport’s bind address (e.g., behind a reverse proxy or load balancer).
3.13 Audit Configuration
{
"audit": {
"enabled": false
}
}
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | If false, disable audit logging at the transport level. ADL domain-level audit (§15.2) is controlled by the domain YAML, not this setting. |
3.14 Environment Variable Substitution
Configuration values MAY reference environment variables using the ${VAR} syntax. Three forms are supported:
| Syntax | Behavior |
|---|---|
${VAR} | Substitute the value of environment variable VAR. If VAR is not set, substitute an empty string. |
${VAR:default} | Substitute the value of VAR. If VAR is not set, substitute default. |
${VAR!} | Substitute the value of VAR. If VAR is not set, the kernel MUST terminate with a configuration error. |
Environment variable substitution is performed by the kernel during configuration loading, before the configuration tree is passed to plugins. Substitution occurs in string values only — it does not apply to keys, integers, booleans, or structural elements.
{
"authd": {
"jwt": {
"secret": "${JWT_SECRET!}"
}
},
"store": {
"postgres": {
"conninfo": "host=${DB_HOST:localhost} port=${DB_PORT:5432} dbname=${DB_NAME} user=${DB_USER:postgres} password=${DB_PASSWORD!}"
}
}
}
In this example, JWT_SECRET and DB_PASSWORD are required — the server will not start without them. DB_HOST, DB_PORT, and DB_USER have defaults. DB_NAME is optional and defaults to an empty string.
3.15 Complete Configuration Example
A production-ready configuration combining all sections:
{
"logging": {
"level": "info",
"format": "json",
"file": {
"path": "/var/log/kalosd/server.log",
"max_size_mb": 50,
"max_files": 10
}
},
"transport": {
"rest": {
"host": "0.0.0.0",
"port": 443,
"tls": {
"cert_file": "/etc/ssl/certs/myapp.crt",
"key_file": "/etc/ssl/private/myapp.key",
"redirect_http": true,
"redirect_http_port": 80
},
"cors": {
"origins": "https://myapp.example.com"
},
"security": {
"hsts_max_age": 31536000,
"x_frame_options": "DENY"
}
}
},
"authd": {
"jwt": {
"algorithm": "HS256",
"secret": "${JWT_SECRET!}",
"issuer": "myapp",
"access_token_ttl_seconds": 900,
"refresh_token_ttl_seconds": 604800
},
"password": {
"algorithm": "argon2id",
"disable_checking": false
},
"store": "default",
"prefix": "/api/v1"
},
"store": {
"postgres": {
"conninfo": "host=${DB_HOST!} port=5432 dbname=${DB_NAME!} user=${DB_USER!} password=${DB_PASSWORD!} sslmode=require",
"name": "default",
"pool_size": 8
}
},
"adl": {
"domain_file": "/opt/myapp/domain.yaml"
},
"seeder": {
"enabled": true,
"run_on_start": true,
"seed_file": "/opt/myapp/seed.yaml"
},
"email": {
"enabled": true,
"smtp_host": "${SMTP_HOST!}",
"smtp_port": 587,
"smtp_user": "${SMTP_USER!}",
"smtp_password": "${SMTP_PASSWORD!}",
"from_address": "noreply@myapp.example.com",
"use_tls": true,
"use_smtps": false
},
"rate_limit": {
"enabled": true,
"max_requests": 120,
"window_seconds": 60,
"auth_endpoints": {
"max_attempts": 5,
"window_seconds": 300
}
},
"app_base_url": "https://myapp.example.com/api/v1",
"core": {
"management_socket": "/var/run/kalosd.sock",
"plugin_dirs": [
"/usr/lib/kalos/plugins"
],
"plugins": [
{ "name": "transport.rest", "path": "kalos_rest.so", "enabled": true },
{ "name": "store.postgres", "path": "kalos_postgres.so", "enabled": true },
{ "name": "handler.authd", "path": "kalos_authd.so", "enabled": true },
{ "name": "adl", "path": "kalos_adl.so", "enabled": true },
{ "name": "seeder", "path": "kalos_seeder.so", "enabled": true },
{ "name": "scripting.lua", "path": "kalos_lua.so", "enabled": true }
]
}
}
End of Chapter 3.
Next: Chapter 4 — REST Transport