tunablestunables version 0.2.0

Open-source TypeScript library

Runtime settings with guardrails, for the app you already run.

Declare each setting once. tunables keeps its value in your Postgres, lets each workspace and person set its own within their plan, puts it on a screen inside your app, checks every change as a whole, and records who changed what, and why.

npm i tunables

Pro plan

Tasklane example

What a workspace on Pro may hold

50

The most projects a workspace may have open at once. Default 50.

What everyone in the workspace sees
  • Workspace Acme keeps 20
  • Workspace Hobby club keeps 35

Saved

At its default. Turn it down past what a workspace keeps to see the save refused.

Tasklane's Pro plan, with the check a save of it runs. Turn Projects down past what a workspace keeps.

Where your settings live today

A value someone needs to change while the app runs ends up in one of four places. None of them knows what else the value bounds.

Why one home
  • Constants in code

    Changing one is a deploy, and nobody outside engineering can see it.

  • Environment variables

    Read at boot, set per deployment, and invisible to the people who decide them.

  • One-off admin forms

    Each checks its own fields, if any, and none checks the rest.

  • A key–value table

    A name and a blob: no unit, no default, no explanation.

No history
Nobody can say who changed a value, what it was before, or why it moved.
Silent breakage
Lower a limit and whatever was configured above it keeps running over it, until something fails somewhere else.
Two homes
A value read from the environment and also from the database needs a precedence rule nobody remembers.

How it works

Four steps take a value from a constant to a knob on a panel, with every turn recorded.

Quickstart
  1. Declare it once

    Each setting states its kind, unit, default, explanation, what changing it affects and where a workspace or person may set its own. Its check, its stored seed, its API schema and its form input all follow from that one declaration.

    settings.ts

    import { defineGroup, number } from 'tunables'export const limits = defineGroup({  id: 'limits',  title: 'Limits',  fields: {    maxProjects: number({      label: 'Projects',      unit: 'count',      default: 3,      // A workspace may lower it, never raise it.      override: { workspace: 'at-most' },      help: 'The most projects a workspace may have open at once.'    })  }})
    Settings and groups
  2. Store it in your Postgres

    Four tables in a schema of your database, written through the client you already have. Code reads typed values; boot never waits on the database.

    server/tunables.ts

    import { createTunables } from 'tunables'import { fromPg, postgresStore } from 'tunables/postgres'export const tunables = createTunables({  store: postgresStore(fromPg(pool)),  groups: [limits],  scopes: ['workspace', 'user']})const { maxProjects } = await tunables.get('limits', {  scope: { workspace: 'acme' }})
    Storage
  3. Embed the screens

    Mount one handler in your server, extend the Nuxt layer, and put a form where the setting matters: for your staff, or for one workspace's owner. The screens take your Nuxt UI theme.

    pages/workspace/limits.vue

    <template>  <TunablesGroupForm group="limits" :scope="{ workspace: 'acme' }" /></template>
    The Nuxt layer
  4. Change it safely

    From the screen or the terminal, a change goes through the same checks and becomes a revision you can read and restore.

    terminal

    $ npx tunables set limits maxProjects=20 --scope workspace=acme --reason "Acme asked for fewer"Saved as revision 14.$ npx tunables history limits --scope workspace=acme --limit 1#14  2026-10-23T09:12:44.021Z  ada (terminal)  “Acme asked for fewer”    maxProjects: null → 20
    History and restore

Guardrails, in plain words

A save is checked whole before anything is written. When it is refused, it says why in the setting's own words, and nothing changes.

Checks and guards
  • Each scope sets its own, within

    override

    Each workspace and person sets its own values, only for the fields declared for them, and never past what they inherit from the plan.

    Refused

    Projects may be at most 50 for Acme, the value it inherits.

  • Rules across settings

    rules

    Settings that bound one another are checked together on every save, at every scope.

    Refused

    Due soon may not be longer than the time before done tasks are archived.

  • Guards for what you keep

    guards

    Your own records that depend on a setting are checked against the new value, in your own words.

    Refused

    Acme has 42 projects open, more than the 30 this change allows.

  • Entries in use stay available

    catalog().retire

    A catalog entry something still picks cannot be retired from under it.

    Refused

    Small and quick is in use by Workspace › Suggestion model; move them to another model first.

  • Confirmations for changes with consequences

    confirm

    A change with consequences is saved only when it is sent again, confirmed.

    Asks first

    Guests will see every task in the project they are invited to, including its comments and attachments.

  • Restore through the same checks

    restore

    Every change is a revision with each value before and after. Restoring one is a save like any other.

    Recorded

    Restored revision 9. The import is done.

Built for coding agents

Three agent skills and a CLI let a coding agent adopt tunables, define settings the right way and verify that every value has one home, through the same checks a person gets.

Skills

Three skills

Installed for Claude Code, Codex CLI, Cursor, Gemini CLI and GitHub Copilot, in the project or for your user.

npx tunables install
  • tunables-adopt

    Brings tunables into an app that has none: inventories every value, gives each one home, and wires it once you approve.

  • tunables-define

    Adds or changes one setting with its kind, unit, default and checks, and says what happens to stored values.

  • tunables-verify

    Reports every value still hard-coded, kept in two homes or unguarded, and fixes only what you choose.

One CLI for people and agents

tunables check
Definitions, .env.example and the Product Model agree, without a database.
tunables explain
Writes each field's explanation from the Product Model.
tunables set … --reason
Changes values through the same checks as the screens.

Explained by your Product Model

Field explanations come from a pdd-kit Product Model, and tunables check fails when a default disagrees with the one the model states.

pdd-kit.dev

It attaches. It is not a service.

There is nothing to deploy beside your app and no account to create. tunables runs in your process, against your database, behind your sign-in.

Server and access
  • Your server

    One fetch handler mounts in Nitro, Hono, Bun or Deno. Express middleware and an oRPC router cover the rest.

    createHandler · tunablesMiddleware · tunablesRouter

  • Your database

    Four tables in a schema of your Postgres, through node-postgres or postgres.js. Changes can join your own transactions.

    postgresStore · migrate · within · hold

  • Your sign-in

    tunables has no users or roles. You say who is asking and whether they may, on every request.

    actor · authorize · refusal

Boot never needs the database: creating tunables checks your definitions and touches nothing else, and a read can fall back to the last values it saw.

Put your first setting on a panel.

The quickstart takes one group from a constant to a screen with a history, in your own app.

Read the quickstart