0011. Settings schema as single source
Status
Section titled “Status”Accepted
2026-10-25
Context
Section titled “Context”Customization is a core principle of Astela, so it has many settings, and every one of them appears in several places: the settings page with search, the stored user file, export and import, the JSON Schema for editors, the user and admin documentation, and the policy templates for Windows (ADMX/ADML), macOS (configuration profiles) and Linux (JSON). Every setting must also be settable and lockable by policy. Maintaining these outputs by hand would let them drift apart within a few releases.
Decision
Section titled “Decision”Each setting is defined exactly once, in the module that owns it, and every other representation is generated from that definition.
export const tabsPosition = defineSetting({ key: 'tabs.position', type: v.picklist(['top', 'bottom', 'left', 'right']), default: 'top', scope: 'global', policy: { name: 'TabStripPosition', recommendedAllowed: true }, ui: { section: 'tabs', control: 'select', keywords: ['tab bar', 'vertical tabs'] }, restartRequired: false,});- Definitions live in the modules’
*.contrib.tsfiles, so generators and the settings page can read them without loading feature code (ADR 0010). - Types are Valibot schemas (ADR 0007); typed setting keys and values are derived from them.
- The scope is
globalorworkspace. - Resolution order: default < recommended policy < user value < workspace override < mandatory policy.
inspect(key)returns the effective value, its source and whether it is locked. - Policies: every setting with a
policyattribute is automatically a policy. Locked values cannot be changed through the UI or the API, and the UI shows them with a lock and the label “Managed by your organization”. - Settings page: generated from the schema, with search over titles, descriptions, keywords and synonyms in English and German, deep links such as
app://settings/#tabs.position, and a “Modified” view that lists differences from the defaults. - Storage: user values live in
settings.jsonwith a$schemaVersion. Writes are debounced and atomic (temporary file,fsync, rename). Invalid keys are moved tosettings.rejected.json; nothing is deleted. - Export and import: JSON with version and schema checks, never containing secrets.
- Generated artifacts:
settings.schema.json, typed keys, the settings reference in the documentation, ADMX/ADML in English (en-US) and German (de-DE), a.mobileconfigexample, and the schema and an example for the Linuxpolicies.json; a Jamf JSON schema follows in M2. - Generated artifacts are not committed. They are built in CI, published as release assets and deployed to the website. A drift check ensures that the generators are deterministic.
Consequences
Section titled “Consequences”Positive:
- A new setting needs one definition to appear in the UI, the search, the documentation, the JSON Schema and every policy template.
- Administrators can rely on templates that always match the shipped version.
- Typed keys turn a misspelled setting into a compile error.
- The resolution order and
inspectmake every value explainable, in the UI and on the diagnostics page.
Negative:
- The generators are significant infrastructure that the team builds and maintains, with tests for each output format.
- Settings that need a custom control still need hand-written UI code, registered through the
uimetadata. - Policy names become a public interface: once released, a policy name cannot be changed without a migration path for administrators.
Alternatives considered
Section titled “Alternatives considered”- Hand-written settings page, documentation and templates: no generators, but drift between the outputs is almost certain.
- JSON Schema as the source: good tooling, but types and runtime validation would need extra generation steps, and UI metadata does not fit the schema format well.
- A generic store library such as electron-store: handles persistence only and offers no policy layers, resolution order or generated outputs.
- Settings defined centrally instead of per module: one file to read, but disabling a module would no longer remove its settings, and ownership would blur.