La gestion locative manuelle au Sénégal ou en Côte d'Ivoire est un cauchemar : Excel pour les loyers, WhatsApp pour les communications, paperasse pour les baux, tournées physiques pour collecte. Au-delà de 30-50 logements gérés, un SaaS dédié devient indispensable.
TL;DR
- Stack : Next.js + Postgres multi-tenant + Wave/OM API + WhatsApp.
- Core features : baux, états des lieux, loyers récurrents, charges, tickets maintenance.
- Marché Afrique francophone : ~200K+ propriétaires gérant 5-200 biens en 2026.
Architecture globale
`
[Locataire] [Propriétaire / gestionnaire]
↓ ↓
[App locataire] [Dashboard gestionnaire]
↓ ↓
[Backend SaaS multi-tenant]
↓
[Postgres + Redis]
↓
┌──────────┴──────────┐
↓ ↓
[Wave / OM] [Brevo + WhatsApp]
`
Étape 1 — modèle de données multi-tenant
`prisma
model Organization {
id String @id @default(cuid())
name String // "Cabinet Diallo Gestion"
email String
plan Plan @default(STARTER)
properties Property[]
users User[]
}
model Property {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
reference String
address String
city String
district String
type String // APT / HOUSE / VILLA / COMMERCIAL
surface Int
rentAmount Int // XOF mensuel
charges Int // mensuel
deposit Int
isOccupied Boolean
currentLeaseId String?
leases Lease[]
tickets MaintenanceTicket[]
}
model Lease {
id String @id @default(cuid())
organizationId String
propertyId String
property Property @relation(fields: [propertyId], references: [id])
tenantId String
tenant Tenant @relation(fields: [tenantId], references: [id])
startDate DateTime
endDate DateTime
rentAmount Int
charges Int
deposit Int
paymentDueDay Int // 1, 5, 10 du mois
status LeaseStatus // ACTIVE / TERMINATED / EXPIRED
inventoryIn Inventory? @relation("InventoryIn")
inventoryOut Inventory? @relation("InventoryOut")
payments RentPayment[]
}
model Tenant {
id String @id @default(cuid())
organizationId String
firstName String
lastName String
email String
phone String
whatsapp String?
cniNumber String // CNI
ninea String?
emergencyContact String?
leases Lease[]
}
model RentPayment {
id String @id @default(cuid())
organizationId String
leaseId String
lease Lease @relation(fields: [leaseId], references: [id])
month String // "2026-05"
amount Int
charges Int
status PaymentStatus // PENDING / PAID / LATE / PARTIAL
paidAt DateTime?
paymentMethod String? // wave | orange_money | cash | bank_transfer
reference String?
@@unique([leaseId, month])
}
model MaintenanceTicket {
id String @id @default(cuid())
organizationId String
propertyId String
property Property @relation(fields: [propertyId], references: [id])
category String // PLUMBING / ELECTRICAL / PAINT / OTHER
description String
status TicketStatus // OPEN / IN_PROGRESS / RESOLVED
reportedAt DateTime @default(now())
resolvedAt DateTime?
cost Int?
}
model Inventory {
id String @id @default(cuid())
leaseId String @unique
type String // ENTRY / EXIT
date DateTime
rooms Json // structure JSON détaillée
pdfUrl String?
signedTenantUrl String?
signedOwnerUrl String?
}
`
Étape 2 — multi-tenancy strict
Tous les queries doivent filtrer par organizationId. Plugin Prisma global :
`ts
// lib/prisma-tenant.ts
import { Prisma, PrismaClient } from '@prisma/client';
export function tenantPrisma(organizationId: string) {
return new PrismaClient().$extends({
query: {
$allModels: {
async $allOperations({ args, model, operation, query }) {
// Skip pour Organization elle-même + User auth
if (['Organization', 'User'].includes(model ?? '')) {
return query(args);
}
// Inject filtre WHERE
if (operation.startsWith('find') || operation === 'count' || operation === 'aggregate') {
args.where = { ...args.where, organizationId };
}
// Inject CREATE
Besoin d'un site web professionnel ?
Kolonell crée des sites web qui attirent des clients, optimisés pour le marché sénégalais. Devis gratuit en 2 minutes.
if (operation === 'create') {
args.data = { ...args.data, organizationId };
}
return query(args);
},
},
},
});
}
`
Sans ce middleware, fuite de données entre tenants = catastrophe.
Étape 3 — collecte loyers automatisée Wave / OM
Cron mensuel (le 1er du mois) :
`ts
// jobs/generate-monthly-rent-payments.ts
export async function generateMonthlyPayments() {
const today = new Date();
const monthKey = ${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')};
const activeLeases = await prisma.lease.findMany({
where: { status: 'ACTIVE' },
include: { property: true, tenant: true, organization: true },
});
for (const lease of activeLeases) {
// 1. Créer RentPayment
const payment = await prisma.rentPayment.create({
data: {
organizationId: lease.organizationId,
leaseId: lease.id,
month: monthKey,
amount: lease.rentAmount,
charges: lease.charges,
status: 'PENDING',
},
});
// 2. Notifier locataire WhatsApp + email
const totalAmount = lease.rentAmount + lease.charges;
const paymentLink = await createWavePaymentLink({
amount: totalAmount,
currency: 'XOF',
reference: payment.id,
callbackUrl: https://app.kolonell.com/api/webhooks/rent-payment,
});
await sendWhatsApp(lease.tenant.whatsapp ?? lease.tenant.phone, {
template: 'rent_due',
params: [
lease.tenant.firstName,
monthKey,
totalAmount.toLocaleString(),
paymentLink,
],
});
}
}
`
Cron tous les 5 jours après échéance pour relances :
`ts
// J+5, J+10, J+15 après date due
const overdue = await prisma.rentPayment.findMany({
where: {
status: 'PENDING',
month: previousMonth,
createdAt: { lt: new Date(Date.now() - 5*24*60*60*1000) },
},
include: { lease: { include: { tenant: true, property: true } } },
});
for (const p of overdue) {
await escalateOverdue(p);
}
`
Étape 4 — état des lieux numérique
`tsx
// app/leases/[id]/inventory/[type]/page.tsx
'use client';
import { useState } from 'react';
export default function InventoryWizard({ leaseId, type }) {
const [rooms, setRooms] = useState([]);
return (
{rooms.map((room, i) => ( key={i} room={room} onUpdate={(updated) => updateRoom(i, updated)} /> ))} Générer PDF + signer (DocuSign / Yousign)État des lieux d'{type === 'ENTRY' ? 'entrée' : 'sortie'}
);
}
function RoomEntry({ room, onUpdate }) {
return (
État
Photos
Notes
);
}
`
PDF généré automatiquement avec photos intégrées + signatures électroniques.
Étape 5 — pricing SaaS
| Plan | Prix mensuel | Limites |
|---|---|---|
| Starter | 25K FCFA | 1-10 logements |
| Pro | 75K FCFA | 11-50 logements |
| Business | 200K FCFA | 51-200 logements |
| Enterprise | Sur devis | 200+ logements |
Marché : ~10 000-20 000 PME gestion immobilière éligibles en Afrique francophone. Marché potentiel : 5-15 milliards FCFA annuel.
Cas réel — Cabinet Diallo (Dakar)
Profile : 87 logements gérés, 3 employés.
| Métrique | Avant SaaS | Après 6 mois |
|---|---|---|
| Tâches admin/mois | ~280 h | ~95 h |
| Loyers en retard | 18 % | 6 % |
| Tickets maintenance suivis | 60 % | 100 % |
| Conflits dépôt de garantie | 12/an | 2/an |
| Erreurs comptables | 8/mois | 0-1/mois |
Économies : ~1.2 M FCFA/mois équivalent salaires + zéro contentieux.
FAQ
Q : Concurrence sur ce marché ?
R : Quelques solutions internationales (LIMS, AppFolio) inadaptées Afrique. Locales : Diaspora-Conseil, NestImmo (CI). Marché peu mature = opportunité.
Q : Conformité RGPD/CDP données locataires ?
R : Critique. Voir guide RGPD + loi 2008-12 →.
Q : Intégration banques pour virements automatiques ?
R : Limitée en 2026. SGBS, Société Générale Sénégal, BNP Paribas Afrique offrent API limitées. Wave/OM = bien plus mature.
Conclusion
Le marché de la gestion locative SaaS en Afrique est en émergence en 2026 — opportunité claire pour qui construit propre. Stack technique 6-12 mois pour MVP, ARR potentiel 50-200M FCFA/an à 24 mois sur Sénégal+CI seul.
Mohamed Bah
Fondateur, Kolonell
Passionné par le digital et l'entrepreneuriat en Afrique, Mohamed accompagne les entreprises sénégalaises dans leur transformation digitale depuis 2020. Fondateur de Kolonell, il croit que chaque PME mérite une présence en ligne professionnelle et accessible.

