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.comps-direct-fulfilment-online.myshopify.comps-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
Clientmodel (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:
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¶
- Who owns the Shopify Partners account that the OMS app is registered under? Email-of-record? Login? 2FA holder?
- Are there any non-PopSockets shops connected that we don't know
about? Run
SELECT shop_url FROM shopify_app_admin_clientsagainst prod to confirm. - Is there a sandbox / development Shopify shop we can use for migration rehearsal without touching live shops?
- 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, < 24in its gemspec while the outer Gemfile pinsshopify_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.