Chapter 3 · Volume I — Platform Architecture

Configuration

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
    }
  }
}
KeyTypeDefaultDescription
levelstring"info"Minimum log level: trace, debug, info, warn, error, critical
formatstring"text"Output format: "text" (human-readable) or "json" (structured)
file.pathstringnonePath to the log file. If omitted, logs are written to stdout only.
file.max_size_mbinteger10Maximum log file size in megabytes before rotation.
file.max_filesinteger3Number 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
    }
  }
}
KeyTypeDefaultDescription
hoststring"127.0.0.1"Bind address. Use "0.0.0.0" to accept connections from all interfaces.
portinteger8080TCP 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
      }
    }
  }
}
KeyTypeDefaultDescription
tls.cert_filestringrequiredPath to the TLS certificate file (PEM format).
tls.key_filestringrequiredPath to the TLS private key file (PEM format).
tls.redirect_httpbooleanfalseIf true, start an HTTP listener that returns 301 redirects to the HTTPS endpoint.
tls.redirect_http_portinteger8080Port 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": "*"
      }
    }
  }
}
KeyTypeDefaultDescription
cors.originsstring"*"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"
      }
    }
  }
}
KeyTypeDefaultDescription
security.hsts_max_ageinteger0Strict-Transport-Security max-age in seconds. Set to 0 to omit the header. 31536000 is one year.
security.x_frame_optionsstring"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

KeyTypeDefaultDescription
jwt.algorithmstring"HS256"JWT signing algorithm. Only "HS256" is supported in this version.
jwt.secretstringrequiredHMAC secret key. MUST be at least 32 characters in production.
jwt.issuerstring"kalos"Value of the iss claim in generated tokens.
jwt.access_token_ttl_secondsinteger900Access token lifetime (default: 15 minutes).
jwt.refresh_token_ttl_secondsinteger604800Refresh 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

KeyTypeDefaultDescription
password.algorithmstring"argon2id"Password hashing algorithm. Only "argon2id" is supported.
password.disable_checkingbooleanfalseIf 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

KeyTypeDefaultDescription
storestring"default"Name of the store plugin instance to use for user/session data.
prefixstring"/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
    }
  }
}
KeyTypeDefaultDescription
pathstringrequiredPath to the SQLite database file. Created if it does not exist.
namestring"default"Store instance name, referenced by other plugins via ICore::get_store(name).
pool_sizeinteger4Number 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
    }
  }
}
KeyTypeDefaultDescription
conninfostringrequiredPostgreSQL connection string (libpq format).
namestring"default"Store instance name.
pool_sizeinteger4Number 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"
  }
}
KeyTypeDefaultDescription
domain_filestringnonePath 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_pathstringautoPath 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_pathstringautoPath 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"
  }
}
KeyTypeDefaultDescription
enabledbooleantrueIf false, the seeder plugin is loaded but does not execute.
run_on_startbooleanfalseIf true, execute the seed manifest during the seeder’s on_start() phase.
seed_filestringrequiredPath 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
    }
  }
}
KeyTypeDefaultDescription
enabledbooleanfalseIf true, enforce rate limits on incoming requests.
max_requestsinteger60Maximum number of requests per client per window.
window_secondsinteger60Duration of the rate limiting window in seconds.
auth_endpoints.max_attemptsinteger10Maximum authentication attempts (login, register) per client per window.
auth_endpoints.window_secondsinteger60Window 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:

HeaderValue
X-RateLimit-LimitThe configured max_requests value
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds 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
  }
}
KeyTypeDefaultDescription
enabledbooleanfalseIf true, enable outbound email delivery.
smtp_hoststring"localhost"SMTP server hostname.
smtp_portinteger25SMTP server port. Common values: 25 (unencrypted), 587 (STARTTLS), 465 (SMTPS), 1025 (MailHog).
smtp_userstring""SMTP authentication username. Empty for unauthenticated relay.
smtp_passwordstring""SMTP authentication password.
from_addressstringrequiredSender address for all outbound emails.
use_tlsbooleanfalseIf true, use STARTTLS to upgrade the connection after initial handshake.
use_smtpsbooleanfalseIf 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"
    }
  }
}
KeyTypeDefaultDescription
enabledbooleantrueIf true, enforce RBAC on admin endpoints.
default_rolestring"user"Role assigned to newly registered users.
cache_ttl_secondsinteger5How long permission lookups are cached in memory.
action_permissionsobject{}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
      }
    ]
  }
}
KeyTypeDefaultDescription
management_socketstringplatform-dependentPath 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_dirsstring[]["."]Directories to search for plugin shared libraries. Paths in the plugins array are resolved relative to these directories.
pluginsobject[][]Ordered list of plugins to load. See below.

Plugin Entry

KeyTypeDefaultDescription
namestringrequiredPlugin instance name. Must be unique. Used for logging and ICore::get_plugin(name).
pathstringrequiredShared library filename. Resolved against plugin_dirs. Platform-specific extensions (.so, .dll, .dylib) SHOULD be included.
enabledbooleantrueIf 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:

  1. Transport plugins first (they register the HTTP listener)
  2. Store plugins second (they provide data persistence)
  3. Auth plugins third (they register authentication actions)
  4. ADL plugin fourth (it registers domain actions, depends on store and auth)
  5. Seeder plugin fifth (it dispatches to ADL and auth actions)
  6. 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"
}
KeyTypeDefaultDescription
app_base_urlstringnoneBase 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
  }
}
KeyTypeDefaultDescription
enabledbooleantrueIf 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:

SyntaxBehavior
${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