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
settingsSchemasdefined withparseSchemaSettingConfig.- App
init-server-filters.tsenqueuesserver_get_settings_schema. - App i18n config initializes cross-env translation filters (
initCrossEnvFilters). - App has TRPC client access for client retrieval.
This page describes the standard kit integration path; adapt app-specific paths and config names when your project differs.
How To Use
Configuration roles
| Config | Purpose | Typical location |
|---|---|---|
parseSchemaSettingConfig | Defines setting keys, validation schema, default values, and storage routing (user_settings, user_attributes, custom providers) | config/settings.schema.config.ts(x) |
parseUISettingConfig | Defines navigation groups/pages and form structure (form, wrapper, ui, input rows, logic inputs) | config/settings.ui.config.tsx |
parseServerSettingConfig | Registers storage providers used by server reads/writes | config/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, anduseClientSettings - 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 type | Category |
|---|---|
text, textarea, phone, select, boolean, number, color, time, radio, theme, question_select | Persisted/interactive field inputs |
ui | Render custom React node |
wrapper | Group nested settings with layout/header |
form | Defines 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 givenlanguage+namespace.applyCrossEnvFilter: appends package namespaces into the app namespace list.
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, });
If these functions are not wired, package namespaces are not enqueued/resolved, and settings-related labels can render as raw translation keys.
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):
SettingsPagesbuilds form schemas fromsettingsSchemas.- On submit, logic callbacks run first.
- Then
clientTrpc.updateSettingsForm.fetch({ settingKeys, values })persists only keys present insettingKeys.
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' });Logic inputs are intentionally excluded from persisted values unless you copy them into persisted setting keys in your submit flow.
Filter API
Settings is a filter-first feature: schema, UI pages, inputs, server providers, and package translations are composed from multiple filter registrations.
| Filter | Parameters | Return | Registered By (package file) | Initialized In (app entrypoint) | Environment |
|---|---|---|---|---|---|
get_settings_schema | {} | SettingsSchema | kit/organization/src/www/filters/use-filters/use-settings-filters.tsx, kit/keybindings/src/filters/use-filters.tsx | apps/dashboard/hooks/use-filters.ts | client |
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.tsx | apps/dashboard/hooks/use-filters.ts | client |
get_settings_extra_inputs | {} | SettingsInputsBase | kit/organization/src/www/filters/use-filters/use-settings-filters.tsx | apps/dashboard/hooks/use-filters.ts | client |
server_get_settings_schema | {} | SettingsSchema | apps/dashboard/lib/init-server-filters.ts, kit/organization/src/www/filters/server-filters.ts, kit/keybindings/src/filters/server-filters.ts | apps/dashboard/lib/init-server-filters.ts | server |
server_get_settings_server_config | {} | ReturnType<typeof parseServerSettingConfig> | kit/organization/src/www/filters/server-filters.ts | apps/dashboard/lib/init-server-filters.ts | server |
cross_env_get_translations | { language: string; namespace: string } | Record<string, string> | null | 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 | apps/dashboard/lib/init-cross-env-filters.ts | cross-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.ts | apps/dashboard/lib/init-cross-env-filters.ts | cross-env |
apps/dashboard/lib/init-server-filters.tsmust enqueue schema/providers and call package server filter initializers.apps/dashboard/hooks/use-filters.tsmust mount package settings client filters (useOrgFilters,useBillingFilters,useKeybindingsFilters,useAIFilters).apps/dashboard/lib/init-cross-env-filters.tsmust register package translation filters.
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
- Add or update schema keys.
- Place keys in UI forms/pages and wire required custom inputs.
- Ensure server filters enqueue schema and providers.
- Use
getServerSettings/getClientSettings/useClientSettingsfor reads. - Use
updateSettingsForm(orSettingServerModel.updateSettingsserver-side) for writes.
Troubleshooting
- Missing key errors indicate schema not enqueued for current app.
- Raw translation keys in settings pages often mean
initCrossEnvFilters,applyCrossEnvAsyncFilter, orapplyCrossEnvFilterare 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 insettingKeys. any-typed values indicate missing typing strategy (generics or registry augmentation).
Related
How this docs website is structured and how to adapt it for customers.
Why the settings architecture matters and how it removes duplicate logic.
How is this guide?
Last updated on 3/27/2026