A real SaaS application has work that should not happen inside a controller. Sending a welcome email, rebuilding a usage report, or expiring an invitation token should run in the background where a slow third party or a crashing process does not take a request down with it.
In this tutorial I install the oban feature with Live SaaS Kit, run the
generated migration, and look at the queues it configures so I know where
my own workers should live. The starting point is a Phoenix application
that already uses Postgres for its main repo.
Step 1 - Install the oban feature
First step is to run the installer.
mix saaskit.feature.install oban
mix deps.get
The feature has no dependencies on other features, so it installs cleanly
on a bare Phoenix project. Two packages land in mix.exs.
# mix.exs
{:oban, "~> 2.20.0"},
{:oban_web, "~> 2.11.0"},
Note that oban_web is included, which means I get the live job
dashboard, not just the queue runner. If the admin feature is installed,
the dashboard is mounted at /admin/oban and a link is added to the admin
sidebar. Without admin, it is mounted at /oban in the main router
scope.
Step 2 - Run the migration
Next step is to run the generated migration.
mix ecto.migrate
The migration file is a single line that delegates to Oban.
# priv/repo/migrations/TIMESTAMP_add_oban_jobs_table.exs
def up do
Oban.Migration.up(version: 12)
end
def down do
Oban.Migrations.down(version: 1)
end
The reason is that Oban manages its own schema. Oban.Migration.up/1
creates the oban_jobs table and the indexes the queue runner needs.
Pinning version: 12 keeps the schema deterministic across environments
even when the library upgrades.
Step 3 - Look at the config
The installer adds a config block to config/config.exs that defines the
repo, the queues, and the plugins.
# config/config.exs
config :my_app, Oban,
engine: Oban.Engines.Basic,
repo: MyApp.Repo,
queues: [default: 10, mailers: 20, high: 50, low: 5],
plugins: [
{Oban.Plugins.Pruner, max_age: 3600 * 24},
{Oban.Plugins.Cron,
crontab: [
# {"0 2 * * *", MyApp.ExampleWorker},
]}
]
There are four named queues with explicit concurrency. The mailers queue
at 20 is sized for transactional email; high at 50 is for fast,
latency-sensitive work; low at 5 is for heavy batch jobs that should
not crowd the others. So when I write a worker, I pick the queue that
matches its character.
The Pruner removes completed jobs older than 24 hours. The Cron plugin
is wired in with an empty crontab — that is where I add scheduled jobs as
the app grows.
The supervisor is updated automatically too.
# lib/my_app/application.ex
{Oban, Application.fetch_env!(:my_app, Oban)},
And tests are kept synchronous so they do not race against the queue.
# config/test.exs
config :my_app, Oban, testing: :manual
:manual mode means jobs do not run during tests unless I drain the
queue explicitly with Oban.drain_queue/1, or assert via
Oban.Testing.assert_enqueued/1.
Step 4 - Write a small worker
The installer does not generate a sample worker, so I write one to confirm
the queue is alive. I put it under lib/my_app/workers/.
# lib/my_app/workers/hello_worker.ex
defmodule MyApp.Workers.HelloWorker do
use Oban.Worker, queue: :default
@impl Oban.Worker
def perform(%Oban.Job{args: %{"name" => name}}) do
IO.puts("hello, #{name}")
:ok
end
end
Then I enqueue it from iex.
# iex -S mix
%{name: "world"} |> MyApp.Workers.HelloWorker.new() |> Oban.insert!()
The server log shows hello, world from the queue worker process, not
from the iex shell. So I have confirmed that a job persisted to the
database, was picked up by the supervisor, and ran in the background.
Step 5 - Open the Oban dashboard
If admin is installed, I sign in as a superuser and open
/admin/oban. The dashboard shows the queues from step 3, the job I
inserted in step 4, and any failures with their full error trace.
If admin is not installed, the dashboard is at /oban in the main
router scope, and I am responsible for putting it behind authentication.
Note that oban_web does not enforce any access control on its own.
Final Result
I now have a Phoenix application that can enqueue background work, watch
it run in a real dashboard, and recover failures without leaving the app.
A natural next step is to install error_tracker so any crash inside an
Oban worker also lands in the error dashboard.