Features
PreviousNext

Settings

Build typed settings pages and typed settings reads with @kit/settings.

What It Does

@kit/settings combines schema definitions, UI composition, storage provider routing, server/client retrieval helpers, and update actions.

When To Use

  • User profile/preferences pages.
  • Organization-level configuration pages.
  • Feature modules that need typed settings reads.

Prerequisites

  • settingsSchemas defined with parseSchemaSettingConfig.
  • App init-server-filters.ts enqueues server_get_settings_schema.
  • App i18n config initializes cross-env translation filters (initCrossEnvFilters).
  • App has TRPC client access for client retrieval.

How To Use

Configuration roles

ConfigPurposeTypical location
parseSchemaSettingConfigDefines setting keys, validation schema, default values, and storage routing (user_settings, user_attributes, custom providers)config/settings.schema.config.ts(x)
parseUISettingConfigDefines navigation groups/pages and form structure (form, wrapper, ui, input rows, logic inputs)config/settings.ui.config.tsx
parseServerSettingConfigRegisters storage providers used by server reads/writesconfig/settings.server.config.tsx

Define settings schema (source of truth for types + validation + storage).

import { parseSchemaSettingConfig } from '@kit/settings/schema-config';
import { z } from 'zod';
 
export const settingsSchemas = parseSchemaSettingConfig({
  schema: {
    user_name: { schema: z.string().default(''), storage: 'user_attributes' },
    user_bio: { schema: z.string().max(500).default(''), storage: 'user_settings' },
    theme: { schema: z.enum(['light', 'dark', 'system']), storage: 'user_settings' },
  },
});

Schema definition drives:

  • input validation on reads and writes
  • default fallback values (when no row exists or parsing fails)
  • inferred return type for getServerSettings, getClientSettings, and useClientSettings
  • storage routing per key (user_attributes, user_settings, custom providers)

Define UI config (page structure + forms + non-persisted logic fields).

import { parseUISettingConfig } from '@kit/settings/ui-config'; import { z } from 'zod'; export const settingsUI = parseUISettingConfig({ ui: [ { group: 'index', label: 'About you', settingsPages: [ { slug: 'profile', title: 'Profile', icon: 'User', settings: [ { type: 'form', id: 'profile-form', settings: [ { type: 'text', slug: 'user_name', label: 'Name' }, { type: 'textarea', slug: 'user_bio', label: 'Bio' }, { slug: null, name: 'confirm_email', type: 'text', schema: z.string().email(), clearOnSubmit: true, onSubmit: async (values) => { if (values.user_email !== values.confirm_email) { throw new Error('Email mismatch'); } }, }, ], }, ], }, ], }, ], });

slug: null + name defines a logic input:

  • validated by its own schema
  • available in form submit handler
  • not persisted unless you explicitly map it into persisted values

Use the available inputs API.

REGISTERED_SETTINGS_INPUTS starts from quick-form built-ins:

Input typeCategory
text, textarea, phone, select, boolean, number, color, time, radio, theme, question_selectPersisted/interactive field inputs
uiRender custom React node
wrapperGroup nested settings with layout/header
formDefines submit boundary and validation scope
logic input (slug: null, name, schema)Non-persisted field with custom submit behavior

Add new custom inputs (app-level or package-level).

Define a custom input component and register it in your inputs map:

import type { QuickFormInput } from '@kit/utils/quick-form';
 
const UserMediaInput: QuickFormInput<{ triggerClassName?: string }> = ({ field }) => {
  return <input value={field.value ?? ''} onChange={(e) => field.onChange(e.target.value)} />;
};
 
const EXTRA_INPUTS = {
  user_media: UserMediaInput,
};

Then pass/merge inputs in settings pages:

const extraInputs = useApplyFilter('get_settings_extra_inputs', EXTRA_INPUTS);
 
<SettingsPages
  inputs={extraInputs}
  settingsSchemas={filteredSettingsSchema}
  settingsUI={settingsConfig}
  clientTrpc={clientTrpc}
  params={awaitedParams}
  Wrapper={Wrapper}
/>

Packages can extend inputs through get_settings_extra_inputs filters.

Register schema and storage providers in server filters.

