A production-grade P2P vehicle rental platform with Stripe Connect multi-party payouts, weekly subscription billing, identity verification, real-time matching, and a fully self-serve admin control plane.
The founder of Rentcaar came to us with a specific problem: Australia's vehicle rental market is dominated by large fleets with inflexible pricing, while thousands of private car owners sit on idle vehicles they rarely use. His vision was a peer-to-peer marketplace — verified owners earning income from their cars, verified renters accessing flexible weekly rentals at rates traditional companies can't match.
That vision is straightforward. Building the software behind it is not. A P2P rental platform isn't just a listings site. It's a financial services product: you're moving real money between strangers, holding security bonds, managing failed payments, detecting no-shows, adjudicating disputes, and verifying identities — all in a regulated market (Australia) with real legal and financial consequences if any of it goes wrong.
We built Rentcaar on Rails 7.2 with a Hotwire-driven frontend, Stripe Connect for multi-party payments, and Didit for identity verification. The platform is live at rentcaar.com, handling the full lifecycle from host onboarding through weekly subscription billing through bond dispute resolution — without needing a developer in the loop for day-to-day operations.
Every rental involves three parties: the renter paying, the host receiving, and the platform taking a commission. That's not a simple payment — it's a multi-party financial transaction with bond holds, weekly recurring billing, partial refunds, and dispute-triggered transfers. The bond alone has at least five resolution paths: full refund to renter (normal completion), full transfer to host (no-show), split decision (damage dispute), forfeiture (admin override), and additional charge (damage exceeding the bond). Building this on Stripe Connect without introducing race conditions, double-charges, or unrecoverable states was the hardest single engineering problem on the project.
Between the moment a renter checks availability and the moment their payment clears on Stripe, another renter could book the same car. This is a classic time-of-check to time-of-use race condition — entirely possible on Stripe's hosted checkout where the renter is off your server for 30–90 seconds. Most marketplaces ignore this until it causes a double-booking in production. We had to solve it before launch.
Beyond "browse and book," Rentcaar needed an active matching layer: renters post requirements, hosts browse demand and reach out; renters browse listings and initiate contact. Both directions needed quota enforcement (to prevent spam), 24-hour expiry windows, verification gates (identity check before accepting), and a cooldown mechanism after quota fill. The state machine had to be correct — an accepted match that bypasses verification or exceeds quota without the cooldown is a trust problem, not just a UX glitch.
Hosts need insurance documents (Certificate of Currency, CTP registration) reviewed and approved before they can publish a listing. Renters need identity verification (government ID + liveness check) before certain matching interactions. These two pipelines are different in nature: one is async human review by an admin, the other is automated via a third-party API (Didit). Both gate critical actions, so any failure or delay in either pipeline stalls the user and needs clear status communication throughout.
The founder needed to run the marketplace — adjusting commission rates, tuning match quotas, reviewing bond claims, approving documents, banning bad actors — without filing tickets to a developer for every operational decision. Building an admin dashboard that is both powerful enough for all these operations and safe enough to use without breaking production is harder than it sounds, especially when financial operations like bond decisions trigger irreversible Stripe API calls.
Renters and hosts need instant feedback: when a match request arrives, when a payment succeeds or fails, when a document gets approved, when an identity check completes. Email alone isn't enough for a marketplace where timing matters (match expires in 24 hours). A real-time notification system needed to work across 35 distinct event types without becoming a maintenance nightmare as the event list grew.
We architected payments in two distinct phases. Phase one: the renter pays the security bond via a Stripe-hosted Checkout session before the booking is confirmed. This keeps us fully outside PCI scope — we never touch card data. Phase two: once bond payment clears and our BookingFinalizationService runs, we create a Stripe Subscription with 13 weekly invoices anchored to the pickup date. The subscription handles all recurring billing automatically, fires webhooks on each success or failure, and cancels itself on the second consecutive payment failure — terminating the booking without manual intervention. Host payouts flow through Stripe Connect Express accounts, with the platform's commission taken as an application fee on each invoice. Bond resolution is handled entirely through our StripeService class — the single gateway rule means every Stripe API call goes through one place, returns a typed Result object, and logs every request/response with PII stripped.
The double-booking race condition is solved in BookingFinalizationService. When the renter returns from Stripe checkout, before we create the Bond record or start the subscription, we acquire a row-level lock on the listing (listing.with_lock), re-run the full availability check inside the transaction, and only proceed if the car is still free. If another booking snuck through during checkout, we immediately refund the bond via Stripe and surface a clear error. The entire finalization — lock, availability check, Bond record, Subscription creation, confirmed_at timestamp — runs in a single database transaction. Either all of it succeeds or none of it does.
The matching system handles two fundamentally different flows — renter-initiated (against a listing) and host-initiated (against a requirement) — using a single Match model. We enforced correct usage at the database level with a CHECK constraint that ensures exactly one of listing_id or requirement_id is set, never both, never neither. Quota limits (renter: 5 concurrent, host: 2 across all cars, per-listing: 3) are enforced at validation time with clear error messages, not silently rejected. The 24-hour expiry runs via a Solid Queue recurring job — no Redis, no separate scheduler daemon. Verification states (needs_renter_verification, needs_host_verification) are first-class enum values in the state machine, so the UI can always show exactly what's blocking an acceptance without conditional logic scattered across views.
We integrated Didit's hosted identity verification flow (government ID + passive liveness + face match) to replace Stripe Identity at a fraction of the cost — 500 free verifications per month plus $0.33 each versus $1.50–2.50 per check with Stripe Identity. The integration handles both the webhook path (Didit fires async confirmation) and the return URL path (user comes back before webhook arrives), setting identity_verified_at from whichever fires first. For host documents, we built a moderation queue in the admin panel: pending uploads arrive with expiry dates, admins approve or reject with notes, and PaperTrail captures the full audit trail including who reviewed what and when. Both pipelines trigger in-app bell notifications and emails on every status change.
Rather than polling or page refreshes, notifications push instantly via ActionCable. Every call to NotificationService.notify() creates a database record then broadcasts a Turbo Stream action to the recipient's private channel — prepending the new notification to the dropdown and updating the unread badge count. The 35 event types share a single JSONB payload field for flexible context without schema migrations every time a new event type ships. Mailers deliver email copies async via Solid Queue. The result is a notification system that feels real-time to users and is trivially extensible for new event types.
We built a SiteSetting singleton record that controls every tunable parameter in the marketplace: commission percentage, bond multiplier, minimum rental weeks, match quotas and cooldowns, feature flags for the matching system and verified car badge, and featured city display. Changes take effect immediately (5-minute cache TTL) without a deploy. The admin dashboard covers the full operational surface: document moderation queue, bond claims adjudication (award host, award renter, split with custom amounts, or deny — all with Stripe API calls wired up), user management with risk scoring and ban actions, listing promotion, and a complete PaperTrail audit log on every irreversible operation. Staff roles can read but not mutate financial decisions; admin roles have full access. Pundit policies enforce this consistently across every controller action.
Let's discuss how we can apply our expertise to your project.
Hi there!
Need help with your project? We're online and ready to assist.