Packages
phoenix_kit
1.7.81
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/install/oban_config.ex
defmodule PhoenixKit.Install.ObanConfig do
@moduledoc """
Handles Oban configuration for PhoenixKit installation.
This module provides functionality to:
- Configure Oban for background job processing
- Set up required queues (default, emails, file_processing)
- Add Oban.Plugins.Pruner for job cleanup
- Add Oban to application supervisor tree
- Ensure configuration exists during updates
"""
use PhoenixKit.Install.IgniterCompat
# Mix functions only available at compile-time during installation
@dialyzer {:nowarn_function, update_existing_oban_config: 3}
@dialyzer {:nowarn_function, ensure_posts_queue: 2}
@dialyzer {:nowarn_function, ensure_sitemap_queue: 2}
@dialyzer {:nowarn_function, ensure_sqs_polling_queue: 2}
@dialyzer {:nowarn_function, ensure_shop_imports_queue: 2}
@dialyzer {:nowarn_function, ensure_newsletters_delivery_queue: 2}
@dialyzer {:nowarn_function, ensure_cron_plugin: 2}
@dialyzer {:nowarn_function, ensure_pruner_max_age: 2}
@dialyzer {:nowarn_function, add_cron_plugin_to_plugins: 2}
alias Igniter.Libs.Phoenix
alias Igniter.Project.Application
alias PhoenixKit.Install.IgniterHelpers
@doc """
Adds or verifies Oban configuration.
This function ensures that Oban is properly configured for PhoenixKit's
background job processing, including:
1. Repo configuration (auto-detected from PhoenixKit config)
2. Required queues for file processing and email handling
3. Pruner plugin for automatic job cleanup
## Parameters
- `igniter` - The igniter context
## Returns
Updated igniter with Oban configuration and notices.
"""
def add_oban_configuration(igniter) do
igniter
|> add_oban_config()
|> add_oban_configuration_notice()
end
@doc """
Checks if Oban configuration exists in config.exs.
## Parameters
- `igniter` - The igniter context for detecting parent app name
## Returns
Boolean indicating if configuration exists.
"""
def oban_config_exists?(igniter) do
config_path = "config/config.exs"
app_name = IgniterHelpers.get_parent_app_name(igniter)
if File.exists?(config_path) do
content = File.read!(config_path)
lines = String.split(content, "\n")
# Check for active (non-commented) Oban configuration with parent app namespace
has_oban_config =
Enum.any?(lines, fn line ->
trimmed = String.trim(line)
# Not a comment and contains config :app_name, Oban
!String.starts_with?(trimmed, "#") and
String.contains?(line, "config :#{app_name}, Oban")
end)
has_queues =
Enum.any?(lines, fn line ->
trimmed = String.trim(line)
# Not a comment and contains queues:
!String.starts_with?(trimmed, "#") and String.contains?(line, "queues:")
end)
has_oban_config and has_queues
else
false
end
rescue
_ -> false
end
# Clean up broken Oban config syntax from previous failed updates
# NOTE: Previously this function attempted to fix syntax issues with greedy
# regexes, but they could corrupt valid commented config. The regexes have
# been removed as they caused more harm than good. If syntax issues occur
# from failed updates, they should be fixed manually or with more targeted
# approaches.
defp cleanup_oban_config_syntax do
:ok
end
# Add Oban configuration to config.exs
defp add_oban_config(igniter) do
# First, clean up any broken syntax from previous failed updates
cleanup_oban_config_syntax()
# Get parent app name and repo
app_name = IgniterHelpers.get_parent_app_name(igniter)
repo_module = get_repo_module(igniter)
oban_config = """
# Configure Oban for PhoenixKit background jobs
# Required for file processing (storage system), email handling, posts, and sitemap
config :#{app_name}, Oban,
repo: #{repo_module},
queues: [
default: 10, # General purpose queue
emails: 50, # Email processing
file_processing: 20, # File variant generation (storage system)
posts: 10, # Posts scheduled publishing
scheduled_jobs: 1, # Scheduled jobs cron
sitemap: 5, # Sitemap generation
sqs_polling: 1, # SQS polling for email events (only one concurrent job)
newsletters_delivery: 10 # Newsletters broadcast deliveries
],
plugins: [
# Pruner: delete completed/discarded jobs after 30 days
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30},
{Oban.Plugins.Cron,
crontab: [
{"* * * * *", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}
]}
]
"""
try do
Igniter.update_file(igniter, "config/config.exs", fn source ->
content = Rewrite.Source.get(source, :content)
# Check if Oban config already exists (with more robust detection)
if oban_config_already_exists?(content, app_name) do
# Update existing config to add posts queue and cron plugin
update_existing_oban_config(source, content, app_name)
else
# Find insertion point before import_config statements
insertion_point = find_import_config_location(content)
updated_content =
case insertion_point do
{:before_import, before_content, after_content} ->
# Insert before import_config
before_content <> oban_config <> "\n" <> after_content
:append_to_end ->
# No import_config found, append to end
content <> oban_config
end
Rewrite.Source.update(source, :content, updated_content)
end
end)
rescue
e ->
IO.warn("Failed to add Oban configuration: #{inspect(e)}")
add_manual_config_notice(igniter, repo_module)
end
end
# Update existing Oban configuration to add posts/sitemap/sqs_polling queues and cron plugin
defp update_existing_oban_config(source, content, app_name) do
Mix.shell().info("🔍 Updating existing Oban configuration for :#{app_name}...")
updated_content =
content
|> ensure_posts_queue(app_name)
|> ensure_sitemap_queue(app_name)
|> ensure_sqs_polling_queue(app_name)
|> ensure_shop_imports_queue(app_name)
|> ensure_newsletters_delivery_queue(app_name)
|> ensure_cron_plugin(app_name)
|> ensure_pruner_max_age(app_name)
if updated_content == content do
Mix.shell().info(
"✅ Oban configuration already up-to-date (queues, cron plugin, and pruner max_age present)"
)
else
Mix.shell().info("✅ Updated Oban configuration (queues, cron plugin, pruner retention)")
end
Rewrite.Source.update(source, :content, updated_content)
end
# Ensure posts queue exists in the queues list
defp ensure_posts_queue(content, app_name) do
# Check if posts queue already exists
if Regex.match?(~r/posts:\s*\d+/, content) do
Mix.shell().info(" ℹ️ Posts queue already configured")
content
else
Mix.shell().info(" ➕ Adding posts queue to Oban configuration...")
# Find the ACTIVE queues configuration (not commented out)
# Pattern: line starts with 'config' (not #), then matches queues block
case Regex.run(
~r/(^config\s+:#{app_name},\s+Oban.*?queues:\s*\[)(.*?)(\n\s*\])/ms,
content,
capture: :all
) do
[full_match, before_queues, queues_content, after_queues] ->
Mix.shell().info(" ✓ Found queues block, adding posts queue")
# Remove trailing whitespace and check for comma
trimmed_content = String.trim_trailing(queues_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
# Add posts queue with proper formatting (no comments to avoid syntax issues)
new_queue_entry =
if has_trailing_comma do
"\n posts: 10"
else
",\n posts: 10"
end
updated_queues = before_queues <> queues_content <> new_queue_entry <> after_queues
String.replace(content, full_match, updated_queues, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse queues block for :#{app_name} - skipping posts queue update"
)
Mix.shell().error(" Please manually add: posts: 10")
content
end
end
end
# Ensure sitemap queue exists in the queues list
defp ensure_sitemap_queue(content, app_name) do
# Check if sitemap queue already exists
if Regex.match?(~r/sitemap:\s*\d+/, content) do
Mix.shell().info(" ℹ️ Sitemap queue already configured")
content
else
Mix.shell().info(" ➕ Adding sitemap queue to Oban configuration...")
# Find the ACTIVE queues configuration (not commented out)
case Regex.run(
~r/(^config\s+:#{app_name},\s+Oban.*?queues:\s*\[)(.*?)(\n\s*\])/ms,
content,
capture: :all
) do
[full_match, before_queues, queues_content, after_queues] ->
Mix.shell().info(" ✓ Found queues block, adding sitemap queue")
# Remove trailing whitespace and check for comma
trimmed_content = String.trim_trailing(queues_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
# Add sitemap queue with proper formatting (no comments to avoid syntax issues)
new_queue_entry =
if has_trailing_comma do
"\n sitemap: 5"
else
",\n sitemap: 5"
end
updated_queues = before_queues <> queues_content <> new_queue_entry <> after_queues
String.replace(content, full_match, updated_queues, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse queues block for :#{app_name} - skipping sitemap queue update"
)
Mix.shell().error(" Please manually add: sitemap: 5")
content
end
end
end
# Ensure sqs_polling queue exists in the queues list
defp ensure_sqs_polling_queue(content, app_name) do
# Check if sqs_polling queue already exists
if Regex.match?(~r/sqs_polling:\s*\d+/, content) do
Mix.shell().info(" ℹ️ SQS polling queue already configured")
content
else
Mix.shell().info(" ➕ Adding sqs_polling queue to Oban configuration...")
# Find the ACTIVE queues configuration (not commented out)
case Regex.run(
~r/(^config\s+:#{app_name},\s+Oban.*?queues:\s*\[)(.*?)(\n\s*\])/ms,
content,
capture: :all
) do
[full_match, before_queues, queues_content, after_queues] ->
Mix.shell().info(" ✓ Found queues block, adding sqs_polling queue")
# Remove trailing whitespace and check for comma
trimmed_content = String.trim_trailing(queues_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
# Add sqs_polling queue with proper formatting (no comments to avoid syntax issues)
new_queue_entry =
if has_trailing_comma do
"\n sqs_polling: 1"
else
",\n sqs_polling: 1"
end
updated_queues = before_queues <> queues_content <> new_queue_entry <> after_queues
String.replace(content, full_match, updated_queues, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse queues block for :#{app_name} - skipping sqs_polling queue update"
)
Mix.shell().error(" Please manually add: sqs_polling: 1")
content
end
end
end
# Ensure shop_imports queue exists in the queues list
defp ensure_shop_imports_queue(content, app_name) do
# Check if shop_imports queue already exists
if Regex.match?(~r/shop_imports:\s*\d+/, content) do
Mix.shell().info(" ℹ️ shop_imports queue already configured")
content
else
Mix.shell().info(" ➕ Adding shop_imports queue to Oban configuration...")
# Find the ACTIVE queues configuration (not commented out)
case Regex.run(
~r/(^config\s+:#{app_name},\s+Oban.*?queues:\s*\[)(.*?)(\n\s*\])/ms,
content,
capture: :all
) do
[full_match, before_queues, queues_content, after_queues] ->
Mix.shell().info(" ✓ Found queues block, adding shop_imports queue")
# Remove trailing whitespace and check for comma
trimmed_content = String.trim_trailing(queues_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
# Add shop_imports queue with proper formatting
new_queue_entry =
if has_trailing_comma do
"\n shop_imports: 2"
else
",\n shop_imports: 2"
end
updated_queues = before_queues <> queues_content <> new_queue_entry <> after_queues
String.replace(content, full_match, updated_queues, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse queues block for :#{app_name} - skipping shop_imports queue update"
)
Mix.shell().error(" Please manually add: shop_imports: 2")
content
end
end
end
# Ensure newsletters_delivery queue exists in the queues list
defp ensure_newsletters_delivery_queue(content, app_name) do
if Regex.match?(~r/newsletters_delivery:\s*\d+/, content) do
Mix.shell().info(" ℹ️ newsletters_delivery queue already configured")
content
else
Mix.shell().info(" ➕ Adding newsletters_delivery queue to Oban configuration...")
case Regex.run(
~r/(^config\s+:#{app_name},\s+Oban.*?queues:\s*\[)(.*?)(\n\s*\])/ms,
content,
capture: :all
) do
[full_match, before_queues, queues_content, after_queues] ->
Mix.shell().info(" ✓ Found queues block, adding newsletters_delivery queue")
trimmed_content = String.trim_trailing(queues_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
new_queue_entry =
if has_trailing_comma do
"\n newsletters_delivery: 10"
else
",\n newsletters_delivery: 10"
end
updated_queues = before_queues <> queues_content <> new_queue_entry <> after_queues
String.replace(content, full_match, updated_queues, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse queues block for :#{app_name} - skipping newsletters_delivery queue update"
)
Mix.shell().error(" Please manually add: newsletters_delivery: 10")
content
end
end
end
# Ensure Pruner has max_age configured for 30-day retention
defp ensure_pruner_max_age(content, _app_name) do
# Check if max_age is already configured
if Regex.match?(~r/Oban\.Plugins\.Pruner.*max_age:/s, content) do
Mix.shell().info(" ℹ️ Pruner max_age already configured")
content
else
# Check for bare Oban.Plugins.Pruner (without tuple)
if Regex.match?(~r/Oban\.Plugins\.Pruner\s*[,\]]/, content) do
Mix.shell().info(" ➕ Adding max_age to Oban.Plugins.Pruner...")
# Replace bare Pruner with tuple form including max_age
Regex.replace(
~r/Oban\.Plugins\.Pruner(\s*)(,|\])/,
content,
"{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30}\\1\\2 # Keep jobs for 30 days"
)
else
# Check for tuple form without max_age: {Oban.Plugins.Pruner}
if Regex.match?(~r/\{Oban\.Plugins\.Pruner\}/, content) do
Mix.shell().info(" ➕ Adding max_age to {Oban.Plugins.Pruner}...")
String.replace(
content,
"{Oban.Plugins.Pruner}",
"{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30} # Keep jobs for 30 days"
)
else
Mix.shell().info(" ℹ️ Pruner configuration not found or already has options")
content
end
end
end
end
# Ensure cron plugin exists in the plugins list
defp ensure_cron_plugin(content, app_name) do
cond do
# Case 1: Old worker exists - REPLACE it with new worker
String.contains?(content, "PublishScheduledPostsJob") ->
Mix.shell().info(
" 🔄 Replacing PublishScheduledPostsJob with ProcessScheduledJobsWorker..."
)
String.replace(
content,
"PhoenixKit.Posts.Workers.PublishScheduledPostsJob",
"PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker"
)
# Case 2: Cron plugin exists with new worker - already configured
String.contains?(content, "Oban.Plugins.Cron") and
String.contains?(content, "ProcessScheduledJobsWorker") ->
Mix.shell().info(" ℹ️ Cron plugin and ProcessScheduledJobsWorker already configured")
content
# Case 3: Cron plugin exists but no scheduled jobs worker - add new worker
String.contains?(content, "Oban.Plugins.Cron") ->
Mix.shell().info(
" ➕ Adding ProcessScheduledJobsWorker to existing cron configuration..."
)
add_scheduled_posts_job_to_crontab(content)
# Case 4: No cron plugin at all - add entire plugin with new worker
true ->
Mix.shell().info(" ➕ Adding Oban.Plugins.Cron with ProcessScheduledJobsWorker...")
add_cron_plugin_to_plugins(content, app_name)
end
end
# Add ProcessScheduledJobsWorker to existing crontab
defp add_scheduled_posts_job_to_crontab(content) do
# Pattern: crontab: [...] within Cron plugin
case Regex.run(~r/(crontab:\s*\[)(.*?)(\])/s, content, capture: :all) do
[full_match, before_crontab, crontab_content, after_crontab] ->
# Check if crontab is empty or has entries
has_entries = String.trim(crontab_content) != ""
new_job_entry =
if has_entries do
",\n {\"* * * * *\", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}"
else
"\n {\"* * * * *\", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}\n "
end
updated_crontab = before_crontab <> crontab_content <> new_job_entry <> after_crontab
String.replace(content, full_match, updated_crontab, global: false)
_ ->
content
end
end
# Add Cron plugin to plugins list
defp add_cron_plugin_to_plugins(content, app_name) do
# Find the ACTIVE plugins block - must not be commented out
# Pattern: line starts with spaces (not #), then plugins: [
case Regex.run(
~r/(^[ \t]+plugins:\s*\[\n)(.*?)(\n[ \t]+\])/ms,
content,
capture: :all
) do
[full_match, plugins_open, plugins_content, plugins_close] ->
Mix.shell().info(" ✓ Found plugins block, adding Cron plugin")
# Check if content ends with comma
trimmed_content = String.trim(plugins_content)
has_trailing_comma = String.ends_with?(trimmed_content, ",")
# Add cron plugin with proper formatting (matching existing indentation)
cron_plugin =
if has_trailing_comma do
"\n {Oban.Plugins.Cron,\n crontab: [\n {\"* * * * *\", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}\n ]}"
else
",\n {Oban.Plugins.Cron,\n crontab: [\n {\"* * * * *\", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}\n ]}"
end
updated_plugins = plugins_open <> plugins_content <> cron_plugin <> plugins_close
String.replace(content, full_match, updated_plugins, global: false)
nil ->
Mix.shell().error(
" ⚠️ Could not parse plugins block for :#{app_name} - skipping cron plugin update"
)
Mix.shell().error(" Please manually add Oban.Plugins.Cron configuration")
content
end
end
# Get repo module from PhoenixKit config or detect from app
defp get_repo_module(igniter) do
config_path = "config/config.exs"
app_name = IgniterHelpers.get_parent_app_name(igniter)
if File.exists?(config_path) do
content = File.read!(config_path)
# First try: Look for existing PhoenixKit repo config
case Regex.run(~r/config :phoenix_kit,\s+repo:\s+([A-Za-z0-9_.]+)/, content) do
[_, repo] ->
repo
_ ->
# Second try: Look for ecto_repos in app config
app_module = Macro.camelize(to_string(app_name))
case Regex.run(~r/config :#{app_name}.*?ecto_repos:\s*\[([A-Za-z0-9_.]+)\]/s, content) do
[_, repo] -> repo
_ -> "#{app_module}.Repo"
end
end
else
app_module = Macro.camelize(to_string(app_name))
"#{app_module}.Repo"
end
rescue
_ ->
app_name = IgniterHelpers.get_parent_app_name(igniter)
app_module = Macro.camelize(to_string(app_name))
"#{app_module}.Repo"
end
# Check if Oban config already exists in the file
defp oban_config_already_exists?(content, app_name) do
lines = String.split(content, "\n")
Enum.any?(lines, fn line ->
trimmed = String.trim(line)
# Not a comment and contains config for Oban
# Also check for variations with spaces
!String.starts_with?(trimmed, "#") and
(String.contains?(line, "config :#{app_name}, Oban") or
Regex.match?(~r/config\s+:#{app_name},\s+Oban/, line))
end)
end
# Find the location to insert config before import_config statements
defp find_import_config_location(content) do
lines = String.split(content, "\n")
# Look for import_config pattern
import_index =
Enum.find_index(lines, fn line ->
trimmed = String.trim(line)
String.starts_with?(trimmed, "import_config") or String.contains?(line, "import_config")
end)
case import_index do
nil ->
# No import_config found, append to end
:append_to_end
index ->
# Find the start of the import_config block
start_index = find_import_block_start(lines, index)
# Split content at the start of import block
before_lines = Enum.take(lines, start_index)
after_lines = Enum.drop(lines, start_index)
before_content = Enum.join(before_lines, "\n")
after_content = Enum.join(after_lines, "\n")
{:before_import, before_content, after_content}
end
end
# Find the start of the import_config block (including preceding comments)
defp find_import_block_start(lines, import_index) do
lines
|> Enum.take(import_index)
|> Enum.reverse()
|> Enum.reduce_while(import_index, fn line, current_index ->
trimmed = String.trim(line)
cond do
# Comment line related to import
String.starts_with?(trimmed, "#") and
(String.contains?(line, "import") or String.contains?(line, "Import") or
String.contains?(line, "bottom") or String.contains?(line, "BOTTOM") or
String.contains?(line, "environment")) ->
{:cont, current_index - 1}
# Blank line
trimmed == "" ->
{:cont, current_index - 1}
# config_env or similar
String.contains?(line, "config_env()") or String.contains?(line, "env_config") ->
{:cont, current_index - 1}
# Stop at any other code
true ->
{:halt, current_index}
end
end)
end
# Add notice about Oban configuration
defp add_oban_configuration_notice(igniter) do
if oban_config_exists?(igniter) do
Igniter.add_notice(
igniter,
"""
⚙️ Oban configured for background jobs (file processing, emails, sitemap, sqs_polling)
If queues were added/updated, restart your server to apply changes.
"""
|> String.trim()
)
else
Igniter.add_notice(
igniter,
"""
⚠️ Oban configuration added to config.exs
IMPORTANT: Restart your server to apply configuration changes.
"""
|> String.trim()
)
end
end
@doc """
Adds Oban to the parent application's supervision tree.
This function ensures that Oban starts automatically when the application starts,
with correct positioning in the supervisor tree:
- AFTER PhoenixKit.Supervisor (PhoenixKit services available)
- BEFORE Endpoint (Oban ready before HTTP requests)
## Important
Oban MUST start AFTER PhoenixKit.Supervisor because PhoenixKit.Supervisor
depends on Repo, and Oban also depends on Repo. The correct order is:
1. Repo (database connection)
2. PhoenixKit.Supervisor (uses Repo for Settings)
3. Oban (uses Repo for job persistence)
## Parameters
- `igniter` - The igniter context
## Returns
Updated igniter with Oban added to application supervisor.
"""
def add_oban_supervisor(igniter) do
app_name = IgniterHelpers.get_parent_app_name(igniter)
{igniter, endpoint} = Phoenix.select_endpoint(igniter)
# Build AST for: Application.get_env(:app_name, Oban)
# Using Sourceror to parse the code string into AST
get_env_code = "Application.get_env(:#{app_name}, Oban)"
get_env_ast = Sourceror.parse_string!(get_env_code)
# Use Igniter API to add Oban with explicit positioning
# Pass {Module, {:code, ast}} format so Igniter doesn't escape the AST
# This ensures correct order: Repo → PhoenixKit → Oban → Endpoint
igniter
|> Application.add_new_child(
{Oban, {:code, get_env_ast}},
after: [PhoenixKit.Supervisor],
before: [endpoint]
)
end
@doc """
Checks if Oban supervisor is configured in application.ex.
## Parameters
- `igniter` - The igniter context for detecting parent app name
## Returns
Boolean indicating if Oban supervisor exists in application.ex.
"""
def oban_supervisor_exists?(igniter) do
app_name = IgniterHelpers.get_parent_app_name(igniter)
app_file = "lib/#{app_name}/application.ex"
if File.exists?(app_file) do
content = File.read!(app_file)
# Check for Oban in children list
String.contains?(content, "{Oban,") or
String.contains?(content, "Application.get_env(:#{app_name}, Oban)")
else
false
end
rescue
_ -> false
end
# Add notice when manual configuration is required
defp add_manual_config_notice(igniter, repo_module) do
app_name = IgniterHelpers.get_parent_app_name(igniter)
notice = """
⚠️ Manual Configuration Required: Oban
PhoenixKit couldn't automatically configure Oban for background jobs.
Please add the following to config/config.exs:
config :#{app_name}, Oban,
repo: #{repo_module},
queues: [
default: 10,
emails: 50,
file_processing: 20,
posts: 10,
scheduled_jobs: 1,
sitemap: 5,
sqs_polling: 1,
newsletters_delivery: 10
],
plugins: [
# Pruner: delete completed/discarded jobs after 30 days
{Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30},
{Oban.Plugins.Cron,
crontab: [
{"* * * * *", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}
]}
]
And add the following to lib/#{app_name}/application.ex in the children list:
{Oban, Application.get_env(:#{app_name}, Oban)}
IMPORTANT: Restart your server after making these changes.
Without this configuration, the storage system cannot process uploaded files,
scheduled posts will not be published automatically, sitemap generation
will not work asynchronously, and SQS polling for email events will not function.
"""
Igniter.add_notice(igniter, notice)
end
end