ExShopifyApp.AccessToken.Store behaviour (ex_shopify_app v1.0.0)
Behaviour for durably persisting and safely refreshing offline access tokens.
Refreshing an expiring offline token rotates both the access token and the
refresh token; Shopify invalidates the previous refresh token once it accepts the
request. The new refresh token must therefore be persisted durably before it is
handed back to any caller, and concurrent refreshes for one shop must be serialized
across all processes and nodes. That serialization is inherently store-specific
(e.g. a SELECT ... FOR UPDATE row lock), which is why refresh_token/2 is part
of this behaviour rather than a generic, store-agnostic manager.
ExShopifyApp.AccessToken.Repo provides the production implementation backed by a
host application's Ecto.Repo. Non-Ecto applications can implement this behaviour
against any datastore that offers an equivalent cross-node lock.
Summary
Callbacks
Fetch the stored token for a shop domain, or {:error, :no_token}.
Durably persist (upsert) the token for a shop domain.
Run a locked refresh decision for shop behind a per-shop, cross-node lock.
Return a usable token for shop, refreshing under the lock only when necessary.
Types
@type shop() :: %{shopify_domain: String.t()}
A shop reference carrying at least its :shopify_domain.
Callbacks
@callback fetch_token(shopify_domain :: String.t()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, term()}
Fetch the stored token for a shop domain, or {:error, :no_token}.
@callback put_token( shopify_domain :: String.t(), token :: ExShopifyApp.AccessToken.Token.t() ) :: :ok | {:error, term()}
Durably persist (upsert) the token for a shop domain.
@callback refresh_token(shop :: shop(), opts :: keyword()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, term()}
Run a locked refresh decision for shop behind a per-shop, cross-node lock.
Implementations must take the lock, re-read the token, and only call the refresh
endpoint when a refresh is still needed — if the token is already current (e.g.
another caller refreshed it while this one waited on the lock), the stored token is
returned without a network call. When a refresh does happen, the new token must be
durably persisted before it is returned. See ExShopifyApp.AccessToken.Repo for the
reference implementation and error taxonomy.
@callback valid_token(shop :: shop(), opts :: keyword()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, term()}
Return a usable token for shop, refreshing under the lock only when necessary.
The safe primary API: implementations read the stored token and decide whether it can
be served as-is or must be refreshed via refresh_token/2 (a fresh token is
returned with no lock or network call). See ExShopifyApp.AccessToken.Repo for the
reference implementation, decision table, and options.