settings
Kickside settings — runtime, admin-editable, DB-backed application settings. Modules declare typed setting definitions in the registry; the DB stores only admin overrides; reads fall back to declared defaults.
Run
wippy run kickside/settingskickside/settings
Runtime, admin-editable, DB-backed application settings — exposed as the
kickside.settings:settings contract. Not env: env is deploy-time wiring;
settings are edited by an admin at runtime and persist in the database.
How it works
-
A module declares each of its settings as a registry entry:
- name: setting.max_items kind: registry.entry meta: type: kickside.settings:definition namespace: kickside.mymodule key: max_items value_type: int # string | int | bool | duration | json | secret default: 50 description: Items returned per page. scope: global editable: true # ── optional declarative fields ── visible: true # false: backend-manageable but hidden from the admin UI schema: # JSON-schema; the UI renders + validates the field from it type: integer minimum: 1 maximum: 100 validate_func: kickside.mymodule:validate_max # ns:name called before persist -
Optional declarative behavior, all read off the definition entry:
visible: false— the setting stays readable/writable through the contract and PUT API but is excluded fromlistso the settings page never renders it.schema— a JSON-schema for the value;listreturns it per setting and the UI renders the field + validates against it (inline errors) before a save.validate_func— a func id (ns:name) the writer calls (funcs.call(validate_func, { key, value, old_value, definition })) before persisting any change; it returns(ok, err)or{ valid, error }and can reject the write. The returned message surfaces on the field in the UI.
-
The DB stores only overrides. Reads fall back to the declared default, so a default change in a module upgrade propagates immediately, and "reset to default" is just deleting the override row.
-
Declarations are registry-discovered (
registry.find), so the admin UI renders every declared setting with its current value, default, and provenance — no central list to maintain.
Contract kickside.settings:settings
get({ namespace, key }) -> { success, value }set({ namespace, key, value, actor_id }) -> { success }(validates against the declared type)unset({ namespace, key }) -> { success }(revert to default)list({ namespace? }) -> { success, settings[] }(declarations merged with overrides + provenance)get_namespace({ namespace }) -> { success, values }
A consumer reads a setting through the contract; e.g. a retention sweeper reads
get({ namespace = "kickside.vacuum", key = "global_retention_days" }).
API
GET /settings[?namespace=]— list for the admin UI.PUT /settings— set one{namespace,key,value}or many{values:[...]};value: nullclears the override.
Both are admin-only by the app-owned positive allowlist:
app.security:user has no kickside.settings.api:* endpoint access, while
app.security:admin has root access. The host router's endpoint_firewall
enforces this; handlers do no in-code permission checks.
Requirements
target_db— required database backingkickside_settings.api_router— required firewall-protected router for the admin API.