Add AshOnetime.Resource to an AshPostgres resource, then protect each effectful, transactional action explicitly. There is no default strategy and no implicit scope.

use Ash.Resource,
  domain: MyApp.Domain,
  data_layer: AshPostgres.DataLayer,
  extensions: [AshOnetime.Resource]

onetime do
  protect :charge do
    strategy :idempotency
    scope [{:tenant, MyApp.TenantScope}, {:static, "charge"}]
    key [{:client, :request_key}, {:attribute, :account_id}]
    fingerprint arguments: [:amount], attributes: [:account_id]
    response MyApp.ChargeCodec, fields: [:id, :status], classify: MyApp.Classifier
    retention {24, :hour}
    on_definite_store_failure :fail_closed
    limits max_cache_entry_bytes: 1_048_576
  end

  protect :redeem do
    strategy :one_time_nonce
    scope [{:static, "redemption"}]
    key {:verified, :proof, MyApp.Verifier}
    window max_age: {10, :minute}, clock_skew: {15, :second}
  end
end

Closed component types

Scope accepts {:static, value}, {:argument, name}, {:attribute, name}, and {:tenant, resolver}. A runtime resolver must return nonempty canonical data; a missing tenant or argument is an error, never a global scope.

Keys accept {:client, argument}, {:argument, argument}, {:external, argument}, {:attribute, attribute}, {:verified, argument, verifier}, and {:minted, minter}. Nonce protection requires only verified or minted sources. Verification callbacks return AshOnetime.Verified; action input cannot supply trusted timestamps, algorithms, or verification state. Choose either verified sources or one minted source for a nonce key; a minted source cannot be combined with another source because fresh minting would change the authoritative replay key on every attempt. Idempotency keys, by contrast, must be deterministic for the same logical operation: the same request must yield the same key so admission can match a prior claim. A nondeterministic source such as a fresh {:minted, minter} would produce a new key on every attempt and never match, defeating idempotency; use stable sources like {:client, ...}, {:argument, ...}, {:attribute, ...}, or a {:verified, ...} proof whose descriptor is stable.

Idempotency requires fingerprint, response, and positive retention. The response codec receives an explicit field allowlist and a classifier that chooses store, reject, or rollback. on_definite_store_failure :execute_untracked is available only to idempotency and only for proof that admission was never dispatched. limits bounds canonical input and cache payload work.

Nonce requires window and deliberately has no response, retention, external-effect, or failure-direction option. external_effect is idempotency-only and requires a recoverable adapter.

Compile-time validation rejects read or nontransactional actions, duplicate protections, empty scope, missing references, reserved verification inputs, unsafe replay callbacks, excessive bounds, and every strategy-incompatible option before the resource loads. Use AshOnetime.Resource.Info.protection/2 and protections/1 for normalized read-only introspection.

The generated Spark DSL reference is the exact option shape.