packages/docs/plugins/settings.md
Persist user preferences, secrets, and configuration with a single API. This guide shows how to define settings, read/write values, and react to changes.
{% hint style="info" %} Access settings via the API object (api.Settings.*) or the React hook described below. {% endhint %}
core.<id>plugin.<pluginId>.<id>id (e.g. theme), skip the prefix.hidden: true are stored but not shown in standard UI.{% tabs %} {% tab title="Register settings" %}
import type { NuclearPluginAPI } from '@nuclearplayer/plugin-sdk';
export default {
async onLoad(api: NuclearPluginAPI) {
await api.Settings.register([
{
id: 'theme',
title: 'Theme',
description: 'Choose your preferred theme',
category: 'Appearance',
kind: 'enum',
options: [
{ value: 'system', label: 'System' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
],
default: 'system',
},
{
id: 'scrobbleEnabled',
title: 'Enable scrobbling',
category: 'Integrations',
kind: 'boolean',
default: false,
widget: { type: 'toggle' },
},
]);
},
};
{% endtab %}
{% tab title="Read and write" %}
// Read a value (string | number | boolean | undefined)
const theme = await api.Settings.get<string>('theme');
// Update a value
await api.Settings.set('theme', 'dark');
// Subscribe to changes
const unsubscribe = api.Settings.subscribe<string>('theme', (value) => {
console.log('Theme changed to', value);
});
// Later
unsubscribe();
{% endtab %} {% endtabs %}
type SettingCategory = string;
type BooleanSettingDefinition = {
id: string;
title: string;
description?: string;
category: SettingCategory;
kind: 'boolean';
default?: boolean;
hidden?: boolean;
widget?: { type: 'toggle' };
};
type NumberSettingDefinition = {
id: string;
title: string;
description?: string;
category: SettingCategory;
kind: 'number';
default?: number;
hidden?: boolean;
widget?:
| { type: 'slider'; min?: number; max?: number; step?: number; unit?: string }
| { type: 'number-input'; min?: number; max?: number; step?: number; unit?: string };
min?: number;
max?: number;
step?: number;
unit?: string;
};
type StringSettingDefinition = {
id: string;
title: string;
description?: string;
category: SettingCategory;
kind: 'string';
default?: string;
hidden?: boolean;
widget?:
| { type: 'text'; placeholder?: string }
| { type: 'password'; placeholder?: string }
| { type: 'textarea'; placeholder?: string; rows?: number };
format?: 'text' | 'url' | 'path' | 'token' | 'language';
pattern?: string; // regex
minLength?: number;
maxLength?: number;
};
type EnumSettingDefinition = {
id: string;
title: string;
description?: string;
category: SettingCategory;
kind: 'enum';
options: { value: string; label: string }[];
default?: string;
hidden?: boolean;
widget?: { type: 'select' } | { type: 'radio' };
};
For settings that need a richer UI than the built-in widgets (OAuth flows, multi-field forms, live previews), use kind: 'custom' with a registered React component.
type CustomSettingDefinition = {
id: string;
title: string;
description?: string;
category: SettingCategory;
kind: 'custom';
widgetId: string;
default?: SettingValue;
hidden?: boolean;
};
The widgetId references a React component registered via api.Settings.registerWidget(). The component receives the current value, a setter, and the setting definition as props.
{% tabs %} {% tab title="Register a custom widget" %}
import type { NuclearPluginAPI, CustomWidgetProps } from '@nuclearplayer/plugin-sdk';
import { FC } from 'react';
const AuthWidget: FC<CustomWidgetProps> = ({ value, setValue }) => {
const session = value as { username: string } | undefined;
if (session) {
return <span>Connected as {session.username}</span>;
}
return (
<button onClick={() => setValue({ username: 'testuser' })}>
Connect
</button>
);
};
export default {
async onEnable(api: NuclearPluginAPI) {
api.Settings.registerWidget('auth', AuthWidget);
await api.Settings.register([{
id: 'session',
title: 'Account',
category: 'Integrations',
kind: 'custom',
widgetId: 'auth',
}]);
},
async onDisable(api: NuclearPluginAPI) {
api.Settings.unregisterWidget('auth');
},
};
{% endtab %} {% endtabs %}
Widget IDs are namespaced by plugin ID automatically. Two plugins can both register a widget called 'auth' without conflict.
The CustomWidgetProps type:
type CustomWidgetProps<API = unknown> = {
value: SettingValue | undefined;
setValue: (value: SettingValue) => void;
definition: CustomSettingDefinition;
api: API;
};
SettingValue accepts any JSON-serializable value (strings, numbers, booleans, objects, arrays, null), so custom widgets can store structured data like { sessionKey: string, username: string }.
{% hint style="warning" %}
Always unregister your widget in onDisable. If a custom setting references a widget that isn't registered, the settings UI will throw an error.
{% endhint %}
theme, apiKey, language, refreshInterval.plugin.my-id.theme.General, Appearance, Integrations.get(id) resolves to the definition’s default or undefined.default on the next run.The SDK exposes a React hook for live values: useSetting(host, id).
import { useSetting, type SettingsHost } from '@nuclearplayer/plugin-sdk';
function ThemeBadge({ host }: { host: SettingsHost }) {
const [theme, setTheme] = useSetting<string>(host, 'theme');
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme ?? 'system'}
</button>
);
}
{% hint style="warning" %}
In typical plugins you won’t have direct access to the SettingsHost. Prefer the async API on api.Settings. The hook is primarily for the core UI and advanced integrations where the app provides a host prop.
{% endhint %}
api.Settings.* before the settings host is available, an error is thrown: “Settings host not available”. In normal plugin lifecycles, the host is ready in onLoad and onEnable.get(id) returns undefined if neither a user value nor a default exists.import type { NuclearPluginAPI } from '@nuclearplayer/plugin-sdk';
export default {
async onLoad(api: NuclearPluginAPI) {
await api.Settings.register([
{ id: 'apiKey', title: 'API Key', category: 'Account', kind: 'string', widget: { type: 'password' }, format: 'token' },
{ id: 'language', title: 'Language', category: 'General', kind: 'enum', options: [
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
], default: 'en' },
{ id: 'debug', title: 'Enable debug logs', category: 'Advanced', kind: 'boolean', default: false, hidden: true },
]);
const lang = await api.Settings.get<string>('language');
if (lang === 'fr') {
// initialize French resources...
}
api.Settings.subscribe<string>('language', (next) => {
// switch translations live
});
},
async onEnable(api: NuclearPluginAPI) {
const scrobbling = await api.Settings.get<boolean>('scrobbleEnabled');
if (scrobbling) {
// start scrobbling service
}
},
};
// Settings management
api.Settings.register(defs: SettingDefinition[]): Promise<{ registered: string[] }>
api.Settings.get<T extends SettingValue>(id: string): Promise<T | undefined>
api.Settings.set<T extends SettingValue>(id: string, value: T): Promise<void>
api.Settings.subscribe<T extends SettingValue>(id: string, cb: (v: T | undefined) => void): () => void
// Custom widgets
api.Settings.registerWidget(widgetId: string, component: CustomWidgetComponent): void
api.Settings.unregisterWidget(widgetId: string): void
// Types
type SettingValue = JsonSerializable | undefined;
type JsonSerializable = string | number | boolean | null | JsonSerializable[] | { [key: string]: JsonSerializable };
type SettingDefinition = BooleanSettingDefinition | NumberSettingDefinition | StringSettingDefinition | EnumSettingDefinition | CustomSettingDefinition;
type CustomWidgetComponent<API = unknown> = FC<CustomWidgetProps<API>>;
hidden: true for internal toggles and feature flags.widget: { type: 'password' } and format: 'token'.| Symptom | Cause | Fix |
|---|---|---|
| “Settings host not available” | Called before plugin onLoad or outside runtime | Move to onLoad/onEnable or use provided API only |
get(id) returns undefined | No default and not yet set | Provide a default or handle undefined |
| Value reverts after restart | Not calling set(id, value) or overriding with defaults | Ensure you persist via set and avoid re-registering with a different default for already-set IDs |
If you spot issues or want new widgets/kinds, open a discussion or PR.