ex_shopify_app

Your entrypoint to create a Shopify Application in Elixir.

Installation

Add ex_shopify_app to your dependencies in mix.exs:

def deps do
[
{:ex_shopify_app, "~> 1.0"}
]
end

The library uses Ecto for the canonical access-token schema, so :ecto is a required dependency. The durable store (ExShopifyApp.AccessToken.Repo) runs against your application's Ecto.Repo (and therefore your :ecto_sql driver) — the library itself does not pull in ecto_sql or a database driver.

Offline access tokens

Shopify's expiring offline access tokens rotate both the access token and the refresh token on every refresh; the previous refresh token is invalidated as soon as Shopify accepts the request. If the new refresh token is not durably persisted before it is used, the merchant may have to re-authorize the app. The access-token modules are built around that safety concern.

1. Run the migration

Create the shopify_access_tokens table. shopify_domain is the primary key, so there is exactly one canonical token chain per shop/installation:

defmodule MyApp.Repo.Migrations.CreateShopifyAccessTokens do
use Ecto.Migration
def change do
create table(:shopify_access_tokens, primary_key: false) do
add :shopify_domain, :string, primary_key: true
add :access_token, :text, null: false
add :refresh_token, :text
add :scope, :text
add :expires_in, :integer
add :refresh_token_expires_in, :integer
add :expires_at, :utc_datetime_usec
add :refresh_token_expires_at, :utc_datetime_usec
add :last_refreshed_at, :utc_datetime_usec
add :last_refresh_error, :text
add :refresh_generation, :integer, null: false, default: 0
timestamps(type: :utc_datetime_usec)
end
create index(:shopify_access_tokens, [:expires_at])
create index(:shopify_access_tokens, [:refresh_token_expires_at])
end
end

2. Define your store

defmodule MyApp.ShopifyAccessTokens do
use ExShopifyApp.AccessToken.Repo, repo: MyApp.Repo
end

This generates a module implementing ExShopifyApp.AccessToken.Store with fetch_token/1, put_token/2, valid_token/2, and refresh_token/2.

3. Persist the token at install / re-auth

{:ok, token} = ExShopifyApp.AccessToken.fetch(shop, session_token, expiring: true)
:ok = MyApp.ShopifyAccessTokens.put_token(shop.shopify_domain, token)

Don't consider the install complete until put_token/2 returns :ok.

4. Get a usable token

valid_token/2 is the safe primary API. It refreshes under a per-shop, cross-node lock only when needed, and never returns a refreshed token until the new refresh token is durably committed:

case MyApp.ShopifyAccessTokens.valid_token(shop) do
{:ok, token} -> # use token.access_token
{:error, :reauthorization_required} -> # send the merchant back through OAuth
{:error, reason} -> # operational error; retry / escalate
end
StateBehaviour
Fresh tokenReturned as-is — no lock, no HTTP call
Stale tokenLocked refresh; on failure returns {:error, reason} (or the old token with stale_while_error: true)
Hard-expired tokenBlocking locked refresh
Refresh token expired{:error, :reauthorization_required}
No stored token{:error, :no_token}

Safety guarantees

The store provides at-least-once persistence attempt after a refresh response, not absolute never-loss. Refresh runs inside Repo.transaction/2 holding a SELECT ... FOR UPDATE row lock for the whole decision: it re-reads the row under the lock, calls Shopify only if a refresh is still required, and synchronously persists the new token before committing. The lock serializes refreshes across all processes and nodes sharing the database.

The unavoidable residual risk is a VM/host crash after Shopify responds but before the commit — Shopify and your database cannot share a transaction. Failures of the write after a successful Shopify refresh surface as the distinct, critical {:error, {:token_persistence_failed_after_refresh, reason}} and emit telemetry plus an error log (token values redacted).

See docs/access-token-refresh-safety.md for the full specification and docs/access-token-refresh-lease-lock-design.md for a future lease-based alternative to holding the row lock during the HTTP call.

Error taxonomy

Telemetry

Metadata carries :shopify_domain, :refresh_generation, and a :result classification — never token values.

Breaking changes

This release replaces the in-memory ETS token store and the local-only Manager/Refresher GenServers with the database-backed ExShopifyApp.AccessToken.Repo. ExShopifyApp.AccessToken.Token is now an Ecto schema rather than a plain struct, and ExShopifyApp.AccessToken.Store now requires fetch_token/1, put_token/2, and refresh_token/2. ExShopifyApp.AccessToken.fetch/3 takes keyword options (type:, expiring:) and requests an expiring offline token by default.

Documentation can be generated with ExDoc and published on HexDocs.