Back

A vehicle rental marketplace where the hard part starts after checkout.

Hobblix is a vehicle rental marketplace for Estonia that I have built alone, end to end, since 2024. Listings and search are what renters see. Most of the work sits underneath: who can act for whom, how a booking moves, how money settles without ever moving twice, and what happens when a pickup goes wrong.

Pre-launch and private during development. A full walkthrough is available on request.

Hobblix landing page
The Hobblix landing page.
Role
Full-stack developer, independent project
Timeframe
2024 - 2026
Product
Vehicle rental marketplace and operations platform
Core stack
Nuxt, Vue, Prisma, Supabase, Stripe Connect

A rental booking isn't finished at checkout. It still has to be accepted, paid for at the right moment, picked up, returned, paid out, and sometimes disputed, and every one of those steps involves two parties who may be individuals or companies.

I built the whole system: the Nuxt frontend, the Prisma data model on Supabase Postgres, Stripe Connect payments, booking automation, handoff and incident flows, chat and notifications, the admin tools, and the tests and deploy checks that keep it honest. The app has about 90 pages, 330 API routes, and 71 data models.

One login, several roles

A person on Hobblix can rent, host their own vehicles, act inside a company, or run the platform. Login is separate from the context a person acts in, and the server checks company membership and role on every request rather than trusting the client. Companies invite teammates, who verify the claim before joining, and platform admin authority is kept separate from anything a company role grants.

Almost every feature depends on this. Listings, bookings, chat, notifications, dashboards, and Stripe readiness all ask whether the user is acting personally, for a company, or as an operator. Identity checks run through Veriff, and hosts finish Stripe onboarding before their listings can take bookings.

Bookings that can't collide

A booking moves through pending, confirmed, active, completed, cancelled, or declined, and carries payment state, handoff proof, claims, and an audit trail alongside. Every status change a person triggers goes through one command, so notifications, chat messages, and logs stay in step with the booking.

Two renters can't take the same car. Reservations take a Postgres advisory lock for the listing, and status updates only succeed if the booking is still in the state the request saw, otherwise they return a conflict. Dates are resolved on the server in the listing's own timezone, and a booking keeps a copy of the schedule, location, and extras it was made with, so editing a listing never rewrites one.

Scheduled work, like expiring unanswered requests, starting and completing rentals, charging future bookings, and sending reminders, runs from a Cloudflare Worker that triggers QStash jobs, with tokens that stop the same run happening twice.

Handoffs and no-shows

Pickup and return are their own phases. Smart Handoff collects confirmations, photos, and proof items from both sides, grades the evidence, and lets participants in through magic links. When someone doesn't show up, a claim can freeze the booking, open a support case, and escalate after a grace period. A missing return at the end of a rental is the most serious case and takes its own path.

Money that can't move twice

Near-term bookings authorize the card and capture later. Far-future bookings save the payment method and charge closer to the trip. Hosts are paid through Stripe Connect destination charges with the platform fee taken on the way.

Before any capture or refund reaches Stripe, the app writes a money-operation row with a fixed idempotency key and the exact request it is about to send. Database constraints allow only one active capture or refund per booking, and a recovery job finishes or replays any operation that was left unclear by a crash or a timeout. Stripe webhooks are stored, retried, and watched for events stuck in processing, and sent emails are recorded so a retry never sends one twice. Payout eligibility, reconciliation, and integrity checks run read-only against the same records.

The map and trip planner

Search and discovery happen on a MapLibre map served from PMTiles, with OSRM routing for planning a road trip around a rental. One map instance is shared between the explore and results layouts, and on phones the map's panels hook into the browser's Back button so it behaves like navigation.

The places on it come from a database I merged from the Estonian Land Board, OpenStreetMap, RMK, and Wikidata, with an admin screen for curating duplicates and aliases, so the map shows places worth driving to rather than whatever a single source happens to list.

Tests and deploy gates

With money and bookings involved, the test suite carries most of the confidence. Thousands of Vitest tests cover the domain rules, the server, and the UI. Search, where an off-by-one in a date or timezone means a wrong result, has golden tests that compare the SQL search against a separate reference implementation and run under two database timezones. Playwright walks through the main user journeys at desktop and phone sizes.

Every pull request runs lint, type checks, the tests, and a build, and a separate job replays every migration into an empty database to catch schema drift. Deploys run the migrations and then check the database's security rules: row-level security is on for every table and denies by default, and a deploy fails if any of it has loosened.

Running it

A marketplace is run by people as much as by code. The admin side lets an operator follow a booking's full history, work support cases, rerun scheduled jobs, inspect and retry Stripe webhooks, and check payouts against what was charged. Every admin action is logged with the state before and after, so a fix made by hand can always be traced.

Outcome

The hard problems in Hobblix were never single pages. They were keeping identity, booking state, money, and incidents consistent with each other as the system grew, and most of the code that matters lives where those meet. It is the project where I learned to treat correctness as something enforced by locks, constraints, and deploy checks, not by being careful.