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)

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
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.
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.
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.
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.
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)


