Au Cœur du Népal
Booking and operations platform for a French-speaking trekking agency
- Performance
- 93
- Accessibility
- 96
- Best Practices
- 100
- SEO
- 100
- Year
- 2022
- Type
- Client work
- Role
- Full-stack engineering and infrastructure
- Status
- Live

A French-language platform for a Kathmandu trekking agency: a catalogue of treks and ascents, bookings against real departure capacity, a staff area for guides and assignments, and an editorial blog. I built it end to end, from the Next.js site and Django API to the production Docker stack.
Context
Au Cœur du Népal is a trekking agency founded in Kathmandu by French-speaking Nepalis. Its clients are French-speaking travellers, so the entire public site, the booking flow and the staff tools work in French.
The agency needed more than a brochure: a catalogue of treks, ascents and cultural programmes with scheduled departures, bookings that respect how many places a departure really has, and a private area where staff, guides and porters see their assignments.
My role
I designed the data model and built the whole system: the Next.js front end, the Django REST API, the admin and operations interfaces, and the production infrastructure: Compose stack, Nginx, backups, CI and deployment.
Approach
The backend is split into apps that match the business: catalogue (treks, ascents, programmes), bookings, operations (staff, availability, assignments) and content (articles, testimonials, FAQs). Each app keeps writes in services and reads in selectors, so views stay thin and business rules have a single home.
The public site is statically rendered and revalidated on publish: when an editor saves in the admin, Django queues a Celery task that tells Next.js exactly which tags to rebuild. Pages stay as fast as static files without editors waiting out a cache window.
System architecture
Client
Browser · Travellers & staff
Travellers browse treks and book; agency staff and guides use the private admin. Everything is French-first.
Calls Nginx
Edge
Nginx · TLS · two hosts
Terminates TLS for the site and the API host, sets cache headers for optimised images, and caps upload sizes per route before a request reaches the application.
Calls Next.js, Django · DRF · Called by Browser
Application
Next.js · SSR · ISR · BFF
Renders public pages statically and revalidates them when content is published. Auth tokens stay in HttpOnly cookies behind same-origin route handlers.
Calls Django · DRF · Called by Nginx
Django · DRF · Gunicorn · REST API
Catalogue, bookings, operations and content APIs with service/selector boundaries. Booking capacity is rechecked under a row lock.
Calls PostgreSQL 17, Redis 7, Cloudflare R2, LanguageTool · Called by Nginx, Next.js, GitHub Actions
Celery worker · Email · revalidation
Sends transactional email and tells Next.js which pages to rebuild, off the request path, with retries.
Called by Redis 7
Beat · Scheduler
Database-backed schedule for cleanup of expired sessions and tokens.
Calls Redis 7 (async)
Data & services
PostgreSQL 17 · Primary store
Catalogue, departures, bookings, staff assignments and content. The application connects with a restricted role.
Called by Django · DRF, Backups
Redis 7 · Cache · queue
Cache, Celery broker, and session revocation and lockout state, in separate logical databases so a cache flush never drops queued jobs.
Calls Celery worker (async) · Called by Django · DRF, Beat
Cloudflare R2 · media.aucoeurdunepal.com
Object storage for photography and video. Uploads are decoded within pixel limits, re-oriented, resized to 2560px and stripped of metadata before storage.
Called by Django · DRF
LanguageTool · Private instance
A self-hosted French grammar checker for editors. Text is proofread without leaving the server.
Called by Django · DRF
Operations
Backups · Dump · verified restore
Scheduled database dumps, with restoration exercised by the production validation suite rather than assumed.
Calls PostgreSQL 17 (async)
GitHub Actions · Test · publish · deploy
Backend, frontend and production-container checks gate image publication to GHCR; deployment uses the exact, immutable images that passed.
Calls Django · DRF (async)
Technical decisions
Why Django REST Framework behind a Next.js front end?
- Decision
A separate Django API owns all data and business rules; Next.js renders and never touches the database.
- Why
The agency's hardest problems are relational: departures, capacity, staff assignments, permissions. Django's ORM, migrations, transactions and admin fit that directly, and the same API serves the public site, the staff tools and the admin.
- Alternatives considered
Next.js route handlers with an ORM such as Prisma, or a hosted headless CMS.
- Trade-off
Two runtimes to build, deploy and monitor, and an API contract to keep in step. In exchange, the business logic is testable in one place, independent of rendering.
Why tokens in HttpOnly cookies behind a BFF?
- Decision
Browser requests go to same-origin Next.js route handlers, which hold access and refresh tokens in HttpOnly cookies and call Django server-side.
- Why
Script on the page can never read a token, and refresh-token rotation with replay detection works without the browser handling secrets.
- Alternatives considered
Tokens in localStorage with an Authorization header.
- Trade-off
An extra hop for authenticated calls, and origin checks on every mutation.
Interface


Engineering challenges
A booking could land on a departure that had just closed
Problem
Booking creation checked capacity under a lock, but checked whether the departure was still open (not closed, cancelled or past) before taking it.
Constraint
Retries had to stay idempotent: a client resubmitting after a timeout must get its existing booking back, not a second one.
Solution
The eligibility check moved inside the transaction, after the idempotent-retry lookup and under the same inventory lock as the capacity check.
Result
Capacity and eligibility are now decided atomically, and a malformed legacy input returns a validation error instead of a database error.
Uploads that were small on disk but huge in memory
Problem
A compressed image can decode to an enormous bitmap, and original photos carried private metadata (including GPS) and far larger dimensions than the site ever displays.
Investigation
Tests with crafted files showed the decoder allocating before any size check ran.
Solution
Input pixels and animation frames are bounded before decoding; images are oriented, resized to at most 2560px, re-encoded as WebP and stripped of metadata.
Result
Smaller files, no location data leaking from guides' phones, and tests covering EXIF removal, transparency, animation timing and invalid files. Existing media need a separate, reviewed migration.
robots.txt hid the site from the renderer it was written for
Problem
The live robots.txt disallowed /_next/, which includes the scripts and optimised images crawlers need to render a Next.js page; separately, sitemap collectors stopped after the first 100 records.
Solution
Rendering assets are crawlable again, private routes are excluded explicitly, and sitemap and catalogue collectors walk every page of the API.
Result
Search engines see pages as visitors do, and large catalogues no longer drop out of the sitemap.
Performance, security and accessibility
A production quality review in September 2026 covered security, performance and accessibility against the live site and the production containers. It led to dependency upgrades that closed every open advisory, image-decoding limits for uploads, a contained mobile-menu focus, crawler access to rendering assets, and complete sitemap pagination. The backend suite runs against PostgreSQL; the browser suite runs against the real production images over HTTPS.
Infrastructure and deployment
Production is a single Compose stack on a VPS: Nginx, Next.js, Gunicorn, a Celery worker and Beat, PostgreSQL 17, Redis 7, a private LanguageTool instance and a backup service. Images are built once per commit, published to GHCR only after backend, frontend and production-container checks pass, and deployed by immutable reference, so what runs is exactly what was tested.
Lessons learned
Most of the serious defects were at boundaries (between a lock and a check, between an upload and a decoder, between robots.txt and the renderer) rather than inside any one component. Testing against the real production images, not a development server, is what surfaced them.
Technology
- TypeScript
- Next.js
- Django
- Django REST Framework
- Celery
- PostgreSQL
- Redis
- Cloudflare R2
- Docker
- Nginx
- GitHub Actions
- Playwright
- LanguageTool