ExShopifyApp.AccessToken.Repo (ex_shopify_app v1.0.0)
Ecto-backed, cross-node-safe store for Shopify offline access tokens.
Mix this into a module in the host application, pointing it at the app's Ecto.Repo:
defmodule MyApp.ShopifyAccessTokens do
use ExShopifyApp.AccessToken.Repo,
repo: MyApp.Repo
endThe generated module implements the ExShopifyApp.AccessToken.Store behaviour and
exposes:
fetch_token/1— read the stored token, or{:error, :no_token}.put_token/2— upsert a token byshopify_domain(initial install / re-auth).valid_token/2— the safe primary API: returns a usable token, refreshing under a lock only when necessary (see below).refresh_token/2— run a locked refresh decision for a shop: under the row lock it re-reads the token and only calls Shopify if a refresh is still needed, otherwise it returns the already-current stored token.
Safety model
Refreshing rotates both the access and refresh tokens, so refresh_token/2 runs
inside Repo.transaction/2 under a SELECT ... FOR UPDATE row lock and persists the
new token before the transaction commits, serializing concurrent refreshes across
nodes. The error taxonomy, telemetry, the host-app migration contract, and the
unavoidable post-response crash window are covered in the README and this module
documentation.
valid_token/2 decision table
- No row →
{:error, :no_token}. - Refresh token expired →
{:error, :reauthorization_required}(no HTTP call). - Fresh token → returned as-is (no lock, no HTTP call).
- Hard-expired token → blocking locked refresh.
- Stale token → locked refresh; on failure, returns
{:error, reason}by default. Passstale_while_error: trueto return the still-valid old token (with telemetry) when the refresh fails but the token is not yet hard-expired.
Options
:skew— hard-expiry skew in seconds (seeExShopifyApp.AccessToken.Token.expired?/3).:soft_window— keyword opts for the soft window (seeToken.stale?/3).:timeout— transaction timeout in milliseconds.:lock_timeout—SET LOCAL lock_timeoutin milliseconds; on timeout the call returns{:error, {:lock_timeout, reason}}.:stale_while_error— seevalid_token/2above (defaultfalse).
Summary
Functions
Fetch the stored token for a shop domain via repo.
Upsert token by shopify_domain via repo.
Run the locked refresh decision for shop via repo.
Return a usable token for shop, refreshing under a lock only when necessary.
Types
@type shop() :: %{shopify_domain: String.t()}
A shop reference carrying at least its :shopify_domain.
Functions
@spec fetch_token(module(), String.t()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, :no_token}
Fetch the stored token for a shop domain via repo.
@spec put_token(module(), String.t(), ExShopifyApp.AccessToken.Token.t()) :: :ok | {:error, term()}
Upsert token by shopify_domain via repo.
@spec refresh_token(module(), shop(), keyword()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, term()}
Run the locked refresh decision for shop via repo.
This does not unconditionally call Shopify. Inside a Repo.transaction/2 it takes a
SELECT ... FOR UPDATE lock on the shop's row, re-reads the token, and:
- returns
{:ok, token}with no Shopify call if the token is no longer stale/expired (e.g. a concurrent caller already refreshed it while we waited on the lock — this is what collapses many concurrent callers into a single refresh); - otherwise calls Shopify and synchronously persists the new token before the transaction commits.
Emits [:ex_shopify_app, :access_token, :refresh] :start/:stop/:exception
telemetry. See docs/access-token-refresh-safety.md for the error taxonomy.
@spec valid_token(module(), shop(), keyword()) :: {:ok, ExShopifyApp.AccessToken.Token.t()} | {:error, term()}
Return a usable token for shop, refreshing under a lock only when necessary.
See the module docs for the decision table and options.