chat with agentaskProjects2 folders 4 filesAI and agents2 folders 2 filesInfrastructure3 foldersDevelopment5 filesAbout5 files
Caerus - car rental in Montenegro, from separation to growth
  • Next.js
  • payments
  • SEO

Caerus - car rental in Montenegro, from separation to growth

From 2024 the rental’s site ran on the engine of the GlobalRental aggregator: in production both products ran the same binary against the same database and differed only by environment variables, with no tenancy in the schema. GlobalRental was closed, but the rental kept taking bookings on that engine. The site had to be separated without taking a working business down along the way.

The split

I started with a production audit: a map of processes, the differences between configurations, what controls what. Took a snapshot as the rollback point, and only then separated them. In a setup like this the data is not the dangerous part - the implicit couplings are: a shared process, shared environment variables, a shared deploy. Until they are written down, any separation is done blind.

After that I ran the product alone: all 92 commits of that stage are mine, and these days it is bug fixes and support. Next.js 14 with React 18 and TypeScript, Express with Prisma over PostgreSQL, three languages via a URL prefix, JWT in cookies, deploys through GitHub Actions onto PM2.

What the product does

A catalog with filters and price calculation, booking with availability checks, a partner dashboard, an admin panel. Contracts are generated as PDFs and emails go through a dedicated service. Online payment was removed from the booking flow at the owner’s request. The Stripe integration is still in the code; it was verified with a live payment back on GlobalRental.

A separate part of the work is organic traffic: human- and search-friendly URLs, pages targeting queries like “car rental in Montenegro”, structured data and meta tags editable from the admin panel without a deploy.

Three bugs that show what production is like

A search crawler took the catalog down. TypeError: e.data.map is not a function arrived only with Baiduspider. The root cause was not in the parser: an axios interceptor returned error.response instead of throwing, so on failure data could be anything and the next .map() crashed. I fixed it in two places at once - a guard on each of the three parallel requests and a type check before iterating - because that anti-pattern hits every consumer of the interceptor, not one page.

An infinite loop in mobile filters. A RangeError when selecting automatic transmission in the Budva catalog, reproducible on iPhone. The cascade: setValue inside an effect triggered the form watcher, the watcher triggered submit, and submit wrote values again. The fix is not a rewrite but a re-entry guard plus an initialization flag with a timer cleared on effect teardown.

A voucher with the wrong time. A customer books 10:00 and the PDF and email say 08:00. The production process runs in UTC, and unlike every other path, the PDF and emails are rendered on the backend without converting the date into the city’s timezone. I added the conversion and a test covering a daylight saving transition, then applied it to the voucher and the confirmation and cancellation emails. I deliberately left the frontend alone: it already shows the correct local time through a backend offset, and “fixing” it there would double-convert.

What came of it

The dashboard became the rental’s working calendar: about 1,400 entries over two years. Direct bookings from the site keep growing: 13 in 2025, 31 in 2026, up to 10 a month in season.