Durable Docket runs can reference app-owned users, accounts, and workflow rows without making the parent application a second persistence driver.
Configure one backend
defmodule MyApp.Docket do
use Docket,
repo: MyApp.Repo,
backend: Docket.Postgres,
tenant_mode: :required,
claim_policy: [
implementation: Docket.Postgres.ClaimPolicy.TenantFair,
default_max_active_runs: 4
],
checkpoint_observers: [MyApp.DocketProjection],
pruner: [
interval_ms: :timer.hours(1),
event_retention_ms: :timer.hours(24 * 30),
run_retention_ms: :timer.hours(24 * 90),
batch_size: 1_000
]
endStart the Repo before the facade in the application's supervision tree:
children = [MyApp.Repo, MyApp.Docket]
Supervisor.start_link(children, strategy: :one_for_one)Docket.Postgres owns graph/run persistence, claiming,
scheduling, and cold recovery. The private ETF state stored by the existing
PostgreSQL stores is not an application wire format.
Publish, then start
Publish an effective graph once and retain its stable reference on the
application's workflow record. Given an application-built graph and workflow
record:
tenant_id = to_string(workflow.account_id)
{:ok, graph_ref} = MyApp.Docket.save_graph(graph, tenant_id: tenant_id)
workflow =
MyApp.Workflows.update_graph_ref!(workflow, %{
graph_id: graph_ref.graph_id,
graph_hash: graph_ref.graph_hash
})Keep graph input separate from app-owned metadata when starting a run:
def run_workflow(user, workflow, attrs) do
# Authorization must establish that this workflow belongs to the user's
# account before its ID becomes the durable storage scope.
tenant_id = to_string(user.account_id)
graph_ref = %Docket.GraphRef{
graph_id: workflow.graph_id,
graph_hash: workflow.graph_hash
}
metadata = %{
"user_id" => user.id,
"account_id" => user.account_id,
"workflow_id" => workflow.id
}
with {:ok, run} <-
MyApp.Docket.start_run(graph_ref, Map.new(attrs),
tenant_id: tenant_id,
metadata: metadata
) do
MyApp.Workflows.link_docket_run!(workflow, run.id)
end
endinput is workflow data. metadata is durable app context that Docket
preserves but does not interpret. Applications should keep metadata terms
portable and free of processes, references, ports, and functions.
The linked workflow must remain in the same authorized account; reads and
signals must derive their tenant ID from that trusted ownership relationship,
not from caller-controlled input.
Read and inspect
Application rows store the Docket run ID and business projections, not a copy of Docket's durable run state:
tenant_id = to_string(workflow.account_id)
{:ok, run} =
MyApp.Docket.fetch_run(workflow.docket_run_id,
tenant_id: tenant_id
)
{:ok, info} =
MyApp.Docket.inspect_run(workflow.docket_run_id,
tenant_id: tenant_id
)fetch_run returns the last committed %Docket.Run{}. inspect_run adds
token-free scheduling and operational health facts.
Administer TenantFair caps
Authorize policy administration in the parent application before calling Docket. The Docket facade deliberately accepts no actor or authorization token and does not persist actor identity:
def change_docket_cap(actor, account, max_active_runs, expected_version) do
:ok = MyApp.Accounts.authorize(actor, :manage_docket_caps, account)
owner_scope = {:tenant, to_string(account.id)}
MyApp.Docket.put_claim_policy_override(owner_scope, max_active_runs,
expected_version: expected_version
)
end
{:ok, override} = change_docket_cap(actor, account, 2, 0)
{:ok, current} =
MyApp.Docket.inspect_claim_policy({:tenant, to_string(account.id)})
{:ok, reset} =
MyApp.Docket.reset_claim_policy_override(
{:tenant, to_string(account.id)},
expected_version: override.version
)Use fetch_claim_policy_default/0 and put_claim_policy_default/2 for the
persisted default. These functions use the configured Repo and prefix; callers
never construct a storage context or depend on PostgreSQL table names.
Project after commit
Checkpoint observers are suitable for best-effort UI projections and notifications. They run after the durable transaction and cannot veto it:
defmodule MyApp.DocketProjection do
@behaviour Docket.Checkpoint.Observer
@impl true
def observe(%Docket.Checkpoint{run: run}, _context) do
MyApp.Workflows.project_status(run.id, run.status, run.updated_at)
:ok
end
endObservers may be lost or duplicated around a crash. Consumers that require a durable delivery guarantee should consume retained events rather than treating the observer as an outbox.
Important boundaries
- Use a stable, non-empty binary
tenant_idfor storage authorization and scope; convert integer application IDs explicitly. - Store business identity in Run metadata, not graph input.
- Store
run.idand app projections in parent tables; do not duplicate the private Run state blob. - Publish graphs explicitly and start runs only from the returned
GraphRef. - Use
fetch_run/inspect_run; v0.1.0 has no host-owned Run map codec or resume orchestration. - Authorize claim-policy changes in the parent application before calling the five public cap operations. Docket does not provide actor persistence, audit history, hold/drain states, bulk changes, weights, borrowing, or preemption.