Marketplace immobilière
Jungle Immobilier
Plateforme de découverte de meublés au Cameroun, où les informations décisives (vidéo d'accès, points d'intérêt du quartier) se débloquent avec des jetons achetés en Mobile Money. Conçue, développée et déployée par une seule personne : API NestJS/GraphQL modulaire, front Next.js, transactions MongoDB et une topologie Traefik où seul le proxy est exposé.
- Client
- Produit personnel
- Rôle
- Fondateur — architecture, développement, infrastructure
- Période
- Décembre 2025 – aujourd'hui
- Statut
- En ligne (phase de démarrage)

Le problème
Au Cameroun, la recherche d'un meublé repose encore sur les visites physiques. Agents et locataires perdent du temps et de l'argent en déplacements pour des logements qui ne conviennent pas, et les informations qui font la décision (accès, quartier, eau, électricité) se découvrent souvent après la signature.
Jungle Immobilier part de ce constat. L'idée : vendre l'information de terrain à la consultation plutôt qu'à la visite, payée en Mobile Money parce que c'est le moyen de paiement réellement utilisé. Sur une telle plateforme, tout ce qui touche à l'argent doit être juste dès le premier jour : un jeton débité deux fois ou un déblocage accordé sans paiement, c'est un client perdu ou une vente perdue.
Le projet devait aussi tenir dans les contraintes d'un fondateur : un seul développeur, un petit VPS et une mise en ligne qui ne pouvait pas attendre une équipe. Le cahier des charges que je me suis fixé : une petite base de code faite proprement plutôt qu'une grande faite à peu près.
Le produit
jungle.immo est un site public Next.js : un fil de meublés classé par engagement, une fiche détaillée (galerie, équipements, points d'intérêt à proximité, règles de la maison, avis et commentaires) et une recherche filtrée. Chaque visiteur dispose d'un portefeuille de « Graines », rechargé par une commande Mobile Money que le site interroge jusqu'à confirmation par le prestataire.
Dépenser 5 Graines débloque une annonce pour 72 heures : la vidéo d'accès et les points d'intérêt restent masqués au niveau des résolveurs GraphQL tant qu'aucun droit actif n'existe pour cet utilisateur et cette annonce. Derrière le site, une API NestJS modulaire à sept bounded contexts (identité, catalogue, contenus, engagement, portefeuille, facturation, déblocage) expose 12 requêtes et 9 mutations au front, uniquement via le réseau Docker interne ; le navigateur ne parle jamais directement à l'API.
Un outil interne d'import et d'enrichissement média, avec sa propre base et sa console d'administration, alimente le catalogue : les annonces arrivent dans une zone de préparation, les images reçoivent leurs variantes et l'habillage de la marque, puis un opérateur les publie dans le catalogue public avec le statut d'import non vérifié. L'ensemble tourne sur un seul VPS derrière Traefik, avec deux replica sets MongoDB provisionnés avec des comptes à privilèges minimaux.
Mon rôle
Je suis le fondateur et le seul ingénieur du projet : périmètre produit, modèle de domaine, API, front, outil d'import, provisioning des bases, déploiement et revue de sécurité. Les 95 commits répartis sur les 10 dépôts sont tous de moi. La plateforme actuelle a été construite en juillet et août 2026, après une première itération démarrée en décembre 2025.
J'ai aussi rédigé la documentation d'exploitation : un guide de déploiement en huit jalons, chacun fermé par une barrière de sortie vérifiable, et quatre notes de conception (bordure réseau, durcissement, exécution, transport), pour que la prochaine personne sur le projet n'ait pas à rétro-concevoir l'installation.
Les défis
Un paywall à jetons qui ne peut pas perdre d'argent
Le problème
Débloquer une annonce, c'est débiter 5 Graines et créer un droit d'accès de 72 heures en un seul geste. Fait naïvement, une requête rejouée pouvait débiter deux fois, deux requêtes simultanées pouvaient accorder deux droits, et une clé d'idempotence capturée sur une annonce pouvait être réutilisée sur une autre. L'audit pré-lancement avait classé cette réutilisation de clé en critique.
Ce que j'ai construit
Le déblocage s'exécute dans une transaction MongoDB multi-documents : expiration des droits périmés, lecture d'un éventuel droit vivant, débit conditionnel par findOneAndUpdate gardé par balance >= amount, puis création du droit. Un index unique partiel sur les droits actifs rend le double déblocage impossible au niveau de la base, et la clé d'idempotence est rattachée à l'utilisateur et à l'annonce avant d'atteindre le ledger. Le ledger lui-même est en ajout seul, chaque écriture portant le solde après opération.
Créditer le Mobile Money sans faire confiance au webhook
Le problème
Un webhook de paiement est un appel HTTP entrant qui affirme qu'une commande est payée. Créditer un portefeuille sur la seule foi de ce contenu, c'est permettre à quiconque sait le forger ou le rejouer de fabriquer des jetons.
Ce que j'ai construit
Le webhook ne fait que déclencher une réconciliation de l'identifiant de commande. Le service de facturation interroge alors le prestataire pour obtenir le statut faisant foi et crédite le portefeuille dans une transaction, avec l'identifiant de commande comme clé d'idempotence : une commande est créditée une fois, jamais deux. Les prestataires se tiennent derrière un port unique PaymentProviderPort (initiate, checkStatus), ce qui rend NoKash et un second prestataire interchangeables. Les deux routes de webhook sont limitées à 30 requêtes par minute et constituent la seule partie de l'API joignable depuis Internet ; l'API refuse de démarrer en production si l'URL de base des webhooks n'est pas en HTTPS.
Rien de public, sauf le proxy
Le problème
Le site devait être mis en ligne sur un VPS qui hébergeait déjà une instance Traefik et d'autres applications, sans publier un seul port ni casser l'existant. Docker ajoute un piège : un port publié sur 0.0.0.0 est joignable depuis Internet même avec le pare-feu de l'hôte activé, parce que Docker insère ses propres règles NAT en amont. L'audit avait trouvé les deux bases exposées de cette façon, sans authentification.
Ce que j'ai construit
Tous les conteneurs rejoignent le réseau de Traefik et un réseau Jungle privé ; aucun port n'est publié sur l'hôte. jungle.immo est routé vers le conteneur web ; api.jungle.immo ne route que le préfixe /webhooks/ ; l'outil d'import est protégé par une authentification basique et un jeton partagé injecté par le proxy ; /health n'est autorisé qu'en boucle locale. MongoDB tourne avec authentification par keyfile, un compte lecture-écriture par base, un compte de sauvegarde en lecture seule et des caches WiredTiger plafonnés, et un script check-env rejette les URI de conteneur qui pointent vers localhost ou vers des ports de l'hôte. Le résultat se vérifie de l'extérieur : 200 sur le site, 403 sur /health, 404 sur le chemin GraphQL de l'API, 401 sur l'outil.
Un audit de sécurité avant le premier utilisateur
Le problème
Le passage d'un prototype local à une plateforme exposée sur Internet qui manipule de l'argent est précisément l'étape où les petits projets sautent la revue. Je voulais la liste de ce qui pouvait mal tourner avant la mise en ligne, pas après.
Ce que j'ai construit
Un audit pré-lancement structuré a produit 67 constats (13 critiques, 13 élevés, 21 moyens, 20 faibles). Chacun a été réexaminé : 40 confirmés, 27 jugés surestimés dans ce contexte, 34 classés bloquants pour le lancement. Les corrections ont été livrées entre le 17 et le 21 août 2026 : rattachement des clés d'idempotence, exposition des bases, introspection et traces d'erreur coupées en production, validation d'environnement à l'échec immédiat qui refuse les secrets courts et les valeurs de développement connues, et une mutation d'achat de test qui n'est même pas enregistrée dans le schéma de production.
Un outil d'import qu'on ne peut pas retourner contre son hôte
Le problème
L'outil d'import récupère des URL fournies par un opérateur et tourne sur le même hôte que des services privés. Sans garde-fou, une URL forgée le transforme en « confused deputy » capable de lire des points d'accès internes ou le réseau des conteneurs. L'audit avait qualifié la faille de SSRF complète.
Ce que j'ai construit
Chaque requête sortante passe par une couche safe-fetch : http et https seulement, refus des plages privées et link-local en IPv4 et IPv6, y compris les adresses IPv4 mappées, résolution DNS des noms d'hôte avant la requête et revalidation à chaque redirection, dans la limite de cinq. La fenêtre résiduelle entre vérification et usage de la résolution DNS est documentée et couverte par le pare-feu de l'hôte, plutôt que passée sous silence.
Le résultat
- Bounded contexts
- 7
- API GraphQL
- 12 requêtes · 9 mutations
- Lignes de TypeScript
- ~20 000API, site et outil d'import
- Commits
- 9510 dépôts, auteur unique
- Audit pré-lancement
- 67 constats17 août 2026 · 40 confirmés · 34 bloquants
- Déblocage
- 5 Graines / 72 h
- Sondes de bordure
- 200 · 403 · 404 · 401site · /health · chemin GraphQL de l'API · outil d'import
au 25 septembre 2026
jungle.immo est en ligne et la topologie décrite ci-dessus se vérifie depuis n'importe quel terminal (dernière vérification le 25 septembre 2026). La plateforme est en phase de démarrage : il n'y a pas encore de chiffres d'usage à communiquer, et aucun ne sera publié avant d'exister. Ce que le projet démontre, c'est la démarche d'ingénierie : une petite base de code d'environ 20 000 lignes de TypeScript entre l'API, le site et l'outil d'import, un découpage de domaine lisible, de l'argent manipulé de façon transactionnelle, et un déploiement audité avant le premier utilisateur plutôt qu'après le premier incident.
Les tests sont le point faible, et je le dis tel quel. Quelques tests unitaires couvrent la frontière entre les données importées et le schéma GraphQL non nullable, ainsi que l'adaptateur d'import, mais il n'existe pas encore de couverture automatisée large. Chaque jalon de déploiement est en revanche verrouillé par des vérifications manuelles consignées dans le guide d'exploitation.
Stack technique
- Backend
- NestJS 11
- Apollo Server 5, GraphQL code-first
- Mongoose 9
- Hachage argon2, JWT
- Limitation de débit, class-validator
- Front
- Next.js 16 App Router, sortie standalone
- React 19
- Tailwind CSS 4
- Server Actions avec cookie de session httpOnly
- Données
- Replica sets MongoDB (plateforme et préparation des imports)
- Transactions multi-documents
- Index uniques partiels
- Ledger de jetons en ajout seul
- Prisma 6 (outil d'import)
- Paiements
- NoKash Mobile Money (signature HMAC, push USSD, webhook)
- Second prestataire derrière le même PaymentProviderPort
- Crédit après réconciliation
- Outil d'import
- NestJS + Prisma
- Abonnements GraphQL (graphql-ws)
- cheerio, sharp
- Console d'administration Next.js avec urql et Zustand
- Infrastructure
- Images Docker multi-étapes, Docker Compose
- Traefik v3 (Let's Encrypt, en-têtes, authentification basique, liste d'IP autorisées)
- VPS Ubuntu 24.04, ufw, fail2ban
- Dépôt git bare avec hook post-receive
- GitHub Actions (lint et build)


