Migrating a Shopify Ruby on Rails App to Expiring Offline Access Tokens

Migrate your Shopify Ruby on Rails app to expiring offline access tokens. Step-by-step technical guide covering database migrations, model storage updates, context setup, and background task management.

Shopify Apps

10 min

With Shopify requiring all public apps to transition to expiring offline access tokens by January 1, 2027, developers must update their application architectures. In the Rails ecosystem, the official shopify_app and shopify_api gems provide the necessary helpers to handle this flow.

This guide walks through the database migrations, configuration updates, and background worker logic needed to adopt token rotation using standard Shopify tools.


Token Lifetime and Expiration Rules :

When implementing token rotation, keep these strict token lifetimes in mind:

  • Access Token (1 Hour): Once generated, the access token is short-lived, with a standard lifespan of 1 hour. The gem automatically handles refreshing it before it expires.

  • Refresh Token (90 Days of Inactivity): The refresh token operates on a sliding window of 90 days. Each successful token refresh resets this 90-day timer. However, if a merchant does not open or interact with your app for 90 consecutive days and no background API tasks are executed to trigger a refresh, the refresh token will expire, requiring complete re-authentication.

Step 1: Update the Gems

To access built-in support for token rotation, ensure your application is running shopify_app version 23.0. (or higher) and shopify_api version 16.0 (or higher).

Update your Gemfile:

gem 'shopify_app', '>= 23.0'
gem 'shopify_api', '>= 16.0'
gem 'shopify_app', '>= 23.0'
gem 'shopify_api', '>= 16.0'
gem 'shopify_app', '>= 23.0'
gem 'shopify_api', '>= 16.0'

Run bundle update to apply the updates.

Step 2: Database Migration

Your session storage model (typically the Shop or Session model) needs new columns to hold the access token expiration timestamp, the refresh token, and the refresh token's expiration timestamp.

Run the official generator to add the necessary columns:

rails generate shopify_app:shop_model --skip
rails generate shopify_app:shop_model --skip
rails generate shopify_app:shop_model --skip

(Using --skip ensures the generator only creates the database migration for missing fields instead of regenerating the model file.)

Alternatively, generate a migration manually:

class AddExpiringTokenFieldsToShops < ActiveRecord::Migration[7.0]
  def change
    change_table :shops do |t|
      t.datetime :expires_at
      t.string :refresh_token
      t.datetime :refresh_token_expires_at
    end
  end
end
class AddExpiringTokenFieldsToShops < ActiveRecord::Migration[7.0]
  def change
    change_table :shops do |t|
      t.datetime :expires_at
      t.string :refresh_token
      t.datetime :refresh_token_expires_at
    end
  end
end
class AddExpiringTokenFieldsToShops < ActiveRecord::Migration[7.0]
  def change
    change_table :shops do |t|
      t.datetime :expires_at
      t.string :refresh_token
      t.datetime :refresh_token_expires_at
    end
  end
end

Run rails db:migrate to update your schema.

Step 3: Update the Shop Model Concern

To support token rotation automatically, you must switch your model to use the modern ShopifyApp::ShopSessionStorage concern. This concern replaces deprecated storage methods.

Modify app/models/shop.rb:

# Remove deprecated concern:
# include ShopifyApp::ShopSessionStorageWithScopes

# Add standard concern:
include ShopifyApp::ShopSessionStorage
# Remove deprecated concern:
# include ShopifyApp::ShopSessionStorageWithScopes

# Add standard concern:
include ShopifyApp::ShopSessionStorage
# Remove deprecated concern:
# include ShopifyApp::ShopSessionStorageWithScopes

# Add standard concern:
include ShopifyApp::ShopSessionStorage

The standard ShopSessionStorage concern dynamically detects the presence of the refresh_token and expires_at database columns and automatically handles the token refresh flow.

Step 4: Configure the Shopify API Context

You must configure the shopify_api gem context to request and handle expiring tokens during new installs and session lookups.

Update your ShopifyAPI::Context.setup configuration (usually located in config/initializers/shopify_app.rb):

ShopifyAPI::Context.setup(
  api_key: ShopifyApp.configuration.api_key,
  api_secret_key: ShopifyApp.configuration.secret,
  api_version: ShopifyApp.configuration.api_version,
  host: ShopifyApp.configuration.host,
  scope: ShopifyApp.configuration.scope,
  is_private: false,
  is_embedded: true,
  # Enable expiring offline access tokens
  expiring_offline_access_tokens: true

ShopifyAPI::Context.setup(
  api_key: ShopifyApp.configuration.api_key,
  api_secret_key: ShopifyApp.configuration.secret,
  api_version: ShopifyApp.configuration.api_version,
  host: ShopifyApp.configuration.host,
  scope: ShopifyApp.configuration.scope,
  is_private: false,
  is_embedded: true,
  # Enable expiring offline access tokens
  expiring_offline_access_tokens: true

ShopifyAPI::Context.setup(
  api_key: ShopifyApp.configuration.api_key,
  api_secret_key: ShopifyApp.configuration.secret,
  api_version: ShopifyApp.configuration.api_version,
  host: ShopifyApp.configuration.host,
  scope: ShopifyApp.configuration.scope,
  is_private: false,
  is_embedded: true,
  # Enable expiring offline access tokens
  expiring_offline_access_tokens: true

Step 5: Migrating Existing Non-Expiring Tokens

For shops that already have your app installed, you need to perform a one-time exchange to trade their existing permanent access token for an expiring/refresh token pair.

The Exchange Helper

You can perform the exchange programmatically using standard SDK methods:

new_session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
  shop: 'merchant-store.myshopify.com',
  non_expiring_offline_access_token: 'existing_permanent_token'

new_session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
  shop: 'merchant-store.myshopify.com',
  non_expiring_offline_access_token: 'existing_permanent_token'

new_session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
  shop: 'merchant-store.myshopify.com',
  non_expiring_offline_access_token: 'existing_permanent_token'

This returns a ShopifyAPI::Auth::Session object containing the new access_token, expires_at, and refresh_token.

Migration Script Pattern

You can write a Rake task or database migration script to loop through existing shops and exchange their tokens:

Shop.find_each do |shop|
  next if shop.refresh_token.present? # Already migrated

  begin
    session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
      shop: shop.shopify_domain,
      non_expiring_offline_access_token: shop.shopify_token
    )

    # Save the new token credentials returned by Shopify
    shop.update!(
      shopify_token: session.access_token,
      expires_at: session.expires_at,
      refresh_token: session.refresh_token,
      refresh_token_expires_at: session.refresh_token_expires_at
    )
  rescue => e
    Rails.logger.error("Failed to migrate shop #{shop.shopify_domain}: #{e.message}")
  end
end
Shop.find_each do |shop|
  next if shop.refresh_token.present? # Already migrated

  begin
    session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
      shop: shop.shopify_domain,
      non_expiring_offline_access_token: shop.shopify_token
    )

    # Save the new token credentials returned by Shopify
    shop.update!(
      shopify_token: session.access_token,
      expires_at: session.expires_at,
      refresh_token: session.refresh_token,
      refresh_token_expires_at: session.refresh_token_expires_at
    )
  rescue => e
    Rails.logger.error("Failed to migrate shop #{shop.shopify_domain}: #{e.message}")
  end
end
Shop.find_each do |shop|
  next if shop.refresh_token.present? # Already migrated

  begin
    session = ShopifyAPI::Auth::TokenExchange.migrate_to_expiring_token(
      shop: shop.shopify_domain,
      non_expiring_offline_access_token: shop.shopify_token
    )

    # Save the new token credentials returned by Shopify
    shop.update!(
      shopify_token: session.access_token,
      expires_at: session.expires_at,
      refresh_token: session.refresh_token,
      refresh_token_expires_at: session.refresh_token_expires_at
    )
  rescue => e
    Rails.logger.error("Failed to migrate shop #{shop.shopify_domain}: #{e.message}")
  end
end


Step 6: Background Jobs & API Requests

When executing API requests in background workers (e.g., Sidekiq/ActiveJob) or controller actions, wrap your calls in the standard session helper.

shop.with_shopify_session do
  # Perform API actions
  ShopifyAPI::Product.all
end
shop.with_shopify_session do
  # Perform API actions
  ShopifyAPI::Product.all
end
shop.with_shopify_session do
  # Perform API actions
  ShopifyAPI::Product.all
end

Automatic Token Refreshing

Under the hood, with_shopify_session will load the session credentials, verify if the access_token is expired or near expiration, and automatically use the stored refresh_token to retrieve a new pair. It then saves the new rotated tokens to the database before executing your API block.

Stale Token Prevention in Job Arguments

Do not pass raw access tokens as arguments to your background queues (e.g., perform_async(token)). Since tokens expire and rotate, background jobs must always fetch the latest credentials dynamically from the database by passing the shop's ID (e.g., perform_async(shop_id)).

Step 7: Handling Re-Authentication

If a token refresh fails for instance, if the refresh token itself has expired due to 90 days of inactivity the API will raise a ShopifyAPI::Errors::RefreshTokenExpiredError.

Here is how you handle re-authentication across different scopes:

A. In Web Requests (Controllers)

When using standard shopify_app controllers (e.g., controllers inheriting from ShopifyApp::AuthenticatedController or including ShopifyApp::EnsureInstalled), the gem automatically catches authorization errors and handles redirects to the login/OAuth flow. No manual code is required.

B. In Background Jobs

Because background workers run out-of-band without a browser session, they cannot redirect the user. You must catch the error, log it, and flag the shop in your database as needing re-authentication (e.g., setting a needs_reauth flag or clearing the invalid token).




When the merchant next opens the Shopify Admin dashboard, your embedded frontend will detect the missing/invalid session and guide them through standard OAuth re-authentication to retrieve a new set of tokens.

Official Documentation Links

For further details and updates on the token rotation APIs:

Say Hello To

Essenify

Tell us about your project and we are ready

to transform your idea into

stunning digital experiences

Share this Blog
Written by
Sachin Gevariya

Sachin Gevariya is a Founder and Technical Director at Essence Solusoft. He is dedicated to making the best use of modern technologies to craft end-to-end solutions. He also has a vast knowledge of Cloud management. He loves to do coding so still doing the coding. Also, help employees for quality based solutions to clients. Always eager to learn new technology and implement for best solutions.

Say Hello To

Essenify

Tell us about your project and we are ready to transform your idea into stunning digital experiences

&

&

footer-logo

solutions@essenify.com

+91-8866572265

Contact

Product Bundles

Discountify

FAQ Expert

Quiz Buddy

Smart Suggest Pro

Facebook

LinkedIn

X

Instagram

DMCA.com Protection Status

©2026 Essenify. All Rights Reserved. Powered by Essenify.

Privacy

Terms of use

Sitemap

footer-logo

solutions@essenify.com

+91-8866572265

Contact

Product Bundles

Discountify

FAQ Expert

Quiz Buddy

Smart Suggest Pro

Facebook

LinkedIn

X

Instagram

DMCA.com Protection Status

©2026 Essenify. All Rights Reserved. Powered by Essenify.

Privacy

Terms of use

Sitemap