SaaS · Security · Supabase

A garage logs the work, the customer confirms it with a one-time code, and only then does the record count. Here is how we built that trust into the database rather than the app.

·3 min read·Appna Studio

Most people in India trust their mechanic blindly. There is rarely a shared record of what was done to a car, what it cost or who did it — and the garages doing honest work have no easy way to prove it. With KnowyourMechanic we set out to fix that with one simple rule: a service record only counts once the customer has confirmed it.

The flow is short. The garage logs a service against the customer's phone number. The customer receives a one-time code and reads it back to the garage. The garage collects payment on its own UPI QR code. The verified record then appears in both the garage's dashboard and the customer's vehicle history.

Simple to describe; the interesting part is making it impossible to fake.

The rule has to live in the database

It is tempting to enforce a rule like this in the app: hide the "approve" button until an OTP is entered, check the code on the phone, and so on. The problem is that anything enforced only in the interface can be bypassed by anyone who talks to the API directly.

So every state change goes through Postgres itself. Row-level security decides who can see which rows, and each step of the workflow is a SECURITY DEFINER function that checks ownership and the current state before it changes anything:

  • Creating a record checks that the caller owns the garage, validates the services and faults being logged, normalises the phone number and writes the record and its related rows in a single transaction.
  • Verifying the OTP locks the active code, enforces its expiry and an attempt limit, and only then marks the record as approved by the customer.
  • Completing payment refuses to run unless the record is already OTP-verified, then writes a unique invoice number and a payment ledger entry.

A buggy screen, or a curious developer with an API client, cannot skip a step, because the database will not let the record move forward.

Why the OTP never touches the garage's app

One early design question was where the one-time code should be generated. The obvious place is the same function that creates the record — but then the function would have to return the code, and any garage-side client could read it and "approve" the job on the customer's behalf.

Instead, OTP generation and delivery happen in a separate server function running with elevated privileges:

  1. It verifies the garage's session and calls the record-creation function as that garage, so the ownership check still runs.
  2. It generates a six-digit code and stores only a salted, peppered hash of it — never the code itself.
  3. It sends the code by SMS to the customer's phone.

The garage's app learns only that a code was sent, never its value. The customer's phone is the only place the code exists.

Small details that matter

Wrong codes are counted, not thrown. When a customer enters the wrong code, the verify function returns a failure instead of raising an error. That sounds like a nitpick, but raising an error would roll back the transaction — including the increment of the attempt counter. Returning keeps the attempt recorded, so the attempt limit actually works.

Codes are single-use and expire. A used, expired or exhausted code is consumed, so the same code can never approve a second job.

The SMS provider sits behind one module. Only one file talks to the SMS gateway, so switching providers is a change in one place rather than a hunt through the codebase.

Verified payments are marked as such. A payment made on the garage's UPI QR is recorded differently from cash, because one can be verified and the other cannot. Trust is only useful if you are honest about where it ends.

What this means for a product

The result is a record both sides can rely on. The customer knows the work in their history was confirmed by them. The garage can point to a verified history of honest work. And neither depends on trusting a screen.

The same approach — rules in the data layer, secrets kept server-side, every change traceable — is how we build SaaS and web applications and the backends behind our mobile apps. If you are building something where trust between two parties is the product, we would like to hear about it.

Free · 30 seconds · No sign-up

How fast is your website on a phone?

Get your score, what Google can’t read, and the three fixes that matter, as a PDF in your inbox.

Check my website →
06Contact

Tell us what you’re building. Every enquiry gets a considered reply with honest thoughts on scope, approach and timeline.

Need an official quote? Attach your RFQ (request for quotation) or project brief to the form, and we’ll reply with a formal, itemised quote.

I’m interested in