📚 Documentation: API README · Mobile README · Web README · OpenAPI Docs (local)
| I'm a... | Start here |
|---|---|
| Backend Engineer | API README — Full setup, architecture, testing |
| Mobile Developer | Mobile README — Expo setup, navigation, offline sync |
| Frontend/Web Dev | Web README — HTML/CSS/JS setup, API integration |
| DevOps/Deployment | API README § Deployment — Railway, Docker, CI/CD |
| Product Manager | Budget Blueprint — Features, roadmap, design |
| Compliance/Legal | Docs README — Privacy, terms, regulations |
- The Problem
- What This Is
- Backend — Production API
- Mobile App — In Progress
- Web App — MVP
- Project Status
- Running Locally
- About
We work hard. We earn. And somehow — month-end comes and we're asking "where did everything go?"
It's not a discipline problem. It's a visibility problem.
Most budgeting apps weren't built with our reality in mind. They assume credit cards, direct bank APIs (like Plaid), and constant high-speed connection. But in Ghana, money moves differently:
- Mobile Money First: The majority of financial transactions flow through MTN MoMo, Telecel Cash, and AT Money—not traditional bank accounts.
- Unstructured Logs: Transactions are notified via SMS alerts. Without automation, tracking spending requires tedious, manual entry that people quickly abandon.
- Data & Connectivity Constraints: Network signals drop frequently. A financial tracker must survive offline and consume minimal data.
CediSmart is built from first principles for this reality — not adapted from a Western template.
💡 Design principle: Optimize for local financial rails (MoMo, bank SMS alerts) and constrained connectivity before optimizing for feature breadth.
CediSmart is a monorepo containing a production-grade REST API (complete) and a React Native mobile app (complete). It is not a tutorial project. It tracks personal financial budgets and is engineered with strict banking-grade accuracy and security standards.
CediSmart/
├── cedismart-api/ # FastAPI backend — Python 3.12+
├── cedismart-mobile/ # React Native (Expo) — Mobile App
└── cedismart-web/ # Landing Page Website & APK Download Portal
CediSmart integrates Google's Gemini 2.5 Flash model (via Google AI Studio) to deliver advanced local features:
- Gemini SMS Parser: In Africa (especially Ghana), transaction syncing via open banking APIs is unavailable. CediSmart solves this by automatically parsing Mobile Money (MTN MoMo, Telecel Cash, AirtelTigo Money) and bank transaction SMS alerts into structured ledger records. Enforced via strict API JSON schemas.
- Ghanaian Pidgin AI Assistant: Provides localized, warm, and highly insightful financial coaching in Ghanaian Pidgin (with terms like chale, wahala, dey active), helping users understand budgets, spending, and savings without corporate jargon.
The backend is fully implemented and tested. 31 endpoints across 7 modules, 85%+ test coverage, CI/CD on GitHub Actions.
✅ Current state: Backend feature-complete for core budgeting, accounts, auth, reporting, and guardrails.
| Layer | Technology | Why |
|---|---|---|
| Framework | FastAPI (async) | Native async, auto-generated OpenAPI, Pydantic v2 validation |
| ORM | SQLAlchemy 2.0 async | Type-safe queries, async-native, no raw SQL |
| Database | PostgreSQL 16 | NUMERIC(14,2) for money — Float is never acceptable in fintech |
| Cache | Redis 7 | OTP storage (5-min TTL), JWT revocation, report caching |
| Auth | RS256 JWT + bcrypt | Asymmetric signing, per-token revocation via jti claims |
| Auth Verification | Clerk SDK / API | Phone verification & SMS OTP offloaded securely to Clerk |
| Rate Limiting | slowapi | Auth endpoints protected at 3–5 req/15min per IP |
| Migrations | Alembic | Schema history is immutable — no direct DB edits, ever |
| Config | pydantic-settings | Fully typed environment variables — no os.getenv() scattered in code |
CediSmart uses a Modular Monolith — a single deployable unit with enforced module boundaries. This is a deliberate choice over microservices: it avoids distributed system complexity at MVP scale while preserving clean boundaries for future extraction.
┌────────────────────────────────────────────────────────────────────────┐
│ FastAPI Application │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Accounts │ │ Trans- │ │ Budgets │ │
│ │ Module │ │ Module │ │ actions │ │ Module │ Reports Module │
│ │ │ │ │ │ Module │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────▼────────────▼────────────▼─────────────▼────────────────────┐ │
│ │ Core Layer │ │
│ │ config · database · redis · security │ │
│ │ dependencies · exceptions · sms │ │
│ └─────────────────────────┬───────────────────────────────────────┘ │
└────────────────────────────┼───────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
PostgreSQL Redis Clerk
(Railway) (Railway) (Auth API)
All endpoints versioned under /api/v1/. Every response uses a consistent error envelope: {"error": {"code": "...", "message": "...", "field": null}}.
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/register/clerk |
Create user profile after Clerk verification |
| POST | /auth/login |
Phone + PIN authentication |
| POST | /auth/token/refresh |
Rotate access token via refresh token |
| POST | /auth/logout |
Revoke refresh token from Redis |
| POST | /auth/pin/reset/confirm |
Reset account PIN using verified Clerk session |
| GET | /users/me |
Fetch authenticated user profile |
| PATCH | /users/me |
Update name, email, currency |
| DELETE | /users/me |
GDPR-style anonymisation + token revocation |
| GET | /accounts/ |
List accounts with computed real-time balances |
| POST | /accounts/ |
Create bank / MoMo / cash account |
| GET | /accounts/{id} |
Account detail + balance |
| PATCH | /accounts/{id} |
Update name or provider |
| DELETE | /accounts/{id} |
Hard delete (no txns) or soft deactivate (has txns) |
| GET | /categories/ |
List system + user categories |
| POST | /categories/ |
Create custom category |
| PATCH | /categories/{id} |
Update custom category |
| DELETE | /categories/{id} |
Delete if no active transactions |
| GET | /transactions/ |
Paginated, filtered transaction list |
| POST | /transactions/ |
Create income / expense / transfer |
| POST | /transactions/bulk |
Idempotent bulk create for offline sync |
| GET | /transactions/summary |
Current month vs last month income/expense |
| GET | /transactions/{id} |
Transaction detail |
| PATCH | /transactions/{id} |
Update transaction |
| DELETE | /transactions/{id} |
Soft delete (is_deleted=True) |
| GET | /budgets/ |
Monthly budgets with computed spend progress |
| GET | /budgets/current |
Current month dashboard budgets |
| POST | /budgets/ |
Upsert monthly budget (create or update) |
| DELETE | /budgets/{id} |
Delete budget |
| GET | /reports/monthly |
Monthly income / expense / top category |
| GET | /reports/categories |
Category breakdown with percentages |
| GET | /reports/trends |
Month-over-month income/expense trend (1–12 months) |
These are the decisions that separate a learning project from production fintech code.
-
Money is never a Float. All monetary values are
NUMERIC(14, 2)in PostgreSQL andDecimalin Python throughout the entire stack.Floatarithmetic introduces rounding errors that silently corrupt financial records. -
Account balances are never stored. Balance =
opening_balance + SUM(income) − SUM(expense), computed at query time via a single SQL aggregation. Storing a cached balance introduces drift bugs when writes partially fail. -
OTP timing attacks are mitigated. OTP comparison uses
hmac.compare_digest()instead of==to avoid early-return timing leaks. -
Phone enumeration is prevented. PIN reset initiation returns an identical response whether phone exists or not.
-
Refresh tokens are individually revocable. Each refresh token carries a
jti; Redis stores active JTIs with TTL for per-device logout. -
Race conditions on free-tier limits are prevented. Limit checks use
SELECT ... FOR UPDATEto serialize concurrent writes. -
Transactions are never hard-deleted. Deletion is logical (
is_deleted = True) to preserve audit trail integrity. -
JWT uses RS256, not HS256. Asymmetric signing separates signing authority (private key) from verification (public key).
| Vector | Mitigation |
|---|---|
| PIN brute-force | bcrypt cost factor 12 + 5 attempts / 15 min rate limit |
| OTP brute-force | 6-digit + 3 sends / 15 min + hmac.compare_digest |
| Token replay | jti tracked in Redis — logout is immediate |
| Resource enumeration | Ownership errors return 404, never 403 |
| Injection | SQLAlchemy ORM only — no raw SQL strings |
| Sensitive data in logs | Phone, PIN, OTP, tokens excluded from logs |
| CORS | Explicit origin allowlist — * never used |
| Transport | HSTS enforced via middleware + Cloudflare SSL |
69 tests · 85.46% coverage · 0 failures
| Module | Coverage |
|---|---|
core/security.py |
100% |
core/sms.py |
100% |
modules/auth/router.py |
100% |
modules/transactions/router.py |
100% |
modules/reports/router.py |
100% |
| Overall | 85.46% |
Tests run against SQLite locally for speed and PostgreSQL 16 in CI. The CI pipeline enforces:
ruff— lintingblack --check— formattingmypy --strict— type checking (zeroAnytolerance)bandit -ll— static security scanpytest --cov-fail-under=80— coverage gate
Three GitHub Actions workflows:
| Workflow | Trigger | What it does |
|---|---|---|
backend-ci.yml |
Push / PR to main |
Lint → type-check → bandit → pytest (Postgres + Redis services) |
backend-deploy.yml |
Push to main (CI passes) |
Railway deploy → health check → Slack alert on failure |
mobile-ci.yml |
Push to mobile/** |
TypeScript check → ESLint (zero warnings) |
React Native (Expo SDK 54, Managed Workflow) with TypeScript strict mode.
📱 See Mobile README for full setup instructions.
| Layer | Technology |
|---|---|
| Navigation | React Navigation v6 |
| Server state | TanStack Query v5 |
| Global state | Zustand |
| HTTP client | Axios with RS256 JWT interceptors |
| Secure storage | Expo SecureStore (tokens, PIN — never AsyncStorage) |
| Offline queue | MMKV |
| Styling | NativeWind (Tailwind for React Native) |
| Forms | React Hook Form + Zod |
| Error monitoring | Sentry |
Implemented screens: Registration → OTP → Set PIN → Login → Dashboard → Transactions → Add Transaction → Budgets → Reports → Accounts → Settings.
To facilitate store submissions (App Store / Play Store) and external beta testing without sending live SMS messages, configure Clerk's native Test Phone Numbers:
- Log in to the Clerk Dashboard and navigate to the phone verification settings.
- Register your testing/reviewer phone numbers (e.g.
+1 555 000 0000). - Associate a fixed code (e.g.
123456) that reviewers can use to verify instantly.
Responsive product marketing landing page and direct Android APK download website for CediSmart. Built using Vanilla HTML/CSS/JS, optimized for conversions, store compliance reviews, and lightweight preview animations.
🌐 See Web README for full deployment instructions.
| Component | Purpose |
|---|---|
| Showcase UI | Interactive sections demonstrating app features with dark forest themes |
| Mockup Simulator | Client-side dashboard mock demonstrating secure GHS balance masking |
| APK Distribution | Direct download buttons linking to production APK installation binaries |
| Legal Pages | Publicly hosted Privacy Policy and Terms of Service documents |
| Phase | Description | Status |
|---|---|---|
| 1 | Planning & Architecture | Complete |
| 2 | Backend Core API | Complete |
| 3 | Mobile Application (Expo) | Complete |
| 4 | Monorepo Integration | Complete |
| 5 | Testing, Security & Hardening | Complete (100% type-safe, 80 pytest tests passing) |
| 6 | Landing Portal & Compliance | Complete |
📌 Project State: Feature-complete, fully integrated, validated, and ready for Apple App Store and Google Play Console reviewer submissions.
Subsequent updates following the MVP release will focus on automation, transaction syncing, and scaling the platform:
- 📲 Automated SMS Background Parser (Android): Native SMS interception to automatically log transactions from mobile money and bank notifications.
- 🔌 Open Banking APIs: Live syncing of accounts and cards using payment platforms like Paystack, Mono, or Fincra.
- ⏰ Automated Recurring Bills: Scheduler engine for recurring payments (e.g. rent, internet data, Susu groups) with push notifications.
- Susu Group Saving & SUSU Ledger: Support for Susu saving circles, allowing tracking of contributions across multiple users.
- 🛡️ Enterprise Security Hardening: Implementing SSL pinning in mobile, API rate limiting, and private database subnets.
- 💱 Multi-Currency Support: Track assets in GHS, USD, NGN, and GBP with real-time exchange rates.
For technical scope and implementation details, see Full Product & Technical Blueprint.
cd cedismart-api
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies
pip install -e ".[dev]"
# Configure
cp .env.example .env # Fill in DATABASE_URL, RSA keys, etc.
# Run migrations
alembic upgrade head
# Start server
uvicorn app.main:app --reload --port 8000Docs at: http://localhost:8000/docs (DEBUG=true only)
Full guide: cedismart-api/README.md
cd cedismart-mobile
# Install dependencies
npm install
# Start Expo dev server
npm start
# Press i (iOS Simulator), a (Android), or w (Web)Full guide: cedismart-mobile/README.md
cd cedismart-web
# Option 1: Simple HTTP server (no build)
python -m http.server 8080
# or: npx http-server
# Option 2: With Vite (hot reload)
npm install
npm run devOpen: http://localhost:8080 (or http://localhost:5173 with Vite)
Full guide: cedismart-web/README.md
Built by Clifford Darko Opoku-Sarkodie — Backend Engineer targeting FAANG, remote-first global companies, and African fintech startups.
This project exists to demonstrate production-grade engineering in a domain (African fintech) that is underrepresented in many portfolios: real market constraints, real security requirements, and real-world architecture tradeoffs.
© 2026 CediSmart. All rights reserved.