From tap to database and back
What happens when someone taps Save, step by step, and the standards that make it reliable.
Meer Habib
Senior Mobile Engineer · Chittagong
Someone taps Save on a note. It feels instant. Underneath, at least nine things happen across four machines, and each one is a place where a well-built app and a flaky one differ. This is the flow I use, and the standards that make it boring in the best way.
1. Show it, before the server agrees
The UI updates immediately: the note appears, the button disables so a second tap can't send a duplicate. This is an optimistic update. The rule that makes it safe: you must be able to undo it when the server says no.
Validate on the device too, but only for speed. Client validation is a courtesy to the user. The server validates again, because anyone can send a request without your app.
2. Write it down before you send it
Before touching the network, put the change in a small local queue with a client-generated ID and an idempotency key. If the phone is offline, goes into a tunnel, or the app is killed mid-request, the change is still there and can be retried later.
The idempotency key is what makes retries safe. If the same request arrives twice, the server recognises the key and returns the first result instead of creating two notes.
3. The request
POST /v1/notes HTTP/2
Authorization: Bearer eyJhbGciOi...
Idempotency-Key: 7c1e2a90-4f3b-4a5e-9d7a-2b1f0c9e8d11
Content-Type: application/json
{ "id": "01J9Z...", "body": "Call the bank", "created_at": "2026-09-26T08:14:03Z" }A few standards live in this snippet:
- HTTPS only. TLS encrypts the request so nobody on the café Wi-Fi can read it.
- A short-lived access token in the header, and a longer-lived refresh token kept in the Keychain or Keystore, never in plain storage.
- Times in UTC, ISO 8601. Format them for humans on the device, never on the server.
- A version in the path. More on why below.
4. Who are you, and are you allowed?
The API does three different checks, in order:
- Authentication: is the token valid, unexpired, signed by us? If not,
401. - Validation: is the body the right shape? If not,
400with field-level errors the app can show. - Authorisation: may this user do this to this row? If not,
403or404.
The third one is where real bugs live. I like to enforce it in the database itself. With Postgres row-level security, a query physically can't return someone else's rows, even if the API code forgets to check.
5 & 6. The database
Everything that must happen together happens in one transaction: insert the note, update a counter, record the idempotency key. Either all of it commits or none of it does.
The database answers with the canonical row: the real ID, server timestamps, defaults. That row, not the optimistic one, is the truth.
7 & 8. Reconcile
The app receives 201 Created, swaps its optimistic note for the server's row, and clears the queue item. If you use TanStack Query or a similar cache, this is where you update or invalidate the affected queries.
When things go wrong, the status code decides what the app does:
| Response | What the app should do |
|---|---|
400 | Show the field errors. Don't retry. |
401 | Refresh the token once, retry once, then sign out. |
403 / 404 | Roll back the optimistic change and tell the user. |
409 | Conflict. Fetch the latest version and let the user decide. |
429 | Wait for Retry-After, then retry. |
5xx / timeout | Retry with exponential backoff and jitter. Safe, because of the idempotency key. |
9. Everyone else finds out
Other devices signed into the same account should see the note without refreshing. The database emits a change, and a realtime channel (a websocket subscription, Supabase Realtime, or a reactive Convex query) delivers it.
Slow side effects, like sending a push notification or an email, don't happen inside the request. They go on a queue after the transaction commits, so a slow email provider never makes Save feel slow.
The mobile-only rule: old versions never die
A website updates for everyone at once. An app doesn't. Someone will open a version from eight months ago, on a train, today. So:
- Never break an API that a shipped version uses. Add fields; don't rename or remove them.
- Version the API so breaking changes get a new path.
- Keep a minimum supported version the app checks on launch, with a friendly "please update" screen for the rare time you truly need it.
The short version
- Optimistic UI, with a rollback.
- Queue it locally with an idempotency key.
- HTTPS, short-lived tokens, UTC timestamps, versioned paths.
- Authenticate, validate, authorise, in that order. Enforce ownership in the database.
- One transaction. The server's row is the truth.
- Retry only what's safe to retry. Side effects go on a queue.
- Assume last year's app is still out there.
Want the bigger picture of the machines involved? Read the twelve boxes behind every app.
Building something like this?
Booking new projects for Q4. Replies within 24h.