Engineering

Two people editing the same row

Two people open the same booking record at 9:14. One moves it to Thursday, the other moves it to Friday, and whoever saves second wins. There is no error and no prompt. The first edit is simply gone, and nobody finds out until the customer calls. This is the oldest problem in multi-user software, and what separates a database from a spreadsheet backend is whether the second writer ever gets told.

The write that always succeeds

Airtable's update endpoint takes a records array, each entry carrying an id and a fields object. The interesting part of the request body spec is what is missing from it. You can set performUpsert or typecast. You cannot say "apply this only if the record still looks the way it did when I read it." The write lands on whatever the row currently holds, and the API returns 200.

PATCH does soften the damage. Airtable's documentation is explicit that a PATCH will only update the fields included in the request and that fields left out stay unchanged, while a PUT performs a destructive update and clears every unincluded cell. Two people editing different columns of the same row will not clobber each other. Two people editing the same column will, quietly, and both of them walk away believing they saved.

Upsert gets closer to a constraint without ever becoming one. With performUpsert set, Airtable matches on up to three fields you nominate as fieldsToMergeOn. Zero matches creates a record, one match updates it, and multiple matches fail the whole request. That failure is genuinely useful. It is also enforcement applied by the API at write time rather than by the table itself, so nothing stops a duplicate from arriving through the UI five minutes later.

What Postgres gives you before you write any code

Read Committed is the default isolation level, and its behaviour under a collision is written down rather than left to chance. The manual says that when a target row has already been updated by another in-flight transaction, the second updater waits for the first to commit or roll back. If the first commits, the second re-evaluates its WHERE clause against the new version of the row and proceeds only if that version still matches. A wait and a recheck, instead of an overwrite nobody logged.

Ask for stronger and you get stronger. Under Repeatable Read, a transaction that tries to modify a row changed since it began is rolled back with the error "could not serialize access due to concurrent update", and the application retries the whole thing. Serializable keeps that behaviour and adds monitoring for anomalies across concurrent transactions, implemented as Serializable Snapshot Isolation. If you would rather be explicit, SELECT FOR UPDATE takes a row-level lock and the second reader blocks until the first commits.

Optimistic locking sits on top of all that in about four lines. Add a version integer, write UPDATE bookings SET ..., version = version + 1 WHERE id = $1 AND version = $2, then check the affected row count. Zero rows means somebody beat you, and now you have a real event you can log and surface to the user. When we moved Stay World Class off Webflow and Xano onto Next.js, NestJS and Supabase, the four-stage ETL mapped every legacy ID to a PostgreSQL UUID so that writes arriving during the cutover could be reconciled rather than guessed at. Lighthouse went from 55.91 to 91 out of 100 and LCP came down 77 percent, but the part the team noticed in week one was that conflicting writes started producing errors instead of mysteries.

Transactions that cover part of the work

Xano does have transactions, and they do roughly what the name promises. Its documentation describes wrapping a set of functions so that every one must succeed for any of them to be executed, with the money transfer example you would expect: if the deposit fails, the withdrawal rolls back too.

The boundary shows up a paragraph later in those same docs. Only database operations are considered when deciding whether to roll back, which means other function types such as conditionals and data transformation can still encounter errors without impacting the database transaction. Your rollback protects the writes but not the logic that chose what to write. The docs also note there are no additional settings on a transaction block, so there is no isolation level to pick and nothing to tune once contention shows up.

Retrying is where the Airtable path gets expensive. The API is limited to 5 requests per second per base, and exceeding it returns a 429 that requires waiting 30 seconds before subsequent requests succeed. A retry loop fighting a write conflict is competing for that same 5 per second against your automations and your sync jobs. Thirty seconds is a long time to leave somebody watching a spinner over a date change.

When staying where you are is correct

Most Airtable bases never hit any of this. If one person owns a record and edits it alone, there is no conflict to resolve, and field-scoped PATCH quietly absorbs a surprising amount of real multi-user editing. Six people updating different columns of a project tracker are fine today and will be fine next year. Nothing about that arrangement is waiting to fail.

The honest test is what one lost write costs you. If the answer is a Slack message and thirty seconds of re-typing, paying for a migration to prevent it is a bad trade and the money belongs somewhere else. If the row is a seat somebody paid for or a balance, the cost is a refund and an apology, and at that point you want the retry logic to be something you wrote and can test rather than something you hoped the platform was doing.

There is no deadline attached to this. Nothing breaks on a schedule. The only question worth answering is whether your application can currently tell a write that landed from a write that landed on top of somebody else's, and whether you would find out if it could not.

Sources

Is your platform outgrowing its stack?

Book a call and we'll walk through how we'd approach your platform, with an estimate grounded in a working prototype.