Available from ash_onetime 1.1.0.
AshOnetime.Transaction is the public boundary for a host that already owns one authoritative
Ecto transaction. It applies the same PostgreSQL-authoritative idempotency and nonce semantics
without wrapping the host effect in an Ash action.
The boundary never starts or commits a transaction. The caller must already be inside a
PostgreSQL READ COMMITTED transaction. Admission, the host mutation, its audit record, and the
exact replay bytes therefore commit or roll back together.
Idempotency
alias AshOnetime.Transaction
MyApp.Repo.transaction(fn ->
options = [
operation: {MyApp.Management, :execute},
partition: tenant_id,
scope: principal_id,
key: idempotency_key,
fingerprint: :crypto.hash(:sha256, canonical_request_bytes),
retention_seconds: 86_400,
codec: "myapp.management-response.v1"
]
case Transaction.idempotency(MyApp.Repo, options) do
{:execute, admission} ->
exact_response_bytes = MyApp.Management.execute_and_encode!()
:ok = Transaction.complete(admission, exact_response_bytes)
{:fresh, exact_response_bytes}
{:replay, exact_response_bytes} ->
{:replay, exact_response_bytes}
{:error, error} ->
MyApp.Repo.rollback(error)
end
end)The same partition/scope/key with a different fingerprint returns
:key_reused_with_different_request. A matching incomplete claim returns
:request_in_progress. Replay validates the stored codec and SHA-256 payload digest before
returning bytes.
One-time nonce
verified = [
%AshOnetime.Verified{
key: nonce,
issued_at: signed_created_at,
expires_at: signed_expires_at,
verifier_id: "management-gateway"
}
]
:ok =
AshOnetime.Transaction.nonce(MyApp.Repo,
operation: {MyApp.Management, :signed_request_nonce},
partition: tenant_id,
scope: credential_id,
key: nonce,
verified: verified,
max_age: 300,
clock_skew: 0
)Every verified fact must carry the exact nonce key. A collision returns
:nonce_already_used. The nonce spend rolls back when the caller transaction rolls back.
Authority identity
operationis a local{module, action}atom pair. It is domain-separated and hashed; it is not caller input.partitionisolates tenant or authority-plane ownership in one store installation.scopeidentifies the principal or credential authority within that partition.keyis the idempotency key or nonce within that scope.fingerprintis an exact 32-byte digest supplied by the host's canonical request boundary.
partition, scope, and key are nonempty bounded UTF-8 binaries with no NUL bytes. Options
are closed: unknown or duplicate keys fail with :invalid_request. The optional prefix
selects an existing PostgreSQL schema; it does not replace logical partitioning.
Installation and upgrade
Fresh install migrations include logical partitions. Existing v1.0 installations run:
mix ash_onetime.gen.logical_partitions --repo MyApp.Repo
mix ecto.migrate
The upgrade backfills existing rows to global. Its down migration refuses while non-global
claims or payloads exist, preventing a rollback from merging distinct authorities. See
Upgrading and Operations.