E-commerce13 min read

Headless commerce Medusa + Wave + MTN MoMo: pan-African store in 14 days

Mohamed Bah·Fondateur, Kolonell
May 7, 2026
Share:
Headless commerce Medusa + Wave + MTN MoMo: pan-African store in 14 days

Headless commerce Medusa + Wave + MTN MoMo: pan-African store in 14 days

E-commerce

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

ComponentServiceCost/mo
Medusa backendHetzner Cloud CX21 (4 vCPU, 8 GB)€8
PostgresNeon Pro (5 GB)€19
RedisUpstash Free → Pay€0-5
Storefront Next.jsVercel Hobby or Cloudflare Pages€0-20
Image CDNCloudflare R2€5-15
Domain + DNSCloudflare€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 prices table 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.

Tags:#Medusa#Headless Commerce#Wave#MTN MoMo#Pan-Africa#Open-Source
Share:

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.