Shopify App Development: Navigating the Complex World of App States and Billing Lifecycle
Hey everyone! I recently stumbled upon a fantastic discussion in the Shopify community that really resonated with me, and I just had to share the insights. It started with Lars_Mayer, who, like many of us, initially thought the Shopify app installation flow would be a straightforward path: install → OAuth → billing approval → initial sync → ready. Sounds simple enough, right?
But as Lars quickly discovered, building a robust Shopify app, especially one with an embedded UI, API, worker, and its own database, throws a lot more curveballs than that simple flow suggests. The reality is far more nuanced, and the community discussion that followed offered some seriously valuable lessons for anyone building on Shopify.
It's Not One State, It's a Matrix!
Lars’s biggest realization was that an app doesn't have just one state. Instead, you're juggling multiple dimensions simultaneously. Think about it: a shop can be installed, but billing isn't active yet. Or billing is active, but the initial data sync is still grinding away. Or worse, the sync failed halfway through, leaving you with partial data while the merchant is paying for a full service!
This led Lars to move away from a simple installed = yes/no flag and instead build a 'state matrix' for various aspects of the app's lifecycle:
- Installation:
installed / uninstalled - Billing:
none / active / cancelled at period end / expired / frozen / declined - Data:
partial / active / pending redact / deleted - Initial sync:
pending / running / success / failed - Reinstall / recovery:
verification pending / OAuth required / reauth required / support required
The real kicker, as Lars pointed out, isn't the individual states, but their combinations. A shop that is installed + billing active + data partial + initial sync running is a completely different beast from one that's installed + billing active + data active + sync successful. This framework is essential because your UI, API, and background workers need to interpret these combined states to know what they're allowed to do.
Who Owns the Truth? Shopify vs. Your App
A recurring theme in the discussion was the concept of 'source of truth.' Luca_Bartoccini and Lars_Mayer both highlighted that for things like billing and authorization, Shopify is the single source of truth. Your app might cache this state locally for performance or stability, but it should never assume its local copy is the definitive, authoritative version.
Lars added a crucial layer to this: even with Shopify as the source, you need exactly one path in your app that's allowed to reconcile and write that truth locally, and one canonical way for the rest of your app (UI, API, worker) to read it. Why? Because, as Luca experienced, two different code paths reading a raw cached value differently can lead to a merchant seeing an upgraded plan in the UI but still being capped due to an incorrect underlying state. The solution: a single accessor for every cached state, ensuring consistency.
Webhooks: Signals, Not Sole Sources of Truth
This was a huge takeaway from adamcharvat and lumine. Webhooks are fantastic for notifying you that 'something changed,' but they shouldn't be treated as the sole, definitive source of truth about that change. Why? Because they can arrive out of order, or worse, not at all!
Shopify retries failing webhook endpoints for about two days, then removes the subscription. This means your app could look perfectly healthy in your database while receiving absolutely nothing from Shopify. The community consensus is to demote webhooks. Treat them as a signal to 'go re-read the source' (i.e., query the Shopify Admin API). For billing, this means querying currentAppInstallation activeSubscriptions when the merchant opens your app, rather than trusting the last webhook payload.
Another critical point from Luca_Bartoccini about webhooks: the app/uninstalled webhook. He learned the hard way that his handler failed because it relied on a valid shop token, which an uninstalled shop no longer has. The fix? Verify the HMAC by hand for all webhooks and never touch a session inside a webhook handler.
Bulletproofing Your App: Scopes, Syncs, and Error Screens
The community offered several other vital considerations:
Scopes: The Silent Killer
lumine introduced a dimension many overlook: scopes. Your app can be installed, billed, and syncing, but if you've added new required scopes, the existing token might not have them. This often surfaces as a cryptic 403 error on a background job at 3 AM. The solution: store the granted scopes during OAuth, compare them against your required set on every app load, and prompt for re-authorization if there's a mismatch.
Resumable Initial Syncs
For large shops, initial syncs can be lengthy and prone to failure. adamcharvat suggested making syncs resumable. Instead of a 'failed' state, implement a cursor per resource, written after every page. If a worker dies, it simply picks up where it left off, making 'partial' data less of a problem and more of a temporary state of 'behind.'
Lars_Mayer also emphasized keeping data readiness separate from billing and the sync job. A shop can be billed but still be in a partial data state, meaning the app isn't fully operational yet.
Robust Error Handling
Ian_Chechin brought up a brilliant, often-missed point: the error screen itself. If your error page relies on App Bridge, your API, or a valid session – the very things that might have just failed – your merchant sees a blank iframe. His team moved to plain static HTML with zero dependencies for bootstrap/auth failures, ensuring merchants always get a helpful message.
Avoiding Billing Race Conditions
Ian also shared a story about a billing race condition where two loaders on the home screen competed to look up the plan, with the free default occasionally winning over the paid plan. The merchant paid, but saw 'Free.' The fix? Make one component the definitive 'owner' of the plan lookup.
Reinstall & Uninstall Nuances
Reinstalling can be tricky. lumine noted that old tokens simply die, resulting in a 401, so 'reauth required' is a discovered state, not something you need to predict. Lars_Mayer, however, takes a more defensive approach for apps with background jobs, tagging installations with a 'generation' to prevent old jobs from becoming active again on reinstall.
The depth of this conversation truly highlights that building a successful Shopify app goes far beyond just getting the basic install flow working. It requires meticulous attention to state management, a clear understanding of data ownership, and a healthy dose of paranoia about what can go wrong. By adopting these robust patterns and learning from the collective experience of the community, you can build a much more resilient and reliable app for your store owners.
