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.

BUSL-1.1 1.9k downloads
Updated 3 days ago Repository
kicksidesettingsconfigadmin

Run

wippy run kickside/settings

kickside/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 from list so the settings page never renders it.
    • schema — a JSON-schema for the value; list returns 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: null clears 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 backing kickside_settings.
  • api_router — required firewall-protected router for the admin API.