Advanced Usage
View SourceThis guide covers advanced patterns and techniques for using Usher in production applications.
Invitation Cleanup Strategies
While Usher doesn't include built-in cleanup functionality, you can implement cleanup strategies to manage expired invitations.
Basic Cleanup Module
Create a reusable cleanup module:
defmodule MyApp.InvitationCleanup do
@moduledoc """
A module for cleaning up expired invitations.
This can be used with a job scheduler like Oban or simply a GenServer
to periodically remove expired invitations from the database.
"""
@doc """
Removes expired invitations older than the given number of days.
This is useful if you want to keep recently expired invitations
for debugging or analytics purposes.
"""
def cleanup_old_expired_invitations(days_old \\ 30) do
cutoff_date = DateTime.utc_now() |> DateTime.add(-days_old, :day)
old_expired_invitations =
Usher.list_invitations()
|> Enum.filter(fn invitation ->
DateTime.compare(invitation.expires_at, cutoff_date) == :lt
end)
deleted_count =
old_expired_invitations
|> Enum.reduce(0, fn invitation, acc ->
# Alternatively, you can use your application's repo to
# `Repo.delete_all/2` expired invitations in bulk.
case Usher.delete_invitation(invitation) do
{:ok, _} -> acc + 1
{:error, _} -> acc
end
end)
deleted_count
end
endManual Cleanup
For one-off cleanup operations:
# Remove all expired invitations
iex> MyApp.InvitationCleanup.cleanup_old_expired_invitations(0)
15
# Remove invitations expired more than 30 days ago
iex> MyApp.InvitationCleanup.cleanup_old_expired_invitations(30)
3Cleanup with Oban (Recommended)
For applications using Oban job processing:
defmodule MyApp.Workers.InvitationCleanupWorker do
@moduledoc """
A worker that periodically cleans up old expired invitations.
This worker can be scheduled to run periodically with Oban Cron.
"""
use Oban.Worker, queue: :cleanup
@impl Oban.Worker
def perform(%Oban.Job{}) do
deleted_count = MyApp.InvitationCleanup.cleanup_old_expired_invitations(7)
{:ok, deleted_count}
end
endAdd to your Oban cron configuration:
# config/config.exs
config :my_app, Oban,
repo: MyApp.Repo,
plugins: [
{Oban.Plugins.Cron,
crontab: [
# Clean up expired invitations daily at 2 AM
{"0 2 * * *", MyApp.Workers.InvitationCleanupWorker}
]}
]Cleanup with GenServer
For applications wanting a self-contained cleanup process:
defmodule MyApp.InvitationCleanupCron do
@moduledoc """
A GenServer that periodically cleans up expired invitations.
Runs cleanup tasks at configurable intervals and can be
added to your application's supervision tree.
"""
use GenServer
require Logger
# Default cleanup interval: 24 hours
@default_cleanup_interval_ms 24 * 60 * 60 * 1000
@default_days_old 7
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(opts) do
cleanup_interval = Keyword.get(opts, :cleanup_interval_ms, @default_cleanup_interval_ms)
days_old = Keyword.get(opts, :days_old, @default_days_old)
schedule_cleanup(cleanup_interval)
state = %{
cleanup_interval: cleanup_interval,
days_old: days_old,
last_cleanup: nil
}
Logger.info("InvitationCleanupCron started with #{cleanup_interval}ms interval")
{:ok, state}
end
@impl GenServer
def handle_info(:cleanup, state) do
deleted_count = MyApp.InvitationCleanup.cleanup_old_expired_invitations(state.days_old)
new_state = %{
state |
last_cleanup: DateTime.utc_now()
}
Logger.info("Cleanup completed. Deleted #{deleted_count} invitations.")
# Schedule the next cleanup
schedule_cleanup(state.cleanup_interval)
{:noreply, new_state}
end
defp schedule_cleanup(interval) do
Process.send_after(self(), :cleanup, interval)
end
endAdd to your application supervision tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# ... other children
{MyApp.InvitationCleanupCron, [
cleanup_interval_ms: 2 * 60 * 60 * 1000, # 2 hours
days_old: 14 # Delete invitations expired more than 14 days ago
]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endComplex Validation Patterns
Multi-Step Validation
defmodule MyApp.InvitationValidator do
def validate_with_context(token, context \\ %{}) do
with {:ok, invitation} <- Usher.validate_invitation_token(token),
:ok <- check_usage_limits(invitation, context) do
{:ok, invitation}
end
end
defp check_usage_limits(invitation, context) do
max_uses = Map.get(context, :max_uses, 100)
invitation_usage_count =
invitation
|> Usher.list_invitation_usages_by_unique_entity(action: :joined)
|> Enum.count()
if invitation_usage_count >= max_uses do
{:error, :usage_limit_exceeded}
else
:ok
end
end
end
# Usage
case MyApp.InvitationValidator.validate_with_context("abc123", %{
max_uses: 50
}) do
{:ok, invitation} -> proceed_with_registration(invitation)
{:error, reason} -> handle_validation_error(reason)
end