ExShopifyApp.AccessToken.Token (ex_shopify_app v1.0.0)
The canonical persisted representation of a Shopify offline access token.
This is an Ecto schema keyed by shopify_domain: there is exactly one offline
token chain per shop/app installation. It captures the full response from an
expiring token exchange or refresh, the absolute timestamps at which the access
token and refresh token expire, and operational refresh metadata.
Lifetime (non-expiring) tokens leave :expires_at and :refresh_token_expires_at
as nil; such tokens are never considered expired or stale.
The struct redacts :access_token and :refresh_token from inspect/2 output so
token values never leak into logs.
Docs: https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens
Summary
Functions
Returns the fields accepted by changeset/2.
Changeset for persisting a token (initial exchange / reauthorization upsert).
Returns true when the access token has hard-expired (within skew seconds).
Builds a token struct from a decoded token-exchange/refresh response body.
Normalizes a shop domain the same way ExShopifyApp.AccessToken.client/1 does:
strips a leading https:// so the stored key matches the host used for requests.
Builds the update changeset applied after a successful refresh.
Returns true when the refresh token has expired. Once this is true the merchant
must re-launch the app to obtain a new token; refreshing is no longer possible.
Returns the fields replaced during an upsert conflict.
Returns true when the access token is inside the proactive soft refresh window
(still valid, but nearing expiry) — a stale-while-revalidate refresh should be
triggered.
Types
@type t() :: %ExShopifyApp.AccessToken.Token{ __meta__: term(), access_token: String.t() | nil, expires_at: DateTime.t() | nil, expires_in: non_neg_integer() | nil, inserted_at: term(), last_refresh_error: String.t() | nil, last_refreshed_at: DateTime.t() | nil, refresh_generation: non_neg_integer(), refresh_token: String.t() | nil, refresh_token_expires_at: DateTime.t() | nil, refresh_token_expires_in: non_neg_integer() | nil, scope: String.t() | nil, shopify_domain: String.t() | nil, updated_at: term() }
An offline access token row with its expiry and refresh metadata.
Functions
@spec castable() :: [atom()]
Returns the fields accepted by changeset/2.
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
Changeset for persisting a token (initial exchange / reauthorization upsert).
Requires shopify_domain and access_token. For expiring tokens (any expiry field
present) it additionally requires refresh_token, expires_at, and
refresh_token_expires_at. Integer durations must be non-negative. The
shopify_domain is normalized consistently with the HTTP client's host handling.
@spec expired?(t(), DateTime.t(), non_neg_integer()) :: boolean()
Returns true when the access token has hard-expired (within skew seconds).
A blocking refresh is required before the token can be used. Lifetime tokens
(expires_at == nil) are never expired.
Builds a token struct from a decoded token-exchange/refresh response body.
Computes absolute expiry timestamps from the expires_in/refresh_token_expires_in
durations relative to the current time. Accepts string-keyed maps (as decoded from
JSON). The result is a transient struct — persist it through a
ExShopifyApp.AccessToken.Store before relying on it.
Normalizes a shop domain the same way ExShopifyApp.AccessToken.client/1 does:
strips a leading https:// so the stored key matches the host used for requests.
@spec prepare_refresh_changes(t(), t()) :: Ecto.Changeset.t()
Builds the update changeset applied after a successful refresh.
Copies the freshly issued values from refreshed onto the locked existing row,
stamps last_refreshed_at, clears last_refresh_error, and uses
Ecto.Changeset.optimistic_lock/3 to increment refresh_generation (and guard
against a concurrent writer). The shopify_domain is preserved.
@spec refresh_token_expired?(t(), DateTime.t()) :: boolean()
Returns true when the refresh token has expired. Once this is true the merchant
must re-launch the app to obtain a new token; refreshing is no longer possible.
@spec replaceable() :: [atom()]
Returns the fields replaced during an upsert conflict.
@spec stale?(t(), DateTime.t(), keyword()) :: boolean()
Returns true when the access token is inside the proactive soft refresh window
(still valid, but nearing expiry) — a stale-while-revalidate refresh should be
triggered.
The window opens once remaining lifetime drops below :fraction of the original
expires_in (default 0.25), plus a deterministic per-shop
jitter of up to :jitter seconds (default 30) so co-issued tokens
stagger their refreshes. Lifetime tokens are never stale.
Options
:fraction- soft-window fraction of total lifetime (default0.25):jitter- max jitter in seconds, spread per shop (default30)