Skip to content
Build bf98f58

Shopify Integration: What It Is and How It Cuts Over

OMS is a multi-tenant Shopify app, not just an app that calls Shopify's API. This doc explains what that means, what's portable, and what needs attention during cutover.

Architecture

The OMS Rails app is registered as a Shopify Partners app. Specific PopSockets-owned shops install the app, OAuth runs, and we store an access token per shop. Each connected shop becomes a row in shopify_app_admin_clients.

From config/engine.yml, the allowable_sites list is three shops:

  • ps-direct-fulfilment-offline.myshopify.com
  • ps-direct-fulfilment-online.myshopify.com
  • ps-diff-direct-fulfillment.myshopify.com

It's a small, fixed set — not a public marketplace install.

Code ownership — important

The bulk of the integration lives in a private Rails engine vendored in-tree at engines/shopify-app-admin/. From its gemspec:

s.authors = ['eShopAdmin Inc.', 'Diff Agency Inc.']
s.email   = ['support@diffagency.com']
s.summary = 'Our private admin and shop sync stuff.'

Diff wrote this engine. The source is already in this repo, so we don't need anything from Diff to keep it running. But future maintenance (bug fixes, Shopify API version bumps, security patches) becomes our responsibility once the relationship ends.

What the engine provides:

  • Multi-tenant Client model (per-shop OAuth + sync state)
  • User / session / invitation management
  • Subscription billing scaffolding (Charges, Plans, Subscriptions)
  • Webhook registration manager (overrides ShopifyApp::WebhooksManager)
  • Shopify resource models (shopify_orders, shopify_products, shopify_variants, shopify_customers, shopify_collects, shopify_fulfillments, shopify_locations, shopify_price_rules, shopify_refunds, shopify_smart_collections, shopify_custom_collections, shopify_customer_saved_searches, shopify_line_items)
  • Inventory audit table

Where each piece lives

