eBadge
Workplace-safety records for Italian construction and industrial firms, and for the external safety consultant who carries several of them at once. A worker gets an NFC and QR badge; an inspector standing on a site can check whether that worker is cleared to be there without an account and without learning who they are. Underneath it is the least glamorous and most consequential kind of software: multi-tenant data that must never leak sideways.
Two hard constraints pull against each other. The first is tenancy: a safety consultant legitimately sees several client companies, some of which compete with each other, so "logged in" says nothing at all about what a request may touch. Every read and every write has to be scoped, including the ones that create a record and the ones that could quietly move a record from one company to another.
The second is that the whole point of the badge is to be readable by a stranger. An inspector arriving on site has no account and never will, but needs to know whether this person is cleared to be here. That answer depends on things like medical fitness expiry, which is exactly the category of data you least want on an unauthenticated page.
> the marketing site is the same compiled bundle as the app, deployed a second time with the landing at the root. cheaper than a separate stack, and it cannot fall out of sync.
Isolation was enforced on reads and only assumed on writes
Reads went through one scoping layer and were correct. Writes went through form validation that narrowed the allowed foreign keys by looking at the user's own company, which works for a company user and silently does nothing for a consultant or a foreman, because neither has one. The result was a cross-tenant write: a consultant could attach a record to another client's worker. The fix made read scope and write scope the same computed thing, shared by both layers, so the two can no longer disagree.
The public badge answers a question without identifying a person
Scanning the badge as a stranger returns a traffic light and nothing else: cleared or not cleared, no name, no company, no photograph, no qualifications. The detail exists behind a PIN printed on the physical badge, so possessing the card is what unlocks the record, and only for that browser session. An inspector gets what they came for, and a lost badge on a train platform reveals that somebody, somewhere, is up to date on their training.
Health data handled by never having any
Fitness for duty is medical information, and the usual answers are encryption or a separate store. This took the cheaper and stronger route: a medical examination is stored as a date it happened, a date it expires, and whether it is blocking. There is no diagnosis field, no clinical note, no judgement text anywhere in the schema, because none was ever added. You cannot leak what the model has no room to hold, and the product loses nothing, since the only question it asks is whether the date has passed.
The deploy pipeline used to wipe the database
The build ran migrations and then a seed step, and the seed step began by deleting the contents of every table. Fine on a laptop, catastrophic once the same pipeline pointed at something real. Two changes: the seeding script now refuses to run against a production environment unless explicitly overridden, and the pipeline was split so shipping code no longer drags a reseed along with it. A guard first, because it stops the bleeding on every path, then the structural fix.
Every badge PIN was in every badge listing
The badge endpoint serialised all fields, which is convenient exactly until you notice the unlock PIN is one of them, returned on every read to anyone inside the tenant. It became write-only, with a separate flag saying whether a PIN is set, and new ones got longer. The compromise worth noting is that shorter legacy PINs stay valid and the verification form still accepts them, because the alternative was invalidating physical cards already printed and in people's wallets.
Registers were being truncated without saying so
The API paginated at twenty rows and the client had no way to ask for more, so attendance registers and dashboards showed the first page and presented it as the whole answer. Not an error, no warning, just a quietly wrong number in a compliance tool. The page size became a parameter with a ceiling, the default stayed put for compatibility, and the client learned to follow pages to the end. Several round trips instead of one, in exchange for totals that are actually totals.
Multi-tenancy is where B2B products quietly go wrong, and the failure is rarely a missing check. It is a check that is correct in one layer and merely implied in another, or a scoping rule written for the common role that degrades to nothing for the unusual one. The habit that helps is making the scope a single computed thing that every layer has to ask for. If you are building something where one customer must never see another's data, that is a review I enjoy doing before it ships rather than after. Let's talk.