Skip to content
Back to work

PropTech marketplace

Jungle Immobilier

A furnished-rental discovery platform for Cameroon where the details that matter (access video, neighbourhood hotspots) are unlocked with tokens bought through Mobile Money. Designed, built and deployed by one person: a modular NestJS/GraphQL API, a Next.js front end, MongoDB transactions and a Traefik topology where nothing but the proxy is exposed.

Client
Own product
Role
Founder — architecture, development, infrastructure
Period
December 2025 – present
Status
Live (early stage)
Home page of jungle.immo showing the hero and the furnished-rental search entry points
Home page of jungle.immo showing the hero and the furnished-rental search entry points
Home
Search page with city, price and amenity filters and a grid of furnished-rental results
Search
Listing detail page with photos, amenities and the token-based unlock panel
Listing detail
How-it-works page explaining the four-step search, discover, unlock and contact flow
How it works

The problem

In Cameroon, finding a furnished rental still runs on physical visits. Agents and tenants spend time and transport money going to see places that turn out to be wrong, and the information that decides a deal (how to get there, the neighbourhood, water and electricity supply) is often discovered after signing.

Jungle Immobilier starts from that gap. The idea is to sell field information per consultation rather than per visit, paid in Mobile Money because that is what people actually use. On such a platform, anything that touches money has to be right from day one: a token debited twice or an unlock granted without payment is a lost customer or a lost sale.

The project also had to fit a founder's constraints: one developer, a small VPS and a launch that could not wait for a team. The brief I set myself was a small codebase done properly rather than a large one done approximately.

The product

jungle.immo is a public Next.js site: a feed of furnished listings ranked by engagement, a detail page with gallery, amenities, nearby points of interest, house rules, reviews and comments, and a filtered search. Each visitor has a wallet of “Seeds”, topped up through a Mobile Money order that the site polls until the provider confirms it.

Spending 5 Seeds unlocks a listing for 72 hours: the access video and the hotspots stay hidden at the GraphQL resolver level until an active grant exists for that user and that listing. Behind the site, a modular NestJS API with seven bounded contexts (identity, catalog, content, engagement, wallet, billing, unlock) exposes 12 queries and 9 mutations to the front end over the internal Docker network only; the browser never talks to the API directly.

An internal import and media-enrichment tool, with its own database and admin console, feeds the catalogue: listings land in a staging area, images get their variants and branding, and an operator publishes them to the public catalogue flagged as unverified imports. Everything runs on one VPS behind Traefik, with two MongoDB replica sets provisioned with least-privilege accounts.

My role

I am the founder and the only engineer on the project: product scope, domain model, API, front end, import tool, database provisioning, deployment and security review. All 95 commits across the 10 repositories are mine. The current platform was built in July and August 2026, after a first iteration started in December 2025.

I also wrote the operating documentation: an eight-milestone deployment guide with a verifiable exit gate for each step, and four design notes covering the edge, hardening, runtime and transport decisions, so that the next person on the project does not have to reverse-engineer the setup.