┌─────────────────────────────────────────────────────────────────┐
│   ON SHOPIFY (out of our control, configured via Partners UI)   │
│                                                                 │
│   • Partners app entry: api_key, api_secret, app URL, OAuth     │
│     redirect URL                                                │
│   • Per-shop webhook subscriptions (URL targets stored on       │
│     Shopify's side; they push to us)                           │
└─────────────────────────────────────────────────────────────────┘
                                │ HMAC-signed HTTPS
┌─────────────────────────────────────────────────────────────────┐
│   IN THIS REPO (✅ already portable)                             │
│                                                                 │
│   • engines/shopify-app-admin/                                  │
│   • config/initializers/shopify_app.rb                          │
│   • app/jobs/shopify/                                           │
│   • app/services/shopify/                                       │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│   IN OUR MYSQL (✅ comes over with DB migration)                 │
│                                                                 │
│   • shopify_app_admin_clients (3 rows, one per shop)            │
│     - shop_url                                                  │
│     - token  ← OAuth access token, the irreplaceable bit        │
│     - sync state timestamps                                     │
│   • shopify_app_admin_users / invitations / sessions            │
│   • shopify_app_admin_webhooks                                  │
│   • shopify_app_admin_shopify_orders / products / variants /    │
│     customers / collections / fulfillments / refunds            │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│   IN ENCRYPTED CREDENTIALS (✅ comes over with RAILS_MASTER_KEY) │
│                                                                 │
│   • shopify_api_key                                             │
│   • shopify_secret                                              │
│   • shopify_deprecated_secret  (for credential rotation grace)  │
└─────────────────────────────────────────────────────────────────┘

Cutover risks, ranked

🔴 Risk 1: Who owns the Shopify Partners app?

The Partners dashboard entry holding the api_key and secret belongs to some Shopify Partners account. We need to confirm whose account that is.

Scenario Effort Impact
PopSockets-owned account Just get the login None — purely operational
Diff-owned account Request "transfer ownership" via Shopify's flow Clerical; ~1 day
Can't get access at all Create a new Partners app, regenerate api_key + secret, each shop uninstalls + reinstalls Multiple hours of coordinated downtime per shop. All Client.token values invalidated; new tokens issued at reinstall.

Action: confirm Partners app ownership before scheduling the cutover. Owner's identity must be in the migration runbook.

Even in the worst case (rebuild the Partners app), historical data is safe — orders, products, customers in our DB are not affected. What would change is the OAuth identity Shopify uses to recognize us.

🟡 Risk 2: Webhook URL stability

Webhook target URLs are computed at runtime in engines/shopify-app-admin/config/initializers/shopify_app/webhooks_manager.rb:

{ topic:, address: "#{CONFIG[:host]}/webhooks/#{topic.gsub('/', '_')}", format: 'json' }

CONFIG[:host] comes from config/engine.yml via app/services/fetch_configurations.rb.

If hostname stays the same (oms.popsockets.com keeps pointing at us, just to the new ingress instead of the old ELB) → zero Shopify-side change.

If hostname changes → all 3 shops need their webhooks re-registered. The good news: config/initializers/shopify_app.rb:24 calls ShopifyApp::WebhooksManager.add_registrations on every app boot. So as soon as the new k8s pods boot with a new CONFIG[:host], they register fresh webhooks pointing at the new URL.

Recommendation: keep the same hostname during cutover. It collapses this risk to nothing.

🟢 Risk 3: TLS + HMAC

Shopify expects: 1. Valid TLS — handled by cert-manager on the new ingress. 2. HMAC signature verifiable with our shopify_secret — survives via RAILS_MASTER_KEY. 3. Endpoint reachable from Shopify — public ingress, no allowlist needed.

All three are mechanical and survive the migration.

What about data loss?

Data Lives Survives?
Synced Shopify orders DB
Synced products / variants / customers DB
OAuth access tokens per shop Client.token ✅ if we keep the Partners app
Webhook subscriptions on Shopify's side Shopify ✅ Auto re-registered on boot
Partners app credentials Encrypted credentials ✅ via master key
Partners dashboard config Shopify ✅ if we own the account

The only loss scenario is if we recreate the Partners app — and that's credentials, not historical data.

Cutover sequence (assuming same hostname)

T-2 weeks: Confirm Partners app ownership; if Diff-owned, initiate
           ownership transfer to a PopSockets Partners account.

T-1 week:  In staging, point a test shop at the new k8s ingress.
           Verify: OAuth flow works, webhook delivery works,
           a sync job runs against the new DB.

T:         DB migration cutover (see database-migration.md).
           DNS flip: oms.popsockets.com → new k8s ingress.
           Web pods boot → WebhooksManager.add_registrations runs →
           Shopify sees webhooks already pointing here → no-op.

T+5 min:   Smoke test: trigger a known webhook from a shop, confirm
           the corresponding Sidekiq job processes correctly against
           the new DB.

T+1 day:   Monitor per-shop sync error rate (Datadog).

T+1 week:  Decommission old infra.

Cutover sequence (if hostname must change)

Same as above, plus:

  • Update the app URL and OAuth redirect URL in the Shopify Partners dashboard before cutover.
  • After DNS flip, manually trigger ShopifyAppAdmin::WebhooksManagerJob.perform_now(shop_url) per shop to ensure webhooks point at the new URL (or rely on the boot-time call).
  • For 24 hours after cutover, watch for any inbound webhook attempts hitting the old hostname (those would be requests Shopify hadn't yet seen the re-registration for — should be zero, but verify).

Open questions to flag with leadership / Diff

  1. Who owns the Shopify Partners account that the OMS app is registered under? Email-of-record? Login? 2FA holder?
  2. Are there any non-PopSockets shops connected that we don't know about? Run SELECT shop_url FROM shopify_app_admin_clients against prod to confirm.
  3. Is there a sandbox / development Shopify shop we can use for migration rehearsal without touching live shops?
  4. Are there any custom Shopify scripts / functions / app extensions tied to this app that aren't in this repo?

Long-term ownership

The shopify-app-admin engine is a Diff-built artifact. Once Diff is no longer in the picture:

  • Bug fixes, Shopify API version bumps (currently 2025-07), and security patches all become PopSockets's responsibility.
  • The engine pins shopify_app >= 5.0.0, < 24 in its gemspec while the outer Gemfile pins shopify_app ~> 23.0 — these are compatible today but will drift. Plan for a long-term refactor to either fold the engine inline or replace it with a maintained equivalent.
  • This is not a migration blocker. Just a longer-term technical-debt item to surface to leadership.