EasyPay Rwanda — Cloudflare edition. Accept MTN MoMo payments via QR codes. React + Clerk auth + Cloudflare Workers + D1. Not officially endorsed by or affiliated with MTN.
# EasyPay Rwanda (Cloudflare edition)
Accept MTN MoMo payments in Rwanda via QR codes and USSD push, with shop
dashboards, customer logs, and SMS campaign management.
This is a full port of the original Firebase-based app to the Cloudflare
developer platform:
| Concern | Before (Firebase) | Now (Cloudflare) |
| --------- | ---------------------------- | ------------------------------------------------- |
| Auth | Firebase Auth (Google) | **Clerk** (`@clerk/clerk-react` + `@clerk/backend`) |
| Database | Cloud Firestore | **Cloudflare D1** (SQLite) |
| Backend | Firestore security rules | **Cloudflare Worker** API (Hono), token-verified |
| Hosting | Static hosting | **Worker Static Assets** (SPA) via `@cloudflare/vite-plugin` |
## Architecture
- **`src/`** — React 19 + TypeScript + Tailwind CSS 4 SPA (Vite).
Routes: `/` (sign-in / routing), `/admin`, `/shop/:shopId/dashboard`,
`/pay/:shopId` (public payment page).
- **`worker/index.ts`** — Hono API running in the same Worker:
- Public: `GET /api/shops/:id`, `POST /api/shops/:id/transactions`
(records the payment and returns the USSD string), and the click tracker
`GET /t/:campaignId/:phone` (302 redirect + one click per phone).
- Authenticated (Clerk session token in `Authorization: Bearer …`):
`GET /api/me`, `GET /api/shops/:id/dashboard`, `PATCH /api/shops/:id`.
- Admin only: `GET /api/admin/overview`, `POST/DELETE /api/admin/shops…`,
`DELETE /api/admin/customers/:phone`, `GET/POST /api/admin/campaigns`.
- **`migrations/0001_init.sql`** — D1 schema: `shops`, `transactions`,
`customers`, `admins`, `campaigns`, `clicks`.
- Admin access = the signed-in user's primary email is in the `ADMIN_EMAILS`
var (comma-separated) or in the `admins` D1 table. No emails are hardcoded
in the frontend.
Requests to `/api/*` and `/t/*` hit the Worker first
(`assets.run_worker_first`); everything else is served as …