import { enqueueServerFilter } from '@kit/utils/filters/server'; enqueueServerFilter('server_get_settings_schema', { name: 'appSettingsSchema', priority: 1, fn: (input) => ({ schema: { ...input.schema, ...settingsSchemas.schema, }, }), }); enqueueServerFilter('server_get_settings_server_config', { name: 'appSettingsProviders', fn: (input) => ({ ...input, providers: { ...input.providers, // organization_settings: organizationSettingsStorage, }, }), });

Initialize package translations through cross-env filters.

Settings pages often include UI/labels from package namespaces (p_auth, p_org-settings, p_billing, etc.).
These three functions are critical:

  • initCrossEnvFilters: enqueues package translation filters.
  • applyCrossEnvAsyncFilter: resolves package JSON translations for a given language + namespace.
  • applyCrossEnvFilter: appends package namespaces into the app namespace list.
apps/dashboard/config/i18n.config.ts
import { parseI18nConfig } from '@kit/i18n/config'; import { DEFAULT_LANG, SUPPORTED_LANGS } from '@kit/shared/config/defined-languages'; import { applyCrossEnvAsyncFilter, applyCrossEnvFilter } from '@kit/utils/filters/cross-env'; import { initCrossEnvFilters } from '~/lib/init-cross-env-filters'; initCrossEnvFilters(); async function i18nResolver(language: string, namespace: string) { const packageTranslations = await applyCrossEnvAsyncFilter('cross_env_get_translations', null, { language, namespace, }); if (packageTranslations) return packageTranslations; const data = await import(`../public/locales/${language}/${namespace}.json`); return data as Record<string, string>; } const namespaces = applyCrossEnvFilter('cross_env_get_namespaces', ['dashboard', 'settings']); export const i18nConfig = parseI18nConfig({ defaultLanguage: DEFAULT_LANG, languages: SUPPORTED_LANGS, namespaces, resolver: i18nResolver, });

Render settings pages.

Typical web composition:

const settingsConfig = useSettingsUiConfig();
const filteredSettingsSchema = useApplyFilter('get_settings_schema', settingsSchemas);
const extraInputs = useApplyFilter('get_settings_extra_inputs', EXTRA_INPUTS);
 
<SettingsPages
  inputs={extraInputs}
  settingsSchemas={filteredSettingsSchema}
  settingsUI={settingsConfig}
  clientTrpc={clientTrpc}
  params={awaitedParams}
  Wrapper={Wrapper}
/>

Fetch settings server-side.

import { getServerSettings } from '@kit/settings/shared/server/get-server-settings';
 
type AppSettingsSchema = typeof settingsSchemas.schema;
 
const settings = await getServerSettings<AppSettingsSchema, ['theme', 'user_name']>({
  settingKeys: ['theme', 'user_name'],
});

db is optional.
If omitted, getServerSettings uses await getDBClient() by default.

Fetch settings client-side.

One-shot fetch:

import { getClientSettings } from '@kit/settings/shared';
 
type AppSettingsSchema = typeof settingsSchemas.schema;
 
const values = await getClientSettings<AppSettingsSchema, 'theme'>({
  clientTrpc,
  settingKeys: ['theme'],
});

React-query hook:

import { useClientSettings } from '@kit/settings/shared';
 
type AppSettingsSchema = typeof settingsSchemas.schema;
 
const query = useClientSettings<AppSettingsSchema, 'theme'>({
  clientTrpc,
  settingKeys: ['theme'],
});

Update settings values.

Automatic update path (recommended):

  • SettingsPages builds form schemas from settingsSchemas.
  • On submit, logic callbacks run first.
  • Then clientTrpc.updateSettingsForm.fetch({ settingKeys, values }) persists only keys present in settingKeys.

Manual client update:

await clientTrpc.updateSettingsForm.fetch({
  settingKeys: ['theme', 'user_name'],
  values: { theme: 'dark', user_name: 'Arnaud' },
});

Manual server update:

import { applyServerFilter } from '@kit/utils/filters/server';
import { SettingServerModel } from '@kit/settings/shared/server/setting-server-model';
import { type SettingSchemaMap } from '@kit/settings/shared';
 
const fullSchema = applyServerFilter('server_get_settings_schema', { schema: {} as SettingSchemaMap<string> });
const serverConfig = applyServerFilter('server_get_settings_server_config', { providers: {} });
const model = new SettingServerModel(async () => db, serverConfig, fullSchema);
 
await model.updateSettings({ theme: 'dark' });

Filter API

Settings is a filter-first feature: schema, UI pages, inputs, server providers, and package translations are composed from multiple filter registrations.

FilterParametersReturnRegistered By (package file)Initialized In (app entrypoint)Environment
get_settings_schema{}SettingsSchemakit/organization/src/www/filters/use-filters/use-settings-filters.tsx, kit/keybindings/src/filters/use-filters.tsxapps/dashboard/hooks/use-filters.tsclient
get_settings_ui_config{ clientTrpc: TrpcClientWithQuery<Router<unknown>> }ReturnType<typeof parseUISettingConfig>kit/organization/src/www/filters/use-filters/use-settings-filters.tsx, kit/billing/core/src/www/filters/use-filters/use-settings-filters.tsx, kit/keybindings/src/filters/use-filters.tsx, kit/ai/src/www/filters/use-filters/use-settings-filters.tsxapps/dashboard/hooks/use-filters.tsclient
get_settings_extra_inputs{}SettingsInputsBasekit/organization/src/www/filters/use-filters/use-settings-filters.tsxapps/dashboard/hooks/use-filters.tsclient
server_get_settings_schema{}SettingsSchemaapps/dashboard/lib/init-server-filters.ts, kit/organization/src/www/filters/server-filters.ts, kit/keybindings/src/filters/server-filters.tsapps/dashboard/lib/init-server-filters.tsserver
server_get_settings_server_config{}ReturnType<typeof parseServerSettingConfig>kit/organization/src/www/filters/server-filters.tsapps/dashboard/lib/init-server-filters.tsserver
cross_env_get_translations{ language: string; namespace: string }Record<string, string> | nullkit/auth/src/www/filters/cross-env-filters.ts, kit/organization/src/www/filters/cross-env-filters.ts, kit/billing/core/src/www/filters/cross-env-filters.ts, kit/keybindings/src/filters/cross-env-filters.ts, kit/ai/src/www/filters/cross-env-filters.tsapps/dashboard/lib/init-cross-env-filters.tscross-env
cross_env_get_namespaces{}string[]kit/auth/src/www/filters/cross-env-filters.ts, kit/organization/src/www/filters/cross-env-filters.ts, kit/billing/core/src/www/filters/cross-env-filters.ts, kit/keybindings/src/filters/cross-env-filters.ts, kit/ai/src/www/filters/cross-env-filters.tsapps/dashboard/lib/init-cross-env-filters.tscross-env

MCP Context

capability: settings_api_fullstack entrypoints: - @kit/settings/schema-config - @kit/settings/server-config - @kit/settings/ui-config - @kit/settings/router - @kit/settings/shared/server/setting-server-model - @kit/settings/shared/server/get-server-settings - @kit/settings/shared (getClientSettings, useClientSettings) - @kit/settings/www/ui (SettingsPages, REGISTERED_SETTINGS_INPUTS) - apps/dashboard/hooks/use-filters.ts - apps/dashboard/lib/init-server-filters.ts - apps/dashboard/lib/init-cross-env-filters.ts - kit/organization/src/www/filters/use-filters/use-settings-filters.tsx - kit/organization/src/www/filters/server-filters.ts - kit/keybindings/src/filters/use-filters.tsx - kit/keybindings/src/filters/server-filters.ts - kit/billing/core/src/www/filters/use-filters/use-settings-filters.tsx - kit/ai/src/www/filters/use-filters/use-settings-filters.tsx - kit/auth/src/www/filters/cross-env-filters.ts - kit/organization/src/www/filters/cross-env-filters.ts - kit/billing/core/src/www/filters/cross-env-filters.ts - kit/keybindings/src/filters/cross-env-filters.ts - kit/ai/src/www/filters/cross-env-filters.ts - @kit/utils/filters/cross-env inputs: - setting_keys - values_payload - app_schema_filter_registration - app_server_provider_registration - custom_inputs_registration - package_translation_namespace_registration outputs: - validated_typed_setting_values - persisted_setting_updates constraints: - keys must exist in schema registered through server filters - client retrieval uses settings router getSettingsValues action side_effects: - reads settings from configured storage providers - writes settings through updateSettingsForm / SettingServerModel.updateSettings

Agent Recipe

  1. Add or update schema keys.
  2. Place keys in UI forms/pages and wire required custom inputs.
  3. Ensure server filters enqueue schema and providers.
  4. Use getServerSettings / getClientSettings / useClientSettings for reads.
  5. Use updateSettingsForm (or SettingServerModel.updateSettings server-side) for writes.

Troubleshooting

  • Missing key errors indicate schema not enqueued for current app.
  • Raw translation keys in settings pages often mean initCrossEnvFilters, applyCrossEnvAsyncFilter, or applyCrossEnvFilter are not wired.
  • Custom input not rendering usually means it was not merged through get_settings_extra_inputs.
  • Value not persisted from form usually means the field is a logic input (slug: null) or not included in settingKeys.
  • any-typed values indicate missing typing strategy (generics or registry augmentation).

How is this guide?

Last updated on 3/27/2026