--- title: Settings description: Declare singleton settings pages with pact.SettingsItem, backed by a model, a fields.yaml form and validation rules, and read them from plugin code. section: backend order: 60 --- # Settings A WinterCMS settings model extends `SettingModel`, stores its values in `system_settings` and registers its page with `registerSettings`. In SummerCMS a settings page is a singleton row of the plugin's own table, edited through a form described in `fields.yaml`, and declared with `pact.HasSettings`. ## Declaring a settings page `pact.HasSettings` returns `pact.SettingsItem` entries. Each names the page's code, label, category and icon for the settings index, the permissions it needs, the form file inside the plugin's embedded tree, and a factory for its model: ```go src=modules/cabana/example_controller_test.go#BlogPlugin.Settings // Settings replaces registerSettings(). func (p *BlogPlugin) Settings() []pact.SettingsItem { return []pact.SettingsItem{{ Code: "blog", Label: "acme.blog::lang.settings.label", Description: "acme.blog::lang.settings.description", Category: "acme.blog::lang.plugin.name", Icon: "icon-pencil", Model: "BlogSettings", Permissions: []string{"acme.blog.access_settings"}, Form: "models/settings/fields.yaml", NewModel: func() any { return &BlogSettings{} }, }} } ``` The model is a GORM struct with a `Fillable` list and a `Rules` method; [cabana](../../modules/cabana/README.md) refuses at start-up a settings model without either: ```go src=modules/cabana/example_controller_test.go#BlogSettings // BlogSettings is the singleton row behind the plugin's settings page. type BlogSettings struct { ID uint `gorm:"column:id;primaryKey"` PostsPerPage int `gorm:"column:posts_per_page"` CommentsEnabled bool `gorm:"column:comments_enabled"` } ``` ```go src=modules/cabana/example_controller_test.go#BlogSettings.Rules // Rules validates a settings save, as a WinterCMS settings model's $rules. func (BlogSettings) Rules() map[string]string { return map[string]string{"posts_per_page": "required|integer|between:1,100"} } ``` The form uses the same field types and options as a controller form; see [Forms](forms.md): ```yaml src=modules/cabana/testdata/docs/models/settings/fields.yaml fields: posts_per_page: label: acme.blog::lang.settings.posts_per_page type: number span: left default: 10 comments_enabled: label: acme.blog::lang.settings.comments_enabled type: switch span: right ``` Create the table in a migration like any other; see [Migrations](../database/migrations.md). ## How values are stored The settings row is the one with ID 1. Before it exists, the page shows the fields' `default` values and the API reports that the row does not exist yet; the first save creates it. A save writes only fillable fields, validates them with the model's rules and the form's `required` flags, and runs in a transaction, as a controller form save does. The admin API serves the page at `/api/v1/settings/{code}` (values) and `.../settings/{code}/schema` (the form), and `GET .../settings` lists the pages the administrator may open. ## Reading settings in plugin code Settings are an ordinary table, so plugin code reads them with GORM: ```go src=modules/beachcomber/example_test.go#gate svc.SetGate(beachcomber.GateFunc(func(ctx context.Context, db *gorm.DB) bool { var enabled bool err := db.WithContext(ctx).Raw(`SELECT search_enabled FROM acme_blog_settings WHERE id = 1`).Scan(&enabled).Error return err == nil && enabled })) ``` That example reads a flag from a settings row inside a search gate, treating a failed read as off. A setting that changes rarely and that operators, not administrators, own belongs in configuration instead; see [Configuration](../services/configuration.md).