Shopify is the easy option, but 4% commission on 50M XOF/month = 2M XOF/month wasted. For e-commerce scaling past 30M XOF monthly — typically marketplaces in Lagos, Abidjan, Nairobi, Douala — Medusa becomes the right answer. Here's how to ship a pan-African stack.
TL;DR
- Medusa = open-source Node.js + Postgres backend, Next.js frontend of your choice.
- Hosting cost: €35-80/month (Hetzner Cloud + Neon Postgres) vs €80/month Shopify Basic + 4% fees.
- Payment coverage: Wave (SN, CI), MTN MoMo (CI, GH, NG, CM, UG, RW, ZM), Airtel Money (NG, KE, TZ, MW), Orange Money (SN, CI, CM, ML, BF, GN, MG), M-Pesa (KE, TZ).
Why pan-African Medusa in 2026
Africa commerce is fragmented by currency and gateway:
- UEMOA (XOF): Wave + Orange Money + PayDunya
- CEMAC (XAF): Orange Money + MTN MoMo + Express Union
- Nigeria (NGN): Paystack + Flutterwave + Squad
- Ghana (GHS): MTN MoMo + Vodafone Cash + Hubtel
- Kenya/Tanzania (KES/TZS): M-Pesa + Tigo Pesa + Stripe
- Rwanda/Uganda (RWF/UGX): MTN MoMo + Airtel Money
Shopify natively supports none of these (except Stripe/Paystack via partners). Medusa lets you code one PaymentProvider per gateway and cover 19 African countries.
Target architecture
`
[Next.js storefront]
↓
[Medusa.js backend (Node)]
↓
[Postgres (Neon)] + [Redis (Upstash)]
↓
[Custom Payment Providers]
├── WavePaymentProvider (UEMOA)
├── MtnMomoPaymentProvider (Anglophone + CEMAC)
├── MPesaPaymentProvider (East Africa)
├── PaystackPaymentProvider (NG, GH)
└── StripePaymentProvider (international)
↓
[Algolia (search) + Cloudinary (images)]
`
Step 1 — bootstrap Medusa
`bash
npx create-medusa-app@latest --skip-db
cd kolonell-store
yarn install
`
Configure medusa-config.js:
`js
module.exports = defineConfig({
projectConfig: {
redis_url: process.env.REDIS_URL,
database_url: process.env.DATABASE_URL,
database_type: 'postgres',
},
modules: [
{ resolve: '@medusajs/cache-redis', options: { redisUrl: process.env.REDIS_URL } },
{ resolve: '@medusajs/event-bus-redis', options: { redisUrl: process.env.REDIS_URL } },
{ resolve: './src/modules/wave-payment' },
{ resolve: './src/modules/mtn-momo-payment' },
{ resolve: './src/modules/mpesa-payment' },
],
});
`
Step 2 — Wave Payment Provider (custom)
`ts
import { AbstractPaymentProvider } from '@medusajs/framework/utils';
class WavePaymentProvider extends AbstractPaymentProvider {
static identifier = 'wave';
async initiatePayment({ amount, currency_code, data }) {
const res = await fetch('https://api.wave.com/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.WAVE_API_KEY},
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: String(amount),
currency: currency_code.toUpperCase(),
success_url: data.success_url,
error_url: data.error_url,
client_reference: data.cart_id,
}),
});
const session = await res.json();
return { data: { wave_id: session.id, wave_launch_url: session.wave_launch_url } };
}
async authorizePayment({ data }) {
const res = await fetch(
https://api.wave.com/v1/checkout/sessions/${data.wave_id},
{ headers: { 'Authorization': Bearer ${process.env.WAVE_API_KEY} } }
);
const session = await res.json();
return {
status: session.payment_status === 'succeeded' ? 'authorized' : 'pending',
data: session,
};
}
async capturePayment(input) { return { data: input.data }; }
async cancelPayment(input) { return { data: input.data }; }
async refundPayment({ data, amount }) {
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
await fetch('https://api.wave.com/v1/refunds', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.WAVE_API_KEY},
'Idempotency-Key': refund_${data.wave_id}_${amount},
},
body: JSON.stringify({ payment_id: data.wave_id, amount: String(amount) }),
});
return { data };
}
}
export default WavePaymentProvider;
`
Step 3 — MTN MoMo Provider (Anglophone Africa)
MTN MoMo covers Ghana, Ivory Coast, Cameroon, Uganda, Rwanda, Zambia, Benin:
`ts
class MtnMomoProvider extends AbstractPaymentProvider {
static identifier = 'mtn-momo';
async initiatePayment({ amount, currency_code, data }) {
const apiUserId = process.env.MTN_API_USER_ID;
const apiKey = process.env.MTN_API_KEY;
const subKey = process.env.MTN_SUB_KEY;
const tokenRes = await fetch('https://sandbox.momodeveloper.mtn.com/collection/token/', {
method: 'POST',
headers: {
'Authorization': Basic ${Buffer.from(${apiUserId}:${apiKey}).toString('base64')},
'Ocp-Apim-Subscription-Key': subKey,
},
});
const { access_token } = await tokenRes.json();
const referenceId = crypto.randomUUID();
await fetch('https://sandbox.momodeveloper.mtn.com/collection/v1_0/requesttopay', {
method: 'POST',
headers: {
'Authorization': Bearer ${access_token},
'X-Reference-Id': referenceId,
'X-Target-Environment': 'sandbox',
'Ocp-Apim-Subscription-Key': subKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: String(amount),
currency: currency_code.toUpperCase(),
externalId: data.cart_id,
payer: { partyIdType: 'MSISDN', partyId: data.payer_msisdn },
payerMessage: 'Order ' + data.cart_id,
payeeNote: 'Kolonell store',
}),
});
return { data: { mtn_reference: referenceId } };
}
}
`
Step 4 — Next.js storefront
Medusa ships a Next.js starter. Adapt Checkout to surface the right provider per country:
`tsx
const providers = useMemo(() => {
switch (cart.region.country_code) {
case 'sn':
case 'ci': return ['wave', 'orange-money', 'paydunya'];
case 'gh': return ['mtn-momo', 'vodafone-cash', 'paystack'];
case 'ng': return ['paystack', 'flutterwave'];
case 'ke':
case 'tz': return ['mpesa', 'stripe'];
case 'cm': return ['orange-money', 'mtn-momo', 'express-union'];
default: return ['stripe'];
}
}, [cart.region.country_code]);
`
Step 5 — deployment & monthly cost
| Component | Service | Cost/mo |
|---|---|---|
| Medusa backend | Hetzner Cloud CX21 (4 vCPU, 8 GB) | €8 |
| Postgres | Neon Pro (5 GB) | €19 |
| Redis | Upstash Free → Pay | €0-5 |
| Storefront Next.js | Vercel Hobby or Cloudflare Pages | €0-20 |
| Image CDN | Cloudflare R2 | €5-15 |
| Domain + DNS | Cloudflare | €1 |
| Total | €33-68/month |
Compare to Shopify Basic (€29/mo) + 2% transaction fee on non-Shopify Payments at 50M XOF volume = ~€840/mo extra.
Known pitfalls
- Medusa v2 vs v1: v2 (late 2024) refactored modules. v1 tutorials are stale — always verify version.
- Postgres migrations: Medusa generates schemas, but in multi-currency pan-Africa, watch
pricestable scaling. - Image performance: Cloudinary > Cloudflare Images if you want on-the-fly transforms.
- MTN MoMo sandbox: very flaky, plan local fixture fallback for CI.
FAQ
Q: Does Medusa work on Vercel?
A: Next.js storefront yes (Edge runtime). Medusa backend needs long-running Node → prefer Hetzner, Railway, Render, Fly.io.
Q: Multi-currency XOF + NGN + KES in same store?
A: Yes via Medusa Regions. Each region = currency + countries + providers.
Q: How many hours for the full stack?
A: ~14 dev-days (1 senior). 2 days setup + auth, 5 days payment providers, 4 days storefront + checkout, 3 days admin & tests.
Conclusion
Medusa becomes in 2026 the pragmatic choice for pan-African e-commerce wanting control, 8× lower fees, and 5+ local gateways. Higher technical entry cost than Shopify, but unbeatable 6-month ROI past 30M XOF/month volume.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.

