<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LunJaiii</title>
    <description>The latest articles on DEV Community by LunJaiii (@lunjaiii).</description>
    <link>https://dev.to/lunjaiii</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4017087%2F19f22b7c-4f4d-4a46-a31f-f8096bdf5be6.png</url>
      <title>DEV Community: LunJaiii</title>
      <link>https://dev.to/lunjaiii</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lunjaiii"/>
    <language>en</language>
    <item>
      <title>Your Shopify app's real OAuth problem isn't Shopify — it's the other API</title>
      <dc:creator>LunJaiii</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:17:38 +0000</pubDate>
      <link>https://dev.to/lunjaiii/your-shopify-apps-real-oauth-problem-isnt-shopify-its-the-other-api-2b14</link>
      <guid>https://dev.to/lunjaiii/your-shopify-apps-real-oauth-problem-isnt-shopify-its-the-other-api-2b14</guid>
      <description>&lt;p&gt;Every Shopify tutorial covers the same OAuth dance: merchant installs, you get a token, done. The framework handles 100% of it.&lt;/p&gt;

&lt;p&gt;Then you build a real app. A real app &lt;em&gt;connects to something&lt;/em&gt; — Xero, QuickBooks, Klaviyo, an ERP, a shipping API — because an app that only talks to Shopify is rarely worth $19/month. And that second OAuth integration is 100% yours. No framework, no guardrails.&lt;/p&gt;

&lt;p&gt;I shipped a Shopify-to-Xero sync app. Here are the four failure modes that only showed up days or weeks after things "worked", and the patterns that survive them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Refresh tokens rotate — persist BOTH tokens, every refresh
&lt;/h2&gt;

&lt;p&gt;Many providers (Xero included) issue a &lt;strong&gt;new refresh token every time you use one&lt;/strong&gt;. If your refresh handler only saves the new access token, everything works... until the next refresh, when your stored refresh token turns out to be single-use and already spent. The connection dies silently. The merchant blames your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;partnerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shop&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encryptSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encryptSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// rotation!&lt;/span&gt;
    &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expires_in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Refresh tokens die of loneliness — keep-alive cron
&lt;/h2&gt;

&lt;p&gt;Xero's refresh tokens expire after ~60 idle days. Think about who goes idle: a merchant with a seasonal shop installs in January, goes quiet, comes back for the summer rush — dead connection, angry merchant, support ticket.&lt;/p&gt;

&lt;p&gt;Fix: a weekly cron that force-refreshes every stored connection, regardless of expiry. Our &lt;code&gt;getValidConnection(shop, { force: true })&lt;/code&gt; flag exists purely for this keep-alive job.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Refresh proactively, not on 401
&lt;/h2&gt;

&lt;p&gt;Refreshing 60 seconds &lt;em&gt;before&lt;/em&gt; expiry instead of reacting to a 401 saves you a failed call + retry — which matters a lot during a webhook burst (a flash sale means dozens of order webhooks, each wanting an API call).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;force&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;decrypted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// still fresh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// otherwise refresh now, before the API call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Rate limits: evenly-spaced throttle + Retry-After
&lt;/h2&gt;

&lt;p&gt;Partner APIs limit per tenant (Xero: 60/min). You don't control your traffic — Shopify webhooks do. Two layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Layer 1: never hit the limit — evenly space calls&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;nextSlot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gapMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;CALLS_PER_MINUTE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextSlot&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;nextSlot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;nextSlot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;gapMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Layer 2: if a 429 slips through, honour Retry-After (up to 3 attempts)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Retry-After&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retryAfter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bonus gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The consent screen can't run inside the Shopify admin iframe.&lt;/strong&gt; Open it in a new tab, and carry the shop domain through the round-trip in the &lt;code&gt;state&lt;/code&gt; param — the callback lands in a bare tab with no session you can trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt tokens at rest.&lt;/strong&gt; Shopify's protected-customer-data expectations (and basic hygiene) mean your partner's refresh token shouldn't sit in plaintext in Postgres. AES-256-GCM with a versioned prefix lets you turn encryption on after launch without a migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design the reconnect UX on day one.&lt;/strong&gt; Connections die (revoked, expired, account closed). A null connection should render a "Reconnect" button, not crash a dashboard — and failed jobs should self-heal via a retry cron once the merchant reconnects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;These patterns are lifted from the production code of my Shopify-to-Xero app, and they're the core of chapter 4 of &lt;a href="https://approvedkit.com" rel="noopener noreferrer"&gt;ApprovedKit&lt;/a&gt; — a boilerplate that layers production-tested modules (billing, GDPR webhooks, this whole OAuth module, a $5 VPS deploy stack) on top of Shopify's official template, plus a live log of our App Store review as it happens.&lt;/p&gt;