The challenges

  1. A token paywall that cannot leak money

    The problem

    Unlocking a listing means debiting 5 Seeds and creating a 72-hour access grant in one move. Done naively, a retried request could debit twice, two concurrent requests could grant twice, and an idempotency key captured on one listing could be replayed on another. The pre-launch audit flagged that key reuse as critical.

    What I built

    The unlock runs in a MongoDB multi-document transaction: expire stale grants, read any live grant, debit with a conditional findOneAndUpdate guarded by balance >= amount, then create the grant. A partial unique index on active grants makes a double unlock impossible at the database level, and the idempotency key is re-scoped to the user and the listing before it reaches the ledger. The ledger itself is append-only, each entry carrying the balance after the operation.

  2. Crediting Mobile Money without trusting the webhook

    The problem

    A payment webhook is an inbound HTTP call claiming that an order is paid. Crediting a wallet on the strength of that payload alone means that anyone able to forge or replay it can mint tokens.

    What I built

    The webhook only triggers a reconciliation of the order ID. The billing service then asks the provider for the authoritative status and credits the wallet inside a transaction, with the order ID as idempotency key, so an order is credited once and never again. Providers sit behind a single PaymentProviderPort (initiate, checkStatus), which keeps NoKash and a second provider interchangeable. The two webhook routes are throttled to 30 requests a minute and are the only part of the API reachable from the internet, and the API refuses to start in production with a webhook base URL that is not HTTPS.

  3. Nothing public except the proxy

    The problem

    The site had to go live on a VPS that already ran a Traefik instance and other applications, without publishing a single port or breaking what was there. Docker adds a trap: a port published on 0.0.0.0 is reachable from the internet even with the host firewall on, because Docker inserts its own NAT rules ahead of it. The audit found both databases exposed that way, without authentication.

    What I built

    All containers join Traefik's network and a private Jungle network, and no port is published on the host. jungle.immo routes to the web container; api.jungle.immo routes only the /webhooks/ prefix; the import tool sits behind basic auth plus a shared token injected by the proxy; /health is allow-listed to loopback. MongoDB runs with keyfile authentication, one read-write account per database, a read-only backup account and capped WiredTiger caches, and a check-env script rejects container URIs that point at localhost or host ports. The result is verifiable from outside: 200 on the site, 403 on /health, 404 on the API's GraphQL path, 401 on the tool.

  4. A security audit before the first user

    The problem

    Going from a local prototype to an internet-facing platform that handles money is exactly where small projects tend to skip the review. I wanted the list of what could go wrong before launch, not after.

    What I built

    A structured pre-launch audit produced 67 findings (13 critical, 13 high, 21 medium, 20 low). Each one was re-examined: 40 were confirmed, 27 were judged overestimated for this context, and 34 were classed as blocking for launch. The fixes landed between 17 and 21 August 2026: idempotency scoping, database exposure, introspection and stack traces disabled in production, fail-fast environment validation that rejects short secrets and known development values, and a sandbox purchase mutation that is not even registered in the production schema.

  5. An import tool that cannot be turned against its host

    The problem

    The import tool fetches URLs supplied by an operator and runs on the same host as private services. Without a guard, a crafted URL turns it into a confused deputy that reads internal endpoints or the container network. The audit rated this a full SSRF.

    What I built

    Every outbound request goes through a safe-fetch layer: http and https only, refusal of private and link-local ranges in IPv4 and IPv6 including IPv4-mapped addresses, DNS resolution of hostnames before the request, and re-validation on every redirect up to a limit of five. The residual DNS time-of-check/time-of-use window is documented and covered by the host firewall rather than assumed away.

The outcome

Bounded contexts
7
GraphQL API
12 queries · 9 mutations
Lines of TypeScript
~20,000API, site and import tool
Commits
9510 repositories, single author
Pre-launch audit
67 findings17 August 2026 · 40 confirmed · 34 blocking
Unlock
5 Seeds / 72 h
Edge probes
200 · 403 · 404 · 401site · /health · API GraphQL path · import tool

as of 25 September 2026

jungle.immo is live and the edge topology described above can be checked from any terminal (last verified on 25 September 2026). The platform is early stage: there are no usage figures to report yet, and none will be published until they exist. What the project demonstrates is the engineering approach: a small codebase of about 20,000 lines of TypeScript across the API, the site and the import tool, a clear domain split, money handled transactionally, and a deployment audited before the first user rather than after the first incident.

Testing is the honest gap. A handful of unit tests cover the boundary between imported data and the non-nullable GraphQL schema, and the import adapter, but there is no broad automated suite yet. Each deployment milestone is instead gated by manual checks written into the runbook.

Technical stack

Backend
  • NestJS 11
  • Apollo Server 5, code-first GraphQL
  • Mongoose 9
  • argon2 password hashing, JWT
  • Request throttling, class-validator
Front end
  • Next.js 16 App Router, standalone output
  • React 19
  • Tailwind CSS 4
  • Server Actions with an httpOnly session cookie
Data
  • MongoDB replica sets (platform and import staging)
  • Multi-document transactions
  • Partial unique indexes
  • Append-only token ledger
  • Prisma 6 (import tool)
Payments
  • NoKash Mobile Money (HMAC-signed, USSD push, webhook)
  • Second provider behind the same PaymentProviderPort
  • Reconciliation-first credit
Import tool
  • NestJS + Prisma
  • GraphQL subscriptions (graphql-ws)
  • cheerio, sharp
  • Next.js admin console with urql and Zustand
Infrastructure
  • Docker multi-stage images, Docker Compose
  • Traefik v3 (Let's Encrypt, headers, basic auth, IP allow-list)
  • Ubuntu 24.04 VPS, ufw, fail2ban
  • Bare git repository with a post-receive hook
  • GitHub Actions (lint and build)