Setting Presets
Overview
Setting presets are named snapshots of a user's Multichat, Events or Music display settings, saved to the Lumio account so they can be reused later and across devices - for example switching between a "Stream" and a "Chill" layout, or seeding a fresh OBS browser source with a known configuration.
The design keeps localStorage as the live state: a preset is written only when the user explicitly saves one and read only when they select one. There is no implicit background sync, so there are no conflict semantics, no migration of existing browser data, and nothing changes for OBS when the server is unreachable - the presets list simply isn't available then.
Using presets
Presets live in a Presets tab inside the settings panel of Multichat, Events and Music - the same panel the gear icon opens, in both the dashboard and the popout. Because Events and Music previously had a single flat settings column, adding the Presets tab gives those two surfaces a tab bar for the first time (Multichat already had one for General / Events / Filters).
The tab has:
- A header with a New preset button and a quota line ("3 of 5 used";
0shows as an unlimited "N saved"). - A card per preset of that surface's kind, showing the name, when it was last changed, a Default badge where applicable, and how many settings sections it holds. Each card offers:
- Load - apply the snapshot to this browser (replace semantics; the page reloads so every open view picks up the new settings).
- Share (share icon) - open the share dialog for that preset: copy a code or a share link, and (with
presets:shareand an active account) toggle whether it is visible to the whole account. The share icon is highlighted while the preset is shared with the account. - Overwrite (gear icon) - re-save the preset with your current settings, after a confirmation.
- Rename (pencil icon) - rename the preset (and optionally mark it as the kind's default).
- Delete (trash icon) - remove the preset, after a confirmation.
Saving opens a full-screen assistant (the same shape as creating a keyword rule): first choose which of your current settings to store (the same section list as the export dialog), then give the preset a name and optionally set it as the default.
Below the card list, a Shared from the channel section lists presets a teammate has shared into the account. These cards show the name, the owner's display name, the section count and the last-changed time, and offer only Load and Save as your own preset (which copies the shared snapshot into your own quota-counted list). They carry no overwrite / rename / delete / share - those stay owner-scoped. The section is hidden when nothing is shared into the account.
Below that sits the export / import controls (moved here from the old General tab).
In a popout (an OBS browser source authenticated by a popout token) the list is read-only in one direction: you can Load a preset (your own; the shared-from-the-channel section never appears over a popout token), but the create / overwrite / rename / delete / share and "save as your own" actions are hidden - presets are authored in the dashboard and loaded in the popout. Export / import stays available in the popout because it is purely local.
Default preset
Each (user, account) may mark one preset per kind as its default (from the save / rename assistant; the card's Default badge marks the current one). The default is applied automatically the first time that surface (Multichat, Events or Music) is opened for that pair in a given browser - which is what makes it useful on a new device or a fresh OBS browser source: your saved view is seeded there without you re-entering it, popout included.
The seeding is deliberately conservative:
- Only when the surface is pristine. The default is applied only if no settings blob exists yet for that
(user, account, kind)in this browser - "no blob" meaning the key is absent, not "the blob holds factory values". An existing local state is never overwritten, so the seed never fights a view you are already editing. - Once per pair. It runs at most once per
(user, account)+ surface; a second mount never re-applies and never resets an in-progress change. - Per (user, account), never global. The default comes from that pair's own presets, so a second account never inherits the first account's default, and a different team member on the same account starts from their own default (or factory).
- Fail-open. If the default cannot be resolved (offline, the presets list errors, or no default is set) the surface renders its normal factory defaults - never a blank view.
Marking a default writes nothing to any browser by itself; it only records which preset is seeded the next time a pristine surface mounts. Loading a preset by hand (the Load button) is unchanged and always available, whether or not a default is set.
Scope: per (user, account)
A preset belongs to the pair (user, account) (ZAF-622 §52c, founder ruling 2026-09-17) - it is "my view on this channel account", not a global per-user view. Each Lumio account needs its own filters, keyword tabs and keys, so a preset created in one account is neither visible nor loadable from another. Creating, editing, renaming, overwriting, deleting and setting-default a preset are all scoped to (the authenticated user, their active account) with no resource:action RBAC permission: one user can never mutate another user's presets, and no user can reach a preset in an account they are not a member of. The one RBAC gate on the surface is presets:share - the act of sharing a preset with your account (see Sharing with your account).
"Channel account" here means the Lumio account (accounts.id / me.activeAccountId), not a single platform connection - one account holds one connection per platform, and the Multichat is one cross-platform window.
Each preset has a kind - chat, events, or music - and the presets list is filtered by kind (a chat preset never appears in the events panel). A user may mark one default per (account, kind) - so the same user can keep an independent chat default in each of their accounts.
Fail-closed on membership. Because a preset is keyed (user_id, account_id), a preset in an account you have left is neither readable nor writable, even by its own owner - every read/write re-checks that the row's owner is still a member of the account (its owner_id or an account_memberships row). The preset row survives (it is removed only when its account is deleted, via ON DELETE CASCADE), so an operator can still find it by user; you simply cannot reach it once you leave.
Since ZAF-622 §52b every captured setting - the display options and the Multichat filter/highlight lists, keyword tabs, alerts and send-platform selection - is keyed to the same (you, the channel account you are on) pair. There is no longer a "display follows the browser profile, filters follow the account" split (the old §49 model): a preset is "my view on this account". So loading a preset applies your saved view into the (you, current account) pair; switching to another channel account, or signing in as a different team member, still starts those settings from defaults until you load a preset or import them, and a preset is the way to carry a view to a new browser or an OBS source within that account.
Sharing with your account
A user can share one of their own presets with the members of their active account so the team can load ("use") it in their own dashboard or OBS popout (ZAF-622 §19, E9 Variante A). The Share icon on a preset card opens a dialog with two independent ways to hand it over:
-
Code / link (local, no account needed). A copy-pasteable code and a share link built straight from the preset's stored snapshot - the exact same transport the settings export/import feature uses (a preset's payload is the export envelope). The link carries the code in its URL fragment, which never reaches the server, so this stays purely client-side: no endpoint, no audit event. When the payload holds viewer-name data (chat filter lists or keyword tabs) the dialog shows the "contains your viewer names" note, because a code/link is reversible cleartext.
-
Account visibility (the switch). A "Visible to all members of {account}" switch - shown only with an active account and the
presets:sharepermission - that shares the preset with, or un-shares it from, the account. The note spells out that every member can see and load it (but not edit, delete or re-share it). -
One boolean column,
setting_presets.shared.false= private;true= shared with the members of the preset's own account. Because a preset is bound to exactly one account (account_id), "shared" can only ever mean "shared with this account's members" - a boolean that cannot contradict itself, and that makes cross-account sharing impossible by construction (a preset built for account A can never appear in account B's team list). On the wire the API still exposesshared_account_idfor contract stability, derived as the preset's own account id when shared, elsenull. -
Sharing is a flag, not a copy. Sharing sets
shared = trueon the existing row (idempotent). It costs no quota:count_presetscounts only the owner's own rows in the account. Loading a shared preset writes nothing server-side (it only updates the reader'slocalStorage), so it costs the reader no quota either. Only an explicit "save as my own" creates a real row under the reader's(user_id, account_id)and runs the normal atomic quota gate. -
Read side (membership re-checked, fail-closed). A first-party member of the account sees, alongside their own presets, every preset shared into that account whose owner is still a member at read time (the account's
owner_id, or anaccount_membershipsrow). Once the owner leaves the account the shared preset disappears from the team's list even thoughsharedis still set. Foreign shared rows are flaggedshared: trueand carry the owner'sowner_display_name(no e-mail, no other user data). -
First-party only. The shared branch is surfaced only to a first-party principal (logged-in session / own API key). Over a popout / overlay token the list stays restricted to your own presets - an unassigned popout authenticates as the account owner, so surfacing the shared branch there would leak the whole team's presets into an OBS source.
-
Foreign presets are read + load only.
update/overwrite/setDefault/delete/unsharestay owner + account scoped (WHERE id = ? AND user_id = ? AND account_id = ?); a member can never mutate a preset shared by someone else.
Sharing (and un-sharing) requires the granular presets:share account permission, granted by default to Owner, Administrator and Moderator (not Viewer). Reading the shared section needs no permission - account membership is enough; the person who shared is told in the UI that every member of the account can see it.
Exporting and importing
The settings export / import controls (export the current settings to a file, clipboard, code string or share link; import a file / pasted code / link) live in the Presets tab, below the card list. Export / import is plan-free - it works even on plans without the presets feature. On such a plan there is no Presets tab, so the same controls appear instead at the bottom of the General tab. Exactly one of the two locations is shown per session; export / import is never both missing and never duplicated. Because it is purely local, it is rendered outside the presets list's own load path, so it keeps working even if the presets list itself fails to load.
When you import a code, link or file, the preview offers - alongside Apply (write the settings into this browser) - a Save as preset button that stores the imported snapshot directly as a new preset of the current surface's kind, without the "apply, reload, re-save" detour. It is disabled (with the same "limit reached" message as New preset) when your preset quota is full.
Quota
The number of presets a user may keep in an account is a per-plan limit, plans.max_setting_presets, spanning all kinds combined (one number, not three). The default quotas are Free = 5 and Pro = 20. It is overridable per account via account_limits.max_setting_presets, resolved as COALESCE(account_limits.x, plans.x). The quota is per account (ZAF-622 §52c): the count and the limit both belong to the active account, so a full account never blocks a different one, and each account's number comes from its own plan (plus any override) - there is no "best plan across the accounts you can reach" walk.
This answers the common question "if this account is on Pro, do its members get the Pro preset quota here?" - yes: the limit is the account's own plan value (20 on Pro), the same for every member of that account. An account-limit override raises (or lowers) the number for that one account. A user who belongs to a Pro account and a Free account gets 20 presets in the Pro account and 5 in the Free account - the quotas do not bleed across accounts.
0 means unlimited (the platform-wide convention). The create path enforces the quota atomically (a per-(user, account) advisory lock plus a count-gated insert), so two browser tabs saving at the limit at the same time cannot both slip through.
The quota is exposed read-only as settingPresetQuota (GraphQL) and its REST twin GET /v1/setting-presets/quota, which return used (your own presets in the active account across all kinds) and limit (0 = unlimited). The presets UI reads that one field to render the "N of M used" line and to disable New preset / Save as preset when the quota is full, without having to create a preset first. Both numbers are resolved by the very functions the create path gates on, so the warning can never disagree with the server's rejection.
Like the rest of the presets surface the quota read is user-scoped within the account: it needs no resource:action permission, only authentication plus feature:setting_presets, and it is popout-token readable. It is deliberately not gated on the account permission plan:read - routing it through accountLimits (which is, correctly, an account-scoped plan read) left every member without that permission with no limit at all, so the warning silently vanished and the cap only surfaced as an error on save. maxSettingPresets still exists on accountLimits / GET /v1/billing/account-limits as the account-scoped plan view for billing surfaces. Neither read carries an audit event: this is personal display/plan information, not a security-relevant action.
Operator control (AdminApp)
Platform operators adjust the quota from the AdminApp - no SQL required:
- Per plan - the plan create/edit form carries a Max Setting Presets field (
0= unlimited), gated onplans:edit. - Per account - the account detail limits form carries a Max Setting Presets override (empty = inherit the plan,
0= unlimited), gated onaccounts:edit. Both account-limit write paths (the full "limits" upsert and the partial "resolved-limits" update) persist the column, and both emit thesystem:account_limits_updatedaudit event.
Operators can also delete a user's presets (see Deleting presets as an operator below).
Feature flag
Presets are gated by the feature:setting_presets feature flag (category feature), with paired plan_features rows so the entitlement is fail-closed. The flag is the whether (presets available at all); the max_setting_presets column is the how much.
Popout access
The read endpoints work with a popout token, so the OBS-source popout can list and load presets - the place the feature is most useful (though a popout only ever sees the caller's own presets, never the shared team section). The write endpoints (create, update, delete, set-default, share, unshare) require a first-party user session (a logged-in user or the user's own API key); a popout/overlay/widget/extension token is rejected. This is deliberate: an unassigned popout token authenticates as the account owner, so allowing writes there would let a lesser member act as the owner. Authoring happens in the dashboard; the popout only loads.
Deleting presets as an operator
An operator manages presets by the owning user from the user detail page in the AdminApp, which lists all of that user's presets across every account (name, kind, default badge, last-changed - never the payload) with a Delete action. The account detail page mirrors the same list per member for convenience. Both surfaces call the same endpoints:
- GraphQL:
adminUserSettingPresets(userId)(query,users:read),adminDeleteUserSettingPreset(id)(mutation,users:edit). - REST:
GET /v1/admin/users/{id}/setting-presets(users:read),DELETE /v1/admin/setting-presets/{id}(users:edit).
Deleting a preset removes that single (user, account) row - a preset lives in exactly one account, so there is no cross-account copy to clean up. The delete emits the system:user_setting_preset_deleted audit event (target user id + preset id/kind, never the payload).
Audit logging
User preset changes (create / rename / overwrite / set-default / self-delete) emit no audit event. The audit log is a security record for security-relevant, tenant-visible or irreversible actions; a personal display setting is none of those (the same reasoning as notification preferences). Note this is not justified by "the data never leaves the browser" - with server-side presets it does - but by the action not being security-relevant.
Sharing a preset with your account does emit, because it makes personal data (a preset payload can hold viewer names) visible to other account members - a tenant-visible disclosure. Sharing emits account:setting_preset_shared and un-sharing emits account:setting_preset_unshared, both account-scoped (keyed to the preset's account, with the acting owner as the actor), on both GraphQL and REST. The metadata carries only the preset id, kind and name - never the payload. Un-sharing fires only when a share was actually removed.
Operator actions do emit, because an operator acts on another tenant's/user's data: deleting a user's preset emits system:user_setting_preset_deleted, and changing an account's numeric limits (including the preset quota) emits system:account_limits_updated. Both are system-scoped (platform-operator log only) - see Audit events.
Privacy
A preset's payload can include entries the user typed themselves - in particular chat filter terms and keyword-tab names - which may contain viewer usernames. These are the user's own account data; they are disclosed in the privacy policy and are deleted with the user's account (the table is attached to the account/erasure path by an ON DELETE CASCADE on the owning user).
API
Full parity on GraphQL and REST:
- GraphQL:
settingPresets,settingPreset(queries);createSettingPreset,updateSettingPreset,setDefaultSettingPreset,deleteSettingPreset,shareSettingPreset,unshareSettingPreset(mutations). See GraphQL API. - REST:
GET/POST /v1/setting-presets,GET/PATCH/DELETE /v1/setting-presets/{id},POST /v1/setting-presets/{id}/default,POST/DELETE /v1/setting-presets/{id}/share. See REST API.
The settingPresets list result marks each foreign shared row with shared: true + owner_display_name; own rows are shared: false. shareSettingPreset / unshareSettingPreset and their REST twins require presets:share; the share target is always your active account.