Let's talk
engineering

A site is not a type, it is a set of capabilities, and the difference shows up in month two

The first customer had one building. Inside it were a press, a store room and a counter where people walked in and bought things. The second customer had a production unit twenty minutes outside town and two shops in it. The third had a store that supplied both.

If a site has a type column, the first customer cannot be represented. You either create three site records for one building — and then stock that is physically on one shelf lives in three rows and has to be transferred between them to be sold — or you invent a fourth type called “mixed” and special-case it everywhere.

So the site record does not have a type. It has a capability list: an array column with values for production, for holding inventory, and for taking till transactions. One building holds all three. The production unit holds one. The shops hold two. Nothing is special-cased and nothing is duplicated.

Why the array beats the alternatives

The three obvious designs are a type enum, three boolean columns, or a set. They are not equivalent and the differences are practical rather than aesthetic.

A type enum forces the combinations to be enumerated. Three capabilities produce seven useful combinations. You will not enumerate seven; you will enumerate the three you have seen and add a fourth when a customer complains, and the enum will end up with values like “production and store” that are combinations wearing a type’s clothing.

Three booleans work, and they scale badly in the query layer. Every rule that says “this endpoint needs a site that can do X” has to name a column, so adding a fourth capability means a migration plus touching every place that reasons about capability. With a set, adding a capability is adding a value to a type, and the rules are written against values.

A set says the true thing. A site can do these things. It is not a taxonomy, it is an inventory of what is physically there, which is what the business is actually telling you when they describe a location.

The routes then read well. A production endpoint requires the production capability. A till endpoint requires the till capability. Stock listings require the inventory capability. The requirement sits next to the route, in the route file, and someone reading it learns what kind of place the endpoint belongs in.

The bug in the check

The middleware that enforces this takes a list of capabilities and passes when the site has any of them:

const hasCapability = capabilities.some(c => site.capabilities.includes(c));

That is some, not every. A route declaring that it needs both the till capability and the inventory capability accepts a site with either one.

Nobody wrote that as a decision. It is what you write when every caller passes exactly one value, because with one value some and every are the same function and the choice does not present itself. The variadic signature invites a second argument that changes the meaning, and the meaning it changes to is the weaker one.

I read it the wrong way round the first time. Seeing a route that named two capabilities, I assumed it required both, and only when I traced a request that should have failed did the some become visible.

When a check takes a list, the combining rule has to be in the name. Not in the implementation, where it is one word and reads the same either way. requireAnyCapability and requireAllCapabilities are two functions and neither of them can be misread. A single function with a variadic argument and a silent combinator is a defect waiting for its second caller.

The same shape shows up in permission middleware, in feature-flag helpers and in role checks everywhere. Any time you see a helper accepting ...things, the first question is what happens with two, and the answer is usually whichever operator the author typed without thinking about it.

What capabilities do not solve

Two limits, both real.

Capabilities describe what a site can do, not what it may do. The set of things a person may do at a site is a different question with a different answer, and conflating them is how a check about physical reality ends up living in an authorisation layer, where an authorisation shortcut can turn it off. Keep the capability test in domain validation, not in the permission chain.

And a capability list does not tell you where stock physically sits within a site. One building with all three capabilities holds one pool of stock, which is right for a small operation and wrong the moment somebody wants shop-floor stock counted separately from back-store stock. That is a bin or sub-location model and it is a different feature. The capability set correctly stops you from inventing it prematurely, and it will not help you when you need it.

The rule

Model a facility by what it can do, not by what it is. Types are a compression of capabilities, and the compression is lossy exactly where your customers differ from each other.

The practical test: describe your three most awkward customer sites out loud. If any of the descriptions needs the word “and”, a type column is going to cost you a migration.

Working on something like this?

We build this kind of software, and we staff the teams that do.

Get in touch