All projects

YathraBook

An invoicing app that has to work in a basement car park, and a serverless backend that costs almost nothing when nobody is using it.

Open YathraBook
Screenshot of YathraBook
Role
Full stack: API, PWA, Android build, billing
Built at
Bpract Software Solutions LLP
Status
Live
Backend
Laravel 12 on AWS Lambda via Bref, Neon Postgres, Upstash Redis, Cloudflare R2
Client
React 19 and Vite PWA with Dexie over IndexedDB, wrapped for Android with Capacitor

The problem

A solo taxi driver bills at the end of a trip, standing next to the car. That is a parking basement, a hotel portico, a village with one bar of signal. Every invoicing tool assumes a desk and a connection, so the driver writes the bill on paper and types it in later, or does not type it in at all.

The second problem is the business model. One driver pays a few hundred rupees a month, which does not buy a server sitting idle at three in the morning. The running cost had to scale down to almost nothing between requests or the product could not be priced for the person it is for.

What it does

Record a trip, raise an invoice and send it, with no network involved. The app is not degraded offline; offline is simply how it works, and the network is what backs it up afterwards.

Expenses, trip history and the driver's own numbers, all readable on the device without waiting for anything.

Subscriptions and autopay through Razorpay, with a super admin control plane for drivers, pricing and support.

An Android app from the same codebase, so there is one product to fix rather than two.

How it is built

Offline is the default, not a fallback

The driver-facing UI reads from a local IndexedDB database through Dexie. It does not read from the API, ever. Recording a trip, creating an invoice and browsing history are local operations that complete whether or not there is signal.

That inverts the usual relationship: the cloud is a backup, not the source of truth for the driver. It is also the invariant everything else has to respect, because the moment one screen quietly needs the network, the promise the product is sold on stops being true in exactly the place it matters.

A sync you can safely retry

Every operation captured offline carries a client-generated UUID, and the server table is unique on the pair of user and that UUID. A batch that is sent twice, because the connection dropped halfway through the response, cannot post anything twice.

Sync is one endpoint. The client pushes its queued operations, the server replies with what changed since the client's cursor, and the client applies it. A single bad operation is isolated and reported rather than failing the batch, so one malformed row cannot wedge a driver's entire queue.

Sync is never behind the paywall

An expired subscription blocks what a driver would use the platform for. It does not block sync. A driver whose payment lapsed while they were working still gets the week of trips sitting on their phone into the cloud.

The alternative is destroying a customer's work to enforce a payment, which is not a trade worth making for any amount of revenue.

Invoice numbers that survive two Lambdas at once

Invoice numbers were allocated by reading the driver's next number and then writing it back, with no lock and no unique index. Under Lambda that is two concurrent requests from one driver getting the same number, which is a billing and compliance problem rather than a cosmetic one.

Allocation now happens inside a transaction that locks the row, with a unique index on the driver and number pair behind it, so the database refuses a duplicate even if the application logic is ever wrong again.

Payment state comes from the webhook, not the browser

The Razorpay webhook is the source of truth for whether money moved. The browser redirect after checkout is treated as a hint: it triggers a fresh read from the Razorpay API and trusts nothing in the request body, because anything the browser sends is something a user can edit.

Access and autopay are deliberately separate. Whether a driver can use the product and whether their mandate is healthy are different questions with different failure modes, and collapsing them into one status field is how a driver with a broken mandate gets locked out of an account they have paid for.

Serverless without the serverless bill

Laravel 12 runs on AWS Lambda through Bref. The database is Neon, the cache and rate limiter are Upstash Redis, and files go to Cloudflare R2, which does not charge for egress.

The marketing site is separate: a Next.js build that fetches the blog once per publish rather than per visitor, and is told to rebuild by the API. In steady state the blog costs one API call each time somebody presses publish.

One codebase, two things to install

The same React app ships as an installable PWA on the web and as an Android app through Capacitor. The Android build disables the service worker, since the native shell is already doing that job and two caches fighting over the same assets is a bug that only appears on real devices.

If you have something like this that needs building or fixing, the contact form reaches me.

Get in touch