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

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:




Essenify – Product Bundles

Bundle smarter. Sell faster. Earn more.

One of the best ways to increase your Average Order Value (AOV) this BFCM is through smart product bundling. But bundling isn’t just about grouping random products together – it’s a strategic move.

Essenify – Product Bundles helps you identify which products are most frequently purchased together by analyzing your store’s trends. You can follow these smart recommendations or freely create your own custom bundles in just a few clicks.

Our app has processed over 1M+ orders with 952+ merchants, maintaining perfect inventory accuracy – even during massive BFCM sales. Whether you’re managing thousands of bundle orders or building a “Mix & Match” experience with Build Your Box, Essenify ensures your inventory and sales stay perfectly aligned.

Our app is fully supported with Subscription apps, Order Printer, and many native Shopify functionalities with different bundle types:

This BFCM, let your customers design their own bundles and watch your revenue rise effortlessly.

FAQ Expert: Page & Product FAQs

Reduce support load. Empower customers. Increase trust.

During BFCM, your support inbox can overflow with repetitive questions – “What’s your return policy?” “When will my order ship?” “Is this product compatible with…?”

Answer them all proactively with FAQ Expert: Page & Product FAQs.

This app helps you reduce inquiries, save support time, and increase customer confidence. You can add FAQs to product pages, policy pages, or anywhere else on your site.

The app is compatible with FAQSchema, which will help you with SEO and AEO as well.

By giving shoppers instant answers, you’re not only saving your team’s time – you’re helping customers make faster, more confident purchase decisions during the busiest sales season of the year.

Discountify

Master the art of discounts and own the BFCM season.

Let’s face it – BFCM and discounts go hand in hand. But offering discounts blindly can eat into your profits. That’s where Discountify shines.

This app helps you craft advanced discount strategies tailored to your products and audience.
Use Volume Discounts to encourage bulk buying for everyday essentials, Bundle Discounts to move slow-selling inventory, or Tiered Discounts to reward higher spending – “the more they buy, the more they save.”

Discountify helps you maximize sales while maintaining control and profitability, turning discounts into a powerful growth tool rather than a margin risk.

Smart Suggest Pro by Essenify

Turn product recommendations into conversion engines.

Cross-selling is one of the simplest yet most overlooked ways to grow sales during BFCM.

While Shopify’s built-in recommendation engine only shows “recently viewed” products, Smart Suggest Pro goes further – it lets you create strategic product suggestions based on your own insights, product relationships, and marketing goals.

You decide what to promote and when – not an algorithm.
This way, you guide customers through your store like a curated shopping experience, not a random browsing session.

This BFCM, use Smart Suggest Pro to subtly plant ideas in your customers’ minds – and watch them add “just one more thing” to their cart.

Quiz Buddy – Product Quiz

Engage, entertain, and convert – all at once.

With so many discounts and ads competing for attention, shoppers often hop from one store to another within seconds. To keep them engaged, you need something interactive and personal – something like Quiz Buddy – Product Quiz.

This app helps you create fun, interactive quizzes that not only entertain visitors but also guide them toward the right products. Based on quiz results, you can offer personalized discounts – giving shoppers a reason to stay longer and buy.

Bonus? It improves your SEO because visitors spend more time on your site, signaling to search engines that your store is engaging and relevant.

Why Choose Essenify This BFCM?

Each Essenify app tackles a key part of the eCommerce growth puzzle:

Essenify – Product

Bundles

: Boosts your AOV

FAQ Expert

: Reduces support and increases trust and SEO

Discountify

: Drives strategic sales growth

Smart Suggest Pro

: Improves cross-selling and personalization

Quiz Buddy

: Engages and retains visitors

Together, they create a complete BFCM powerhouse toolkit – helping you attract, engage, and convert customers more effectively than ever.

Share this Blog

Share this Blog

Sachin Gevariya

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

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