Africa-first multi-tenant SaaS for church governance, financial management, and member administration.
Backend: https://churchos.up.railway.app | Frontend: https://churchos.vercel.app
Version: v1.0.0 | Maintainer: James Koero · Kisumu, Kenya
- Problem Statement
- Research Questions
- Methodology and Design Decisions
- System Architecture
- Tech Stack
- Features
- Data Models
- API Reference
- Local Development
- Deployment
- Environment Variables
- Security
- Roadmap
- Changelog
- Author
African churches are among the most active social institutions on the continent, yet most still run on paper ledgers, WhatsApp groups, and verbal attendance rolls. A mid-sized church in Kisumu with 300 members and weekly tithes above KES 50,000 has no reliable way to reconcile M-Pesa STK push confirmations against its offering records, track attendance over time, or produce a clean monthly financial report.
Generic SaaS tools like Planning Center or Breeze are designed for American church structures. They assume credit cards, flat membership hierarchies, and US dollar pricing. They have no awareness of the Bishop → Overseer → Pastor → Leader → Member governance chain that defines most Pentecostal and evangelical denominations in East and Central Africa. They have no M-Pesa integration.
ChurchOS is a direct answer to that gap.
- What does a church actually need to manage? → Members, Attendance, Finance, Events
- What hierarchy does African Pentecostal governance follow? → 5-tier RBAC
- How should money move through the system? → M-Pesa as first-class citizen
- What happens when the internet is unstable? → PWA with offline service worker
- How should multi-tenancy be enforced? → church_id filter at ORM layer
Why Flask, not Django? Flask gives more control for a single-developer project targeting rapid iteration. The application is small enough to reason about every route.
Why React, not server-rendered templates? Real-time charts, responsive mobile layout, and PWA offline behaviour all require a React SPA. Deployed independently on Vercel so the API and UI can be updated separately.
Why Railway for the backend? Render's free PostgreSQL expires after 90 days — a serious risk for a church depending on years of financial records. Railway persists indefinitely and accepts M-Pesa GlobalPay Visa for billing.
Why JWT with refresh token rotation? Access tokens expire in 8 hours. Refresh tokens rotate on every use — if a token is used twice, both are immediately revoked, preventing stolen token replay without forcing frequent re-login.
Why CHR-XXXXXX for member IDs? Sequential per-church namespace. The prefix is fixed so it does not encode the church name — members who transfer keep their identity without ambiguity.
Client (React 18 PWA) ── HTTPS / JWT ──► Flask API (Railway)
│
┌─────────────────┼──────────────┐
│ │ │
PostgreSQL M-Pesa Daraja Flutterwave
(Railway) STK Push API Webhooks
Seven Blueprint modules: auth · members · attendance · finance · events · users · dashboard. Every SQLAlchemy query filtered by church_id at the ORM layer.
| Layer | Technology |
|---|---|
| Backend | Python 3.11, Flask 3.0, SQLAlchemy, Flask-JWT-Extended |
| Database | PostgreSQL (Railway prod) / SQLite (dev) |
| Frontend | React 18, React Router 6, Recharts, Axios |
| Auth | JWT access + refresh tokens, RBAC (5 roles) |
| Payments | M-Pesa Daraja STK Push, Flutterwave |
| Deploy | Railway (backend + DB) + Vercel (frontend) |
| PWA | Service Worker, Web App Manifest |
| CI/CD | GitHub Actions |
- Multi-church tenancy — 30-day free trial per church, full data isolation
- Member registry — CHR-XXXXXX auto-IDs, full CRUD, search, pagination
- Attendance tracking — by service type and date, trend charts
- Financial ledger — KES transactions, M-Pesa references, audit log
- M-Pesa STK Push — trigger payment request to member's phone
- Events management — services, conferences, outreaches
- Real-time dashboard — KPI cards, income vs expenses, Recharts
- RBAC (5 roles) — admin / pastor / secretary / treasurer / viewer
- Audit log — every financial action timestamped and attributed
- PWA — installable on Android, offline-capable
All models carry church_id FK. Every query filtered on this field.
| Model | Key columns |
|---|---|
| Church | id, name, subscription_plan, trial_ends_at |
| User | id, church_id, username, password_hash, role |
| Member | id, church_id, member_id (CHR-XXXXXX), full_name, phone |
| Attendance | id, church_id, member_id, service_type, service_date |
| Finance | id, church_id, type, category, amount_kes, mpesa_ref |
| Event | id, church_id, title, event_type, start_datetime |
Base URL: https://churchos.up.railway.app
All endpoints require Authorization: Bearer <access_token> except /api/auth/login.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login | Obtain access + refresh tokens |
| POST | /api/auth/refresh | Rotate refresh token |
| POST | /api/auth/logout | Revoke token |
| GET/POST | /api/members | List or create members |
| GET/PUT/DELETE | /api/members/:id | Read, update, delete |
| GET/POST | /api/attendance | List or record attendance |
| GET/POST | /api/finance | Ledger or record transaction |
| POST | /api/finance/mpesa/stk | Trigger M-Pesa STK Push |
| GET/POST | /api/events | List or create events |
| GET | /api/dashboard | Aggregate KPIs + chart data |
# Backend
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
flask run --port 5000
# Frontend
cd frontend
npm install
echo "REACT_APP_API_URL=http://localhost:5000" > .env.local
npm startDefault credentials (dev only): admin / Admin@2026
Backend → Railway
Connect repo → set env vars → Railway reads railway.toml → auto-deploys on push to main.
Accepts M-Pesa GlobalPay Visa for billing.
Frontend → Vercel
Import repo → root directory: frontend → set REACT_APP_API_URL → auto-deploys.
| Variable | Required | Description |
|---|---|---|
| SECRET_KEY | Yes | Flask session signing |
| JWT_SECRET_KEY | Yes | Must differ from SECRET_KEY |
| DATABASE_URL | Yes | Railway PostgreSQL URL |
| FRONTEND_URL | Yes | Vercel URL for CORS |
| MPESA_CONSUMER_KEY | M-Pesa | Daraja API credentials |
| MPESA_CONSUMER_SECRET | M-Pesa | Daraja API credentials |
| FLW_SECRET_KEY | Flutterwave | Dashboard secret key |
| FLW_SECRET_HASH | Flutterwave | Webhook verification hash |
- Login rate-limited: 5 requests/min per IP
- JWT blocklist — logout actually revokes tokens
- Refresh token rotation — stolen tokens expire after one use
- CORS locked to FRONTEND_URL only
- Multi-tenant isolation — church_id on every ORM query
- Webhook HMAC signature verification (M-Pesa + Flutterwave)
- Passwords: Werkzeug PBKDF2-SHA256 with random salt
- Multi-church tenancy, 30-day trial
- Member registry (CHR-XXXXXX)
- Attendance, Finance, Events modules
- JWT RBAC (5 roles), M-Pesa STK Push, Flutterwave
- PWA, Railway + Vercel deployment
- SMS via Africa's Talking API
- PDF financial reports (monthly, annual)
- Email password reset
- Branch-level sub-accounts
- Cross-branch reporting for Overseers and Bishops
- React Native mobile app
- Rebranded from CMDMS (Carwash Main Altar) to ChurchOS
- Migrated from Render to Railway + Vercel
- Member IDs updated from MRH-XXXXXX to CHR-XXXXXX
- Added Flutterwave alongside M-Pesa
- JWT refresh token rotation, rate limiting, webhook verification
- Fixed CI/CD workflow for Railway + Vercel
James Koero · Junior ML Engineer · Kisumu, Kenya
LinkedIn · Nyando Flood AI · AfriSalaries · Loan Risk
Built by James Koero · Kisumu, Kenya · 2026 · MIT License