← Liana Grigory
Insights

The write that has to see the write before it

An atomic batch is all-or-nothing, and security rules evaluate every document in it against the world as it was before the batch. Sign-up needs three writes in order, which means it cannot be atomic, which means the failure modes are yours to design.

By Liana Grigory · 12 September 2026 · 9 min read

Creating an agency on a multi-tenant product is three documents. A tenant. An owner user inside that tenant. A default set of services belonging to it. The obvious implementation is a batched write: three documents, one commit, atomic, no half-created tenants.

It fails, and the way it fails is instructive, because the reason is not a bug in anything. It is two correct mechanisms with incompatible views of time.

What a batch actually promises

A batched write promises atomicity of effect: either all the documents are written or none are. What it does not promise, and what people read into it, is that the documents are written in order and can see each other.

Security rules are evaluated for every write in the batch against the state of the database before the batch. Every document in the batch is judged against the same pre-batch world. So a rule on the services document that says the writer must be the owner of the tenant this belongs to, implemented by reading the user document, reads a user document that does not exist yet — because the write that creates it is in the same batch, invisible to the evaluation.

The batch is rejected in full. Nothing is created. And the error you get back says permission denied, which sends you looking for a mistake in the rule rather than a mistake in the shape of the transaction.

The wrong three fixes

There are three ways out of this that all look reasonable and are all worse than the problem.

Relax the rule. Allow the services write if the user is merely signed in, on the grounds that they are creating their own tenant anyway. This works instantly and quietly removes the tenant boundary from that collection: any authenticated user can now write services into any tenant's namespace. Sign-up is a once-per-account event; the rule is permanent. Trading a permanent property for a one-time convenience is close to the definition of a bad trade, and it is the single most common way multi-tenant isolation gets lost.

Make the rule read the incoming batch. You cannot. There is no expression for "another document in this same write". This is not an omission to be worked around; it is what makes rules cheap and predictable to evaluate.

Move it to a trusted server. A privileged backend bypasses rules entirely and can write all three documents however it likes. This genuinely works, and there are systems where it is right. But it means standing up and paying for a server whose entire job is to be trusted, and every operation that moves behind it stops being governed by the rules and starts being governed by whatever that code happens to do. On a portfolio whose cost model depends on having no server that has to be running, and whose security model depends on the rules being the whole story, this is a large structural concession to buy one convenience.

The shape that works

Three sequential commits, in dependency order, each awaited before the next begins.

  1. The tenant. Its rule checks only that the caller is signed in and that the document names them as owner. Nothing else has to exist.
  2. The owner user. Its rule reads the tenant document, which now exists, and checks that the caller is its owner.
  3. The services. Their rule reads the user document, which now exists, and checks role and tenant.

Each write is checked against a world in which its prerequisite is already true. Every rule stays as tight as it would be for any ordinary operation later in the account's life. Nothing is widened for sign-up.

What you give up is atomicity, and you give it up honestly rather than by accident. That is the trade, and it is the right one, but only if you then do the work that atomicity was doing for you.

Now you own the failure modes

There are two partial states, and they are not equally bad.

Tenant created, user not. A tenant with an owner field pointing at somebody who has no membership document. The account appears broken to the person who just signed up: they are authenticated, and they belong to nothing.

Tenant and user created, services not. A functioning account with an empty catalogue. Mildly wrong, entirely recoverable, and the user may not even notice.

The ordering above is chosen so that the more recoverable failure is the later one. Put the services first and a failure leaves orphaned data with no owner, which is worse in every dimension including cleanup. When you cannot have atomicity, order the steps so that the most common failure leaves the most repairable state.

Then handle them, in roughly this order of usefulness:

  • Make each step idempotent. Deterministic document identifiers rather than generated ones, so a retry writes the same document rather than a second one. This alone converts most partial failures into a non-event, because the fix is to run it again.
  • Retry from the client, on resume. Sign-up is interactive: the user is present, and their next action is a natural retry point. A check at load that repairs its own prerequisites turns "tenant without user" into a state that lasts until the next page view.
  • Make step three lazy. Default data does not have to be written at sign-up at all. Create it on first use, and the third failure mode stops existing.
  • Detect the rest. A tenant whose owner has no user document is a query you can run, and it is the sort of thing that should be visible somewhere you actually look rather than discovered through a support message.

Retry is doing most of the work here, and it only works because of the first bullet. An idempotent step can be retried by anyone, any number of times, including by a user who refreshes out of frustration. Idempotency is what makes "just try again" a design rather than a hope.

Client-side, with the user's own permissions

One more property of this shape is worth drawing out, because it is the reason to prefer it beyond the security argument.

Three sequential writes from the client, each authorised by the rules, need no server. The work happens on the user's device, with the user's own token, at the moment they act. Nothing has to be running when nobody is signing up. The cost of the feature is three writes per account created, which is a number you can put in the commit message and defend.

The batched version and the trusted-server version both cost more than that, in different currencies. And the client-side version has the property that the rules are the complete description of what is permitted — there is no second authorisation model living in server code that can drift away from them.

The generalisation

This is not really about sign-up. It is about a class of operation that appears whenever authorisation is derived from data that the operation itself is creating.

Any time a rule for document B reads document A, writing A and B together is impossible, and the operation has to be a sequence. Adding a member to an organisation. Accepting an invitation. Transferring ownership. Creating a workspace with anything in it. Every one of them has the same shape and the same temptation to collapse into a single call by widening a rule.

The habit worth building is to look at a multi-document write and ask: does any rule here read a document another write in this set is creating? If yes, it is a sequence, and the only remaining decisions are the order and what happens when it stops halfway.

And the rule that must not be broken while making those decisions: the intermediate states are your problem to design, not the rules' problem to accommodate. A rule that was loosened to make a sequence work is a permanent hole opened to save a few lines of retry logic.

Written by Liana Grigory, Entrepreneur and Software Engineer, from work on The Care Royal, Tegula Stone and Unified Savers. Everything above describes decisions actually made on those systems, including the ones that turned out to be wrong.

Home · Terms of Use · Privacy Notice