Tellgo
An eSIM store on Medusa, migrated off a single supplier while live — the supplier we moved to now carries three quarters of a 7,700-plan catalogue
YEAR
2023
CLIENT
TellGo (Tell Go Company)
SCOPE
PLATFORM
Web, iOS, Android
The Challenge
Medusa is a commerce engine built around physical stock. You hold inventory, you pick it, you ship it. An eSIM has none of that. It does not exist until somebody pays for it, and at that moment it has to be bought from a supplier and provisioned before the customer can use it. There is no warehouse and no shipment, and there is no second chance — a plan bought against a payment that later fails is money gone, not a box that comes back. The catalogue is the second problem. An eSIM store is not fifty products, it is every country crossed with every operator crossed with every data-and-duration combination, and it moves whenever the supplier's does. Nobody was going to type that in. TellGo needed both kinds of thing in one cart anyway. Someone buying a data plan for Japan and a power bank to go with it checks out once, pays once, gets one receipt — even though one is provisioned from a supplier in the moment and the other comes off a shelf — settling with a mix of card, wallet, loyalty points spent as currency, gift voucher and discount, all against the same order, all itemised. Then the hard one arrived, the one nobody designs for on day one. The first version was built against a single supplier, the way every first version is: the supplier's name went into the domain model. The table was called airalo. The join column was airalo_order_id. That is fine right up until the business needs a second supplier — for a country the first covers badly, or a price it cannot match — and finds the vendor's name is load-bearing across the schema of a live store with real orders in it.
Objectives
- Sell a digital good that does not exist until payment clears, alongside the physical accessories that go with it, from one catalogue and one cart.
- Keep a catalogue of thousands of country-and-operator combinations current without anyone maintaining it by hand.
- Never sell a plan the business cannot actually fulfil, and never strand a customer when a supplier is slow.
- Let one order settle from five sources of value at once, and account for each on the receipt.
- Add a second supplier without a rewrite, without downtime, and without breaking an order already in the database.
- Run the catalogue in Arabic and English down to individual product copy.
- Ship web, iOS and Android against one backend, on pipelines the team controls.
- Keep the app upgradable in a market where a stale build talks to a moved API — without ever locking a customer out of it.
Our Approach
Extend Medusa, never fork it
Everything went in through Medusa's own extension points — services, subscribers, routes, scheduled jobs, models and migrations. The engine underneath stays a dependency we can upgrade, not a codebase we now own.
Generate the catalogue, don't curate it
An hourly job reads the supplier's countries, operators and packages and upserts them as collections, products and variants, each stamped with the provider that owns it. The merchandising team's job becomes pricing and presentation, not data entry, and a supplier adding a country shows up in the store within the hour.
Rename the vendor out of the domain, in migrations, with a way back
Adding a second supplier started as a schema problem, not an integration problem. One migration renames airalo to esim_data, renames airalo_order_id to provider_order_id, and adds a provider enum defaulting to airalo — so every row already in the table answers the new question correctly the moment the column exists. A second backfills the same default across the catalogue. Both have a working down(). None of it was clever; the point is that it was reversible.
One interface, two shapes of supplier
Ordering and authentication are separate interfaces, because suppliers differ more in how they let you in than in what they sell. Airalo speaks OAuth2 client-credentials; eSIM Card wants a reseller email and password. Both tokens cache in Redis for a day, and both providers recognise their own expiry from a failed response rather than trusting a clock — Airalo, in practice, by treating a 525 as expired as well as a 401. The rest of the system asks for a provider and gets one.
Assume the supplier will be slow, and design the slow path first
Provisioning is attempted inline, but a supplier that does not answer immediately must not become a failed order. The order records a pending marker instead, and a job every five minutes resolves it against the right supplier. The customer sees a plan that arrives shortly rather than a checkout that broke.
Solve the supplier problem twice — once on the server, once on the phone
The two suppliers disagree about how an activation code arrives: one returns a QR image, the other returns the raw LPA string. Rather than normalise that server-side and lose fidelity, the app renders the code itself at 512 pixels when it gets a string and displays the image when it gets one. The adapter pattern that keeps the backend supplier-agnostic shows up again on the client, for the same reason.
Give a stockless good a stock check
The reason to keep inventory is so you never sell what you cannot deliver. An eSIM has no inventory, but the reseller account behind it has a balance, so that is where the check goes: before checkout completes, the supplier's balance is compared against net price times quantity. If it is short the customer gets a plain "not available right now" — not a successful order that quietly never arrives.
The Solution
Two suppliers, one catalogue
Airalo and eSIM Card sit behind a common interface. Their APIs disagree about almost everything — response shapes, auth, how a package is described — and none of that reaches the rest of the system, which sees one normalised view of a plan, an order and a usage reading. Browsing is by country or continent, mapped to catalogue handles, with a global package for people crossing several.
A self-maintaining product catalogue
The hourly importer keeps thousands of plans in step with the suppliers, creating and updating collections, products and variants. Because plans are ordinary Medusa products, they merchandise, discount, translate and report exactly like the headphones sold beside them.
Provisioning, with a reconciler behind it
On payment the plan is bought and the customer gets the QR and install instructions in their account, alongside what is left on the plan, queried live against whichever supplier issued it. Anything the supplier has not returned yet is picked up by the five-minute job and completed without anyone being asked to try again.
Wallet and Yellow Money
A stored-value wallet and a separate loyalty currency, both spendable at checkout, both derived from a transaction ledger rather than a mutable balance field. Medusa has neither concept, so both are services with their own models, admin screens, reports and line on the receipt.
Five-tender checkout
A single order can draw on card, wallet, loyalty points, gift voucher and discount at once, each carried separately through to the receipt. That separation is what makes reconciliation possible later.
Payments and paperwork
MyFatoorah in Kuwaiti dinar on both web and app, because that is what customers in the GCC expect at checkout. Every order renders a PDF invoice through headless Chrome, stores it on S3 and mails it via SES with every tender itemised.
A bilingual catalogue
Titles, subtitles and descriptions carry a language and are unique per product and language, with the storefront routed under a language segment. The Arabic store is a real store, not a machine-translated shadow of the English one.
The apps
One Flutter codebase for iOS and Android, now at version 3, structured as a service locator over repositories with a service and controller per domain. Firebase auth with Google, Facebook and Apple sign-in; push through Firebase Messaging; profile photos picked, cropped and compressed on the device rather than shipped full-size to the server.
The activation screen renders the eSIM QR locally, so a traveller who has landed without data can still install the plan they bought before they took off.
Version enforcement is the part that took real work, because the two platforms do not offer the same thing. Android uses Play's In-App Update API and knows how to resume an update the user abandoned halfway. iOS has no equivalent, so the app asks the App Store what version is live and prompts from there — and deliberately stays quiet when the store reports a build that is still in review, so nobody is told to update to something they cannot download. Every failure path in that check falls through to the login screen. A version check is never a reason to lock someone out of an app that holds their money.
Admin and the pipeline
An extended Medusa admin covering eSIM products and orders, wallets, offers, banners, promotional email and reporting, with widgets putting the ICCID and payment reference on the order screen where support actually needs them. Backend deploys are a button the team presses when it is ready, not something that fires on merge; the iOS build runs in GitHub Actions.
Under the Hood
TellGo is a Medusa commerce backend in TypeScript over PostgreSQL, with Redis carrying the cache, the event bus and both suppliers' access tokens. Around twenty custom services extend the engine — the important one being a supplier abstraction with two implementations behind it — plus scheduled jobs that keep the catalogue current and finish what the suppliers leave pending. A Next.js storefront and a Flutter app consume the same store API. It runs on AWS container infrastructure from images built in CI.
Commerce core
Medusa — extended rather than forked. PostgreSQL and TypeORM — the store's data, custom models for eSIM data, wallets, transactions and translations, and the migrations that reshaped them in place. Redis — cache, event bus, and supplier token storage on a 24-hour TTL.
Supply and payments
Airalo and eSIM Card — two suppliers behind one interface, selected per product, each with a pre-sale balance check. MyFatoorah — GCC payments in KWD, web and mobile. AWS S3 and SES — invoice storage and transactional mail.
Clients
Next.js — the storefront, routed under a language segment. Flutter — one codebase for both stores at version 3, with GetIt for dependencies, repositories per domain, the MyFatoorah SDK, Firebase auth and messaging, and platform-specific update enforcement. Medusa Admin — extended with the screens the business runs on. Product copy is translated once on the server and consumed by every client, so the app carries interface strings only.
Infrastructure and delivery
AWS ECS — the backend runs as a container service, with its task definition committed to the repo so infrastructure changes arrive by pull request and get reviewed like code. ECR and Docker — images built in GitHub Actions and tagged by commit. GitHub Actions — the branch decides the environment, release to production and staging to staging, triggered by hand and blocking until the service reports stable.
The Decisions
Four calls worth stating, because they shaped everything else.
The eSIM is bought after the money, not before
Provisioning hangs off payment rather than order placement, so a checkout that never pays never buys a real plan. The cost is a slightly later delivery. The alternative is paying a supplier for orders that never complete.
The supplier's name came out of the schema before the second supplier went in
The tempting version is an if statement and a second column. We did the migration instead: rename the table, rename the join, add an enum defaulting to what is already true, backfill the catalogue, keep a down(). More work up front, and the reason a third supplier is a class rather than a refactor.
The slow path is a scheduled job, not a retry loop
A pending SIM is a row a cron can find, not a request holding a connection open. It means the checkout returns quickly, a supplier outage resolves itself when the supplier comes back, and the state of every unfinished order is a query rather than a guess.
An update prompt must never become a locked door
Checking whether an app is current means calling something that can fail — a store API, a network, a plugin. Every one of those paths ends at the login screen rather than a dead splash. The customer with money in a wallet gets into the app even on the day the update check is broken.
Deploys are a button
The pipeline builds and ships on demand rather than on merge, so a merge to release is a decision to be ready and pressing deploy is a decision to go. For a store taking real payments in a single market, that separation is worth the extra click.
Outcomes
TellGo runs today at tellgo.net and in the App Store and Play Store: an eSIM catalogue and an electronics store in one place, in two languages, with a wallet and a loyalty currency customers spend against real orders — served by two suppliers instead of one, on a catalogue that maintains itself hourly, a schema that no longer knows either supplier's name, and an app on its third major version.
We have not put numbers on this page. We were not given traffic or revenue figures we could stand behind, and a metric we cannot source is worth less than the description above.
Other Case Studies

Speed Machine
A direct-to-customer insurance platform that integrates leading insurer APIs so people can...

Advantage
A multi-tenant insurance benefits portal PrishaPolicy offers to its corporate clients,...

Vantage
An internal insurance operations platform built for PrishaPolicy, unifying sales, policy...

Prisha Policy
Empowering a prominent insurance broker to safeguard client assets and liabilities through...

Centuro Global
An Angular platform and its AWS infrastructure for a compliance SaaS operating in 170+...

Akoya
An AI itinerary planner built on text-davinci-003 in early 2023 — templated prompts,...

BillMart
Providing comprehensive trade finance solutions through Working Capital Loans, Channel...

H&S Store
An online store for a Kuwaiti electronics retailer, kept in step with the ERP, the...

Empathable
EmpathablePX — an AI patient-simulation platform where clinicians rehearse high-stakes...