&lt;p&gt;Early access is $99 for the first 30 seats at &lt;a href="https://approvedkit.com" rel="noopener noreferrer"&gt;approvedkit.com&lt;/a&gt;. And even if you never buy anything: persist both tokens. Future you will thank present you.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>oauth</category>
      <category>node</category>
      <category>saas</category>
    </item>
    <item>
      <title>Your brand-new Shopify app template ships with a TypeScript error (and the one-line fix)</title>
      <dc:creator>LunJaiii</dc:creator>
      <pubDate>Mon, 06 Jul 2026 06:16:00 +0000</pubDate>
      <link>https://dev.to/lunjaiii/your-brand-new-shopify-app-template-ships-with-a-typescript-error-and-the-one-line-fix-2n4o</link>
      <guid>https://dev.to/lunjaiii/your-brand-new-shopify-app-template-ships-with-a-typescript-error-and-the-one-line-fix-2n4o</guid>
      <description>&lt;p&gt;Run this today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shopify app init   &lt;span class="c"&gt;# choose Remix&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;your-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and there's a good chance you'll be greeted by this wall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;app/shopify.server.ts: error TS2322: Type 'PrismaSessionStorage&amp;lt;PrismaClient&amp;lt;...&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'
&lt;/span&gt;&lt;span class="go"&gt;is not assignable to type 'SessionStorage'.
  Types of property 'storeSession' are incompatible.
&lt;/span&gt;&lt;span class="c"&gt;    ...
&lt;/span&gt;&lt;span class="go"&gt;      Types have separate declarations of a private property 'compressedScopes'.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You haven't written a single line of code yet. Welcome to Shopify app development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually happening
&lt;/h2&gt;

&lt;p&gt;That last line is the tell: &lt;strong&gt;"separate declarations of a private property"&lt;/strong&gt; means TypeScript is comparing two different copies of the &lt;em&gt;same&lt;/em&gt; class from two different places in &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;ls&lt;/span&gt; @shopify/shopify-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a fresh template you'll see two versions. As of this writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@shopify/shopify-app-session-storage-prisma@8&lt;/code&gt; depends on &lt;code&gt;@shopify/shopify-api@^12&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@shopify/shopify-app-remix@4&lt;/code&gt; bundles &lt;code&gt;@shopify/shopify-api@^13&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those ranges don't overlap, so npm installs &lt;strong&gt;both&lt;/strong&gt; — one at the top level, one nested under &lt;code&gt;shopify-app-remix&lt;/code&gt;. Your &lt;code&gt;PrismaSessionStorage&lt;/code&gt; is built against v12's &lt;code&gt;Session&lt;/code&gt; class; the &lt;code&gt;shopifyApp()&lt;/code&gt; config expects v13's. Same class name, different declarations, TypeScript (correctly) refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that don't fix it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm dedupe&lt;/code&gt; — can't collapse a genuine semver conflict. Tried it, still duplicated.&lt;/li&gt;
&lt;li&gt;Deleting &lt;code&gt;node_modules&lt;/code&gt; and reinstalling — same resolution, same result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;as any&lt;/code&gt; on the session storage — compiles, and now you've turned off type checking on the exact seam where your auth sessions live.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-line fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @shopify/shopify-app-session-storage-prisma@^9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;v9 peers against &lt;code&gt;@shopify/shopify-api@^13&lt;/code&gt;. The nested copy disappears, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;ls&lt;/span&gt; @shopify/shopify-api   &lt;span class="c"&gt;# exactly ONE version&lt;/span&gt;
npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;              &lt;span class="c"&gt;# green&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The related mystery cast you'll see in older code
&lt;/h2&gt;

&lt;p&gt;If you've read open-source Shopify apps, you may have seen billing plan names cast like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;MONTHLY_PLAN&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// ← why??&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same root cause. With two &lt;code&gt;@shopify/shopify-api&lt;/code&gt; copies, billing plan-name type inference breaks, and &lt;code&gt;as never&lt;/code&gt; was the workaround that let people ship. With a single copy, the cast is unnecessary (though harmless).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm writing this down
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://approvedkit.com" rel="noopener noreferrer"&gt;ApprovedKit&lt;/a&gt; — a Shopify app boilerplate extracted from a real production app (currently going through App Store review, and we're logging the whole review process live). While verifying that our modules apply cleanly to a &lt;em&gt;fresh&lt;/em&gt; official template, we hit this exact error on a clean clone — which means every new Shopify app developer hits it too.&lt;/p&gt;

&lt;p&gt;That's the kit's whole philosophy: every gotcha we hit becomes a documented fix at the exact line it matters. If that sounds useful, the early-access version is &lt;a href="https://approvedkit.com" rel="noopener noreferrer"&gt;$99 for the first 30 seats&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy shipping — and run &lt;code&gt;npm ls @shopify/shopify-api&lt;/code&gt; before you trust your types.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
