Discourse Site Settings
Use this skill before writing or reviewing ordinary Discourse site settings. For upcoming-change
feature flags, also use discourse-upcoming-changes. For database migrations that rename or
remove settings, use discourse-migration.
Workflow
- Find nearby examples with
rg; avoid reading all of config/site_settings.yml.
- Put core settings in
config/site_settings.yml; put plugin settings in
plugins/<plugin>/config/settings.yml.
- Add an i18n description under
en.site_settings.<setting_name> in the matching
config/locales/server.en.yml.
- Set
client: true only when frontend code must read the setting through siteSettings.
- Prefer an
area: for admin grouping when the setting belongs on a filtered settings page.
- Access settings as
SiteSetting.setting_name in Ruby and through @service siteSettings in JS.
- Add a validator when a setting depends on another site setting for correctness.
- Write focused tests for behavior controlled by the setting, including default behavior.
- Use Playwright/browser to visit the admin site settings page and verify the setting name and
i18n description render correctly.
Naming
- Use a name that describes the behavior, not only the implementation.
- Use the conventional suffix for typed settings:
*_groups for group lists.
*_categories for type: category_list.
*_category for type: category.
- Avoid
_ids in public site setting names for category lists even though values are stored as ids.
- For paired settings, prepend the same feature name so admins can recognize the settings as a
related group.
Descriptions
- Write descriptions for admins, not developers.
- State the default or safest interpretation when ambiguity is likely.
- Mention privacy/security boundaries that always apply.
- If a setting only matters when another setting has a certain value, say so in the description.
- When referencing another site setting, use the
{{setting:setting_name}} token instead of writing
the raw setting name or hardcoding the translated label. This lets the admin UI render the
referenced setting consistently and avoids copy drifting when labels change.
- Keep option names consistent with enum values and admin labels.
Common Options
default: choose a safe default for existing sites. For upload settings, use the id of the
seeded upload from db/fixtures/010_uploads.rb.
min, max, regex, and validator: enforce value constraints server-side; descriptions and
frontend affordances are not enough.
mandatory_values: pipe-separated values that must always remain in the setting. Common for
group_list settings that must always include admins/moderators.
disallowed_groups: pipe-separated group ids hidden from group selectors and stripped from API
updates. This only applies to group_list settings.
requires_confirmation: use for risky changes that need an admin confirmation dialog. Valid
values are simple, simple_on_enable, and simple_on_disable; add matching client i18n under
admin.site_settings.requires_confirmation_messages.<setting_name> when the default copy is not
specific enough.
themeable: true: only for client-side UI settings that themes may override. Prefer simple types
such as bool, integer, list, or enum; normal SiteSetting.setting = value writes are not
allowed for themeable settings.
localizable: use for public text/content settings that content localization may translate. Use
a hash for metadata such as max_length: and cooked: true when localized markdown should be
cooked.
area: use the filtered admin settings page key when the setting belongs with an existing admin
workflow.
refresh: true: use when clients should reload after the setting changes.
Setting Types
Start with the simplest type that matches the admin's mental model:
bool: on/off behavior.
integer / float: numeric values; set min/max where possible.
enum: one value from a fixed set; use enum: "ClassName" for reusable translated enums.
list: multiple values from choices; use list_type: compact for selector-style UI and
list_type: simple for reorderable item lists.
group / group_list: one or many groups; use mandatory_values and disallowed_groups where
relevant.
category / category_list: one or many categories through the category picker.
tag_list, emoji_list, tag_group_list, host_list, email, username, color, icon,
upload, uploaded_image_list, and file_size_restriction: use when the name describes the
data shape directly.
json_schema / objects: structured settings. Prefer these only when a scalar/list setting is
not enough, and keep tests around schema validation and serialization.
Category Lists
Use a plain type: category_list when the setting means exactly "these categories". Do not add a
scope enum just because the setting involves categories. For example, user-default settings such as
default_categories_watching should stay as a single category list; admins are selecting the
categories to apply, not defining an include/exclude query scope.
default_categories_watching:
type: category_list
default: ""
area: "user_defaults"
Use the two-setting category-scope pattern only when the feature filters a topic/post result set and
admins may reasonably need all/public/include/exclude choices or subcategory control.
xyz_category_scope:
type: enum
default: "public"
enum: "CategoryScopeSiteSetting"
client: false
area: "feature-name"
xyz_categories:
type: category_list
default: ""
client: false
area: "feature-name"
depends_on:
- xyz_category_scope
depends_on_values:
xyz_category_scope:
- include
- include_strict
- exclude
- exclude_strict
depends_behavior: "hidden"
dependent_setting_display: "inline"
Semantics:
all: all regular topics.
public: all non-read-restricted regular topics.
include: use xyz_categories and their subcategories.
include_strict: use only xyz_categories.
exclude: start from all regular topics, then remove xyz_categories and their subcategories.
exclude_strict: start from all regular topics, then remove only xyz_categories.
The CategoryScopeSiteSetting enum provides translated admin labels for these stored values. Pair
it with depends_on_values, depends_behavior: "hidden", and dependent_setting_display: "inline"
so the category picker appears under the scope setting only when include/exclude modes need it.
Do not make exclude mean "public minus selected categories" unless the product requirement
explicitly says that. It is harder to explain and usually surprises admins.
Example from Discourse AI admin dashboard highlights:
ai_admin_dashboard_highlights_category_scope:
type: enum
default: "public"
enum: "CategoryScopeSiteSetting"
client: false
area: "ai-features/admin_dashboard"
ai_admin_dashboard_highlights_categories:
type: category_list
default: ""
client: false
area: "ai-features/admin_dashboard"
depends_on:
- ai_admin_dashboard_highlights_category_scope
depends_on_values:
ai_admin_dashboard_highlights_category_scope:
- include
- include_strict
- exclude
- exclude_strict
depends_behavior: "hidden"
dependent_setting_display: "inline"
Recommended description shape:
ai_admin_dashboard_highlights_category_scope: "Choose which categories AI highlights can use. Include and exclude options use {{setting:ai_admin_dashboard_highlights_categories}}. Personal messages are always excluded."
ai_admin_dashboard_highlights_categories: "Categories used when the AI highlights category scope is set to <strong>include</strong> or <strong>exclude</strong>."
Implementation notes:
- Use
Category.subcategory_ids(category_id) when non-strict modes include descendants.
- Keep
IN (:category_ids) / NOT IN (:category_ids) parameterized; parse category-list values
to integers before passing them into SQL.
- Handle empty lists explicitly:
- Empty include means match nothing.
- Empty exclude means exclude nothing.
- If results are cached, include both the scope and the configured category list in the cache key.
- Test public default, all, include, include strict, exclude, exclude strict, private categories,
personal messages if topics are involved, and subcategories.
Enum Settings
- Prefer stable machine values such as
include_strict; let descriptions/admin labels explain them.
- Keep enum values short, lowercase, and underscore-separated.
- When changing values before merge, update all tests and cache keys. After merge, treat value
changes as migrations/compatibility work.
Dependent Settings
- Add a validator when one setting only works if another setting is enabled, configured, or has a
compatible value.
- Keep dependency enforcement server-side; admin descriptions are not enough.
depends_on without depends_on_values is for boolean parent settings.
- Use
depends_on_values when the parent is an enum/string setting and the dependent only applies
to specific values.
- Use
depends_behavior: "hidden" when the dependent setting should be hidden until applicable.
- Use
dependent_setting_display: "inline" when a hidden dependent belongs visually under its
parent; save/reset controls are grouped with the parent setting.
- Test both the valid and invalid combinations.
Review Checklist
- Is the default safe for existing sites?
- Is the setting hidden from the client unless JS needs it?
- Does a dependency on another setting need a validator?
- Does the admin description answer the likely "what does this include?" question?
- Is the setting grouped into the right admin area?
- Was the admin settings page checked in the browser so the name and description render correctly?
- Are cache keys or background jobs affected by setting changes?
- Are private categories, personal messages, subcategories, deleted records, and anonymous access
handled where relevant?
1---2name: discourse-site-settings3description: Use when adding, modifying, renaming, or reviewing Discourse site settings in core or plugins. Covers config/site_settings.yml and plugin config/settings.yml, setting names, types, defaults, client exposure, areas, validators, i18n descriptions, category/category_list conventions, category scope dropdowns, access patterns, cache implications, tests, browser verification, and migration handoffs.4---56# Discourse Site Settings78Use this skill before writing or reviewing ordinary Discourse site settings. For upcoming-change9feature flags, also use `discourse-upcoming-changes`. For database migrations that rename or10remove settings, use `discourse-migration`.1112## Workflow13141. Find nearby examples with `rg`; avoid reading all of `config/site_settings.yml`.152. Put core settings in `config/site_settings.yml`; put plugin settings in16 `plugins/<plugin>/config/settings.yml`.173. Add an i18n description under `en.site_settings.<setting_name>` in the matching18 `config/locales/server.en.yml`.194. Set `client: true` only when frontend code must read the setting through `siteSettings`.205. Prefer an `area:` for admin grouping when the setting belongs on a filtered settings page.216. Access settings as `SiteSetting.setting_name` in Ruby and through `@service siteSettings` in JS.227. Add a validator when a setting depends on another site setting for correctness.238. Write focused tests for behavior controlled by the setting, including default behavior.249. Use Playwright/browser to visit the admin site settings page and verify the setting name and25 i18n description render correctly.2627## Naming2829- Use a name that describes the behavior, not only the implementation.30- Use the conventional suffix for typed settings:31 - `*_groups` for group lists.32 - `*_categories` for `type: category_list`.33 - `*_category` for `type: category`.34- Avoid `_ids` in public site setting names for category lists even though values are stored as ids.35- For paired settings, prepend the same feature name so admins can recognize the settings as a36 related group.3738## Descriptions3940- Write descriptions for admins, not developers.41- State the default or safest interpretation when ambiguity is likely.42- Mention privacy/security boundaries that always apply.43- If a setting only matters when another setting has a certain value, say so in the description.44- When referencing another site setting, use the `{{setting:setting_name}}` token instead of writing45 the raw setting name or hardcoding the translated label. This lets the admin UI render the46 referenced setting consistently and avoids copy drifting when labels change.47- Keep option names consistent with enum values and admin labels.4849## Common Options5051- `default`: choose a safe default for existing sites. For upload settings, use the id of the52 seeded upload from `db/fixtures/010_uploads.rb`.53- `min`, `max`, `regex`, and `validator`: enforce value constraints server-side; descriptions and54 frontend affordances are not enough.55- `mandatory_values`: pipe-separated values that must always remain in the setting. Common for56 `group_list` settings that must always include admins/moderators.57- `disallowed_groups`: pipe-separated group ids hidden from group selectors and stripped from API58 updates. This only applies to `group_list` settings.59- `requires_confirmation`: use for risky changes that need an admin confirmation dialog. Valid60 values are `simple`, `simple_on_enable`, and `simple_on_disable`; add matching client i18n under61 `admin.site_settings.requires_confirmation_messages.<setting_name>` when the default copy is not62 specific enough.63- `themeable: true`: only for client-side UI settings that themes may override. Prefer simple types64 such as `bool`, `integer`, `list`, or `enum`; normal `SiteSetting.setting = value` writes are not65 allowed for themeable settings.66- `localizable`: use for public text/content settings that content localization may translate. Use67 a hash for metadata such as `max_length:` and `cooked: true` when localized markdown should be68 cooked.69- `area`: use the filtered admin settings page key when the setting belongs with an existing admin70 workflow.71- `refresh: true`: use when clients should reload after the setting changes.7273## Setting Types7475Start with the simplest type that matches the admin's mental model:7677- `bool`: on/off behavior.78- `integer` / `float`: numeric values; set `min`/`max` where possible.79- `enum`: one value from a fixed set; use `enum: "ClassName"` for reusable translated enums.80- `list`: multiple values from choices; use `list_type: compact` for selector-style UI and81 `list_type: simple` for reorderable item lists.82- `group` / `group_list`: one or many groups; use `mandatory_values` and `disallowed_groups` where83 relevant.84- `category` / `category_list`: one or many categories through the category picker.85- `tag_list`, `emoji_list`, `tag_group_list`, `host_list`, `email`, `username`, `color`, `icon`,86 `upload`, `uploaded_image_list`, and `file_size_restriction`: use when the name describes the87 data shape directly.88- `json_schema` / `objects`: structured settings. Prefer these only when a scalar/list setting is89 not enough, and keep tests around schema validation and serialization.9091### Category Lists9293Use a plain `type: category_list` when the setting means exactly "these categories". Do not add a94scope enum just because the setting involves categories. For example, user-default settings such as95`default_categories_watching` should stay as a single category list; admins are selecting the96categories to apply, not defining an include/exclude query scope.9798```yaml99default_categories_watching:100 type: category_list101 default: ""102 area: "user_defaults"103```104105Use the two-setting category-scope pattern only when the feature filters a topic/post result set and106admins may reasonably need all/public/include/exclude choices or subcategory control.107108```yaml109xyz_category_scope:110 type: enum111 default: "public"112 enum: "CategoryScopeSiteSetting"113 client: false114 area: "feature-name"115116xyz_categories:117 type: category_list118 default: ""119 client: false120 area: "feature-name"121 depends_on:122 - xyz_category_scope123 depends_on_values:124 xyz_category_scope:125 - include126 - include_strict127 - exclude128 - exclude_strict129 depends_behavior: "hidden"130 dependent_setting_display: "inline"131```132133Semantics:134135- `all`: all regular topics.136- `public`: all non-read-restricted regular topics.137- `include`: use `xyz_categories` and their subcategories.138- `include_strict`: use only `xyz_categories`.139- `exclude`: start from all regular topics, then remove `xyz_categories` and their subcategories.140- `exclude_strict`: start from all regular topics, then remove only `xyz_categories`.141142The `CategoryScopeSiteSetting` enum provides translated admin labels for these stored values. Pair143it with `depends_on_values`, `depends_behavior: "hidden"`, and `dependent_setting_display: "inline"`144so the category picker appears under the scope setting only when include/exclude modes need it.145146Do not make `exclude` mean "public minus selected categories" unless the product requirement147explicitly says that. It is harder to explain and usually surprises admins.148149Example from Discourse AI admin dashboard highlights:150151```yaml152ai_admin_dashboard_highlights_category_scope:153 type: enum154 default: "public"155 enum: "CategoryScopeSiteSetting"156 client: false157 area: "ai-features/admin_dashboard"158159ai_admin_dashboard_highlights_categories:160 type: category_list161 default: ""162 client: false163 area: "ai-features/admin_dashboard"164 depends_on:165 - ai_admin_dashboard_highlights_category_scope166 depends_on_values:167 ai_admin_dashboard_highlights_category_scope:168 - include169 - include_strict170 - exclude171 - exclude_strict172 depends_behavior: "hidden"173 dependent_setting_display: "inline"174```175176Recommended description shape:177178```yaml179ai_admin_dashboard_highlights_category_scope: "Choose which categories AI highlights can use. Include and exclude options use {{setting:ai_admin_dashboard_highlights_categories}}. Personal messages are always excluded."180ai_admin_dashboard_highlights_categories: "Categories used when the AI highlights category scope is set to <strong>include</strong> or <strong>exclude</strong>."181```182183Implementation notes:184185- Use `Category.subcategory_ids(category_id)` when non-strict modes include descendants.186- Keep `IN (:category_ids)` / `NOT IN (:category_ids)` parameterized; parse category-list values187 to integers before passing them into SQL.188- Handle empty lists explicitly:189 - Empty include means match nothing.190 - Empty exclude means exclude nothing.191- If results are cached, include both the scope and the configured category list in the cache key.192- Test public default, all, include, include strict, exclude, exclude strict, private categories,193 personal messages if topics are involved, and subcategories.194195### Enum Settings196197- Prefer stable machine values such as `include_strict`; let descriptions/admin labels explain them.198- Keep enum values short, lowercase, and underscore-separated.199- When changing values before merge, update all tests and cache keys. After merge, treat value200 changes as migrations/compatibility work.201202### Dependent Settings203204- Add a validator when one setting only works if another setting is enabled, configured, or has a205 compatible value.206- Keep dependency enforcement server-side; admin descriptions are not enough.207- `depends_on` without `depends_on_values` is for boolean parent settings.208- Use `depends_on_values` when the parent is an enum/string setting and the dependent only applies209 to specific values.210- Use `depends_behavior: "hidden"` when the dependent setting should be hidden until applicable.211- Use `dependent_setting_display: "inline"` when a hidden dependent belongs visually under its212 parent; save/reset controls are grouped with the parent setting.213- Test both the valid and invalid combinations.214215## Review Checklist216217- Is the default safe for existing sites?218- Is the setting hidden from the client unless JS needs it?219- Does a dependency on another setting need a validator?220- Does the admin description answer the likely "what does this include?" question?221- Is the setting grouped into the right admin area?222- Was the admin settings page checked in the browser so the name and description render correctly?223- Are cache keys or background jobs affected by setting changes?224- Are private categories, personal messages, subcategories, deleted records, and anonymous access225 handled where relevant?