Hi all,
I wanted to share one thing I underestimated while building a Shopify app.
At the beginning I thought the install flow would be quite simple:
install → OAuth → billing approval → initial sync → ready
At least on paper it looked like this ![]()
My setup is not very exotic: embedded UI, API, worker and a database.
Because the app stores Shopify data locally, I also need an initial sync after installation, and there are background jobs running in the worker.
This is the point where it became more complex than I expected.
Very quickly I noticed that I do not really have one app state.
I actually have several state dimensions at the same time, for example installation, billing, data readiness, sync and later also uninstall/reinstall and recovery.
A shop can be installed, but billing is not active yet.
Or billing is active, but the initial sync is still running.
Or the app is installed and billing is fine, but the local data is only partial because a sync failed.
Then there are billing states like cancelled at period end, expired, frozen or declined.
And uninstall made the whole thing even more interesting.
The app can already be uninstalled while local data is still waiting for cleanup or deletion.
And then I started thinking about reinstall.
What happens if the merchant installs again while something from the previous installation is still being cleaned up?
What happens to background work which belongs to the old installation?
Can I still trust the existing local data, do I need a new sync, or do I first need a new authorization?
At some point I stopped thinking about one single installed = yes/no state and started creating a state matrix instead.
Very simplified, I now have separate areas like:
-
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, etc.
The interesting part is not really the individual states.
It is their combination.
For example:
installed + billing active + data partial + initial sync running
is a completely different situation than:
installed + billing active + data active + sync successful
even though both shops are technically “installed”.
Because UI, API and worker run separately, I persist the relevant state in the database and let every part decide from this state what it is currently allowed to do.
Another lesson for me was not to build the logic around the assumption that every webhook or event will always arrive in exactly the order I would prefer.
So the diagram below is intentionally not a complete state machine. It is more a picture of the different state dimensions which can exist at the same time.
What surprised me most is how much lifecycle logic came out of something that initially sounded like a pretty small feature.
I would be really interested how this developed in other apps.
Did you also end up with much more lifecycle state than you expected at the beginning?
What approach worked well for you to keep all these moving parts manageable?
And is there any edge case you only discovered later and wish you had thought about earlier? ![]()
Would be interesting to hear your experiences.
