We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
Screencast
35. Schedule Delete
Permanently delete database records after a delay — with the option to undo
Deleting a record immediately is straightforward, but it leaves no room for recovery. A common approach is to mark a record as deleted first, then remove it from the database after a short delay. This gives users a window to undo the action before it becomes permanent.
This screencast walks through building that pattern in Phoenix LiveView using Ecto, Oban, and Phoenix PubSub — from the database migration to the real-time table update when the job finally runs.
What this covers
- Adding a
deleted_attimestamp column to soft-delete records instead of removing them immediately - Scheduling a hard delete with Oban, with a configurable delay before permanent removal
- Using Phoenix PubSub to broadcast deletions and update the LiveView table in real time — without a page refresh
- Applying conditional CSS classes in the table component to visually mark deleted rows, using Tailwind's parent-targeting syntax
- Implementing an undo function that clears
deleted_at, which the Oban job checks before proceeding with deletion - Extending a shared table component with a
row_deletedattribute without breaking other usages of the same component
A taste of what you'll build
Scheduling the hard delete through Oban after soft-deleting a record:
def soft_delete_brewery(%Brewery{} = brewery) do
changeset = Ecto.Changeset.change(brewery, %{deleted_at: DateTime.utc_now(:second)})
with {:ok, brewery} <- Repo.update(changeset),
{:ok, _job} <- schedule_hard_delete(brewery) do
{:ok, brewery}
end
end
defp schedule_hard_delete(brewery) do
%{id: brewery.id}
|> HardDeleteBreweryWorker.new(schedule_in: 60)
|> Oban.insert()
end
The Oban worker that runs the permanent delete — and handles the case where the record was already restored:
def perform(%Oban.Job{args: %{"id" => id}}) do
case Breweries.hard_delete_brewery(id) do
{:ok, _brewery} -> :ok
{:error, :not_found} -> :ok
end
end
The not_found case is intentional. If a user undoes the deletion before the job runs, deleted_at is cleared and the hard delete query finds nothing — the job completes cleanly either way.