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 tunablesPro plan
Tasklane example
What a workspace on Pro may hold
The most projects a workspace may have open at once. Default 50.
- 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.
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.
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.
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 and groupssettings.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.' }) }})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.
Storageserver/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' }})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.
The Nuxt layerpages/workspace/limits.vue
<template> <TunablesGroupForm group="limits" :scope="{ workspace: 'acme' }" /></template>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.
History and restoreterminal
$ 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
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.
Each scope sets its own, within
overrideEach 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
rulesSettings 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
guardsYour 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().retireA 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
confirmA 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
restoreEvery 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.
Three skills
Installed for Claude Code, Codex CLI, Cursor, Gemini CLI and GitHub Copilot, in the project or for your user.
npx tunables installtunables-adoptBrings tunables into an app that has none: inventories every value, gives each one home, and wires it once you approve.
tunables-defineAdds or changes one setting with its kind, unit, default and checks, and says what happens to stored values.
tunables-verifyReports 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.
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.
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.