Bonfire Settings System Documentation
View SourceThis guide explains how to create, read, and update settings in Bonfire, covering the complete workflow from database storage to UI presentation.
Table of Contents
- Architecture Overview
- Settings Hierarchy
- Core Settings API
- Creating Settings Components
- Settings UI Integration
- Practical Examples
- Best Practices
- Troubleshooting
Architecture Overview
Bonfire's settings system is built around a hierarchical scope system that allows for flexible configuration at multiple levels. The system follows these core principles:
Key Components
Bonfire.Common.Settings
- Core settings API for get/put/set operationsBonfire.Data.Identity.Settings
- Database schema for storing settings as JSON- Settings Components - UI components for settings forms and inputs
- LiveHandler - Handles form submissions and real-time updates
- Scope System - Manages user/account/instance level settings
Data Flow
User Input → Form → LiveHandler → Settings.set() → Database → Cache → UI Update
Settings Hierarchy
Settings follow a bottom-up hierarchy where more specific settings override general ones:
- User Settings (highest priority) - Individual user preferences
- Account Settings - Shared account/team preferences
- Instance Settings - System-wide defaults set by admins
- OTP Config - Compile-time and runtime application configuration
- Default Values (lowest priority) - Hardcoded fallbacks
Scope Examples
# User-level setting
Settings.get([:ui, :theme], "light", current_user: user)
# Account-level setting
Settings.get([:ui, :theme], "light", current_account: account)
# Instance-level setting
Settings.get([:ui, :theme], "light", scope: :instance)
Core Settings API
Reading Settings
# Basic usage
Settings.get(:my_key, "default_value")
# With nested keys
Settings.get([:ui, :theme, :dark_mode], false)
# With user context
Settings.get([:ui, :theme], "light", current_user: user)
# With specific scope
Settings.get([:ui, :theme], "light", scope: :instance)
# Required setting (raises if not found)
Settings.get!([:required, :setting])
Writing Settings
# Set single value
Settings.put(:my_key, "new_value", current_user: user)
# Set nested value
Settings.put([:ui, :theme, :dark_mode], true, current_user: user)
# Set multiple values at once
Settings.set(%{
ui: %{
theme: "dark",
font_size: 16
}
}, current_user: user)
# Instance-level setting (requires admin)
Settings.put(:system_setting, "value", scope: :instance, current_user: admin)
Scope Control
The scope
parameter determines where settings are stored:
:user
- Current user's personal settings:account
- Account/team shared settings:instance
- System-wide settings (admin only)%User{}
- Specific user object%Account{}
- Specific account object
Creating Settings Components
There are two main approaches to creating settings components:
Approach 1: Custom Settings Components
For complex settings that need custom UI logic:
defmodule MyExtension.Settings.CustomSettingLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings_component(l("My Custom Setting"),
icon: "fluent:settings-16-filled",
description: l("Description of what this setting does"),
scope: :user # or :account, :instance
)
end
Approach 2: Template Components
For simple settings using built-in UI templates:
Toggle Setting
defmodule MyExtension.Settings.EnableFeatureLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:toggle, l("Enable Feature"),
keys: [MyExtension, :enable_feature],
description: l("Enable this awesome feature"),
default_value: false,
scope: :user
)
end
Select Setting
defmodule MyExtension.Settings.ThemeSelectLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:select, l("Choose Theme"),
keys: [MyExtension, :theme],
options: [
light: l("Light Theme"),
dark: l("Dark Theme"),
auto: l("Auto (System)")
],
default_value: :auto,
description: l("Select your preferred theme"),
scope: :user
)
end
Input Setting
defmodule MyExtension.Settings.ApiKeyLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:input, l("API Key"),
keys: [MyExtension, :api_key],
description: l("Enter your API key for external service"),
scope: :user
)
end
Number Setting
defmodule MyExtension.Settings.ItemLimitLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:number, l("Items per page"),
keys: [MyExtension, :items_per_page],
default_value: 20,
unit: l("items"),
description: l("How many items to show per page"),
scope: :user
)
end
Available Form Template Components Types
:toggle
- Simple on/off checkbox:toggles
- Multiple checkboxes:radios
- Radio button group:select
- Dropdown selection:input
- Text input field:textarea
- Multi-line text area:number
- Number input with unit label
Settings UI Integration
Manual Form Integration
For custom forms, use the standardized form components when possible:
<form data-scope="my_setting_scope" name="settings" phx-change="Bonfire.Common.Settings:set">
<input name="scope" value={@scope} type="hidden">
<Bonfire.UI.Common.SettingsToggleLive
name={l("Enable Feature")}
description={l("This enables the awesome feature")}
keys={[MyExtension, :enable_feature]}
scope={@scope}
/>
</form>
LiveHandler Integration
The Bonfire.Common.Settings.LiveHandler
provides these events:
"set"
- Set multiple settings from form data"put"
- Set single setting by key path"save"
- Save settings with success message
def handle_event("set", attrs, socket) do
# Automatically handled by LiveHandler
# Shows flash message on success
end
Practical Examples
Example 1: Simple Toggle Setting
Create a setting to enable/disable a feature:
- Create the settings component:
# extensions/my_extension/lib/components/settings/enable_notifications_live.ex
defmodule MyExtension.Settings.EnableNotificationsLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:toggle, l("Enable Email Notifications"),
keys: [MyExtension, :enable_notifications],
description: l("Receive email notifications for new messages"),
default_value: true,
scope: :user
)
end
- Use the setting in your code:
# Check if notifications are enabled
if Settings.get([MyExtension, :enable_notifications], true, current_user: user) do
# Send notification
send_email_notification(user, message)
end
Example 2: Select Setting with Multiple Options
Create a setting for choosing notification frequency:
# extensions/my_extension/lib/components/settings/notification_frequency_live.ex
defmodule MyExtension.Settings.NotificationFrequencyLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:select, l("Notification Frequency"),
keys: [MyExtension, :notification_frequency],
options: [
immediate: l("Immediate"),
hourly: l("Hourly Digest"),
daily: l("Daily Digest"),
never: l("Never")
],
default_value: :daily,
description: l("How often to receive notification emails"),
scope: :user
)
end
Example 3: Custom Settings Component
For complex settings that need custom logic:
# extensions/my_extension/lib/components/settings/advanced_config_live.ex
defmodule MyExtension.Settings.AdvancedConfigLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings_component(l("Advanced Configuration"),
icon: "fluent:settings-16-filled",
description: l("Advanced settings for power users"),
scope: :user
)
def render(assigns) do
~F"""
<div class="space-y-4">
<form data-scope="advanced_config" name="settings" phx-change="Bonfire.Common.Settings:set">
<input name="scope" value={@scope} type="hidden">
<div class="form-control">
<label class="label">
<span class="label-text">{l("Custom API Endpoint")}</span>
</label>
<input
type="url"
name={input_name([MyExtension, :api_endpoint])}
value={Settings.get([MyExtension, :api_endpoint], "", context: @__context__)}
class="input input-bordered"
placeholder="https://api.example.com"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">{l("Timeout (seconds)")}</span>
</label>
<input
type="number"
name={input_name([MyExtension, :timeout])}
value={Settings.get([MyExtension, :timeout], 30, context: @__context__)}
class="input input-bordered"
min="1"
max="300"
/>
</div>
</form>
</div>
"""
end
defp input_name(keys) do
Bonfire.Common.Settings.LiveHandler.input_name(keys)
end
end
Example 4: Instance-Level Setting
Create an admin-only instance setting:
# extensions/my_extension/lib/components/settings/instance_limits_live.ex
defmodule MyExtension.Settings.InstanceLimitsLive do
use Bonfire.UI.Common.Web, :stateless_component
prop scope, :any, default: nil
declare_settings(:number, l("Maximum Items Per User"),
keys: [MyExtension, :max_items_per_user],
default_value: 1000,
unit: l("items"),
description: l("Maximum number of items each user can create"),
scope: :instance # Admin only
)
end
Best Practices
Naming Conventions
Keys should be hierarchical:
[:my_extension, :feature, :sub_feature] # Good [:my_random_key] # Avoid
Use your module or extension as the top-level key:
[Bonfire.UI.Social.Feed, :default_sort] # Good [:feed, :default_sort] # Avoid - conflicts possible
Default Values
Always provide sensible defaults:
# Good - provides fallback
Settings.get([MyExt, :timeout], 30, current_user: user)
# Bad - could return nil unexpectedly
Settings.get([MyExt, :timeout], current_user: user)
Scoping
Choose appropriate scopes:
- User scope - Specific preferences for that profile (privacy settings, notifications)
- Account scope - General personal preferences (theme, language)
- Instance scope - System-wide configuration (limits, features)
Performance
- Instance-level settings are cached in OTP config
- User and account settings are loaded from database (all loaded once per page rather than queried individually when needed)
- Preload settings associations when querying user/account objects
Security
- Instance settings require admin permissions
- Avoid storing secrets in settings (use environment variables)
- Validate and sanitize user input
- Consider privacy implications of settings data
Testing
defmodule MyExtension.SettingsTest do
use Bonfire.DataCase
alias Bonfire.Common.Settings
test "setting default value" do
user = fake_user!()
# Test default
assert Settings.get([MyExt, :feature], false, current_user: user) == false
# Test setting value
{:ok, _} = Settings.put([MyExt, :feature], true, current_user: user)
assert Settings.get([MyExt, :feature], false, current_user: user) == true
end
test "hierarchy works correctly" do
user = fake_user!()
# Instance setting
{:ok, _} = Settings.put([MyExt, :limit], 100, scope: :instance, skip_boundary_check: true)
# User override
{:ok, _} = Settings.put([MyExt, :limit], 50, current_user: user)
# User setting takes precedence
assert Settings.get([MyExt, :limit], 10, current_user: user) == 50
end
end
Troubleshooting
Common Issues
Settings not saving:
- Check that form has
phx-change="Bonfire.Common.Settings:set"
- Ensure
scope
input is included in form - Verify user has permission for the scope
- Check that form has
Settings not loading:
- Confirm keys match exactly (atoms vs strings)
- Check that default value is provided
- Verify context includes current_user/current_account
Permission errors:
- Instance settings require admin permissions
- Check
skip_boundary_check: true
for testing - Verify user is properly authenticated
Settings not appearing in UI:
- Check file is in correct extension directory
- Verify component uses
declare_settings_component
ordeclare_settings
- Ensure extension is enabled
Debugging
Enable debug logging:
# In your settings call
Settings.get([MyExt, :setting], default,
current_user: user,
debug: true # Shows lookup process
)
Check database directly:
SELECT * FROM bonfire_data_identity_settings
WHERE id = 'user_id_here';
Key Validation
Settings keys should follow these patterns:
# Good patterns
[MyExtension, :feature_name]
[MyExtension, :category, :setting]
[Bonfire.UI.Common, :theme, :dark_mode]
# Bad patterns - avoid
[:global_setting] # Too generic
["string_key"] # Use atoms where sensible
[MyExtension, "mixed", :types] # Be consistent
Conclusion
The Bonfire settings system provides a flexible, hierarchical way to manage configuration at multiple levels. By following the patterns and best practices outlined in this guide, you can create robust, user-friendly settings for your extensions.
Key takeaways:
- Use the hierarchical scope system (user > account > instance > OTP config > code)
- Choose between custom components and template components based on complexity
- Always provide default values and consider performance
- Test your settings thoroughly and follow security best practices
- Leverage the automatic UI integration for consistent user experience
For more examples, examine existing settings components in the bonfire_ui_*
extensions.