feat(core,api): an organisation owns hardware, and a domain says who belongs

Two kinds of machine were modelled as one. A host somebody brings is theirs,
reached through a team, and should die with their account. A host bought to
serve other people's workloads is none of those things — and there was nowhere
to put it, so it had to be registered under an employee's personal team, where
it was that person's property and their account going away took it with them.

Ownership becomes an either/or. A machine names a team or an organisation,
exactly one, enforced by a check constraint rather than by convention: both
null is a host nothing can bill, and both set is two answers to "whose is
this?" where whichever join a query happens to take decides who pays. Hardware
an organisation owns has no team and no person at all, which is the point.

The organisation is deliberately not a billing subject and has no plan columns.
It says who owns the metal; a team pays for what it uses either way.

Membership is derived from a verified email domain rather than stored. An
address is already the root identity, so a second record of who belongs where
is a second answer that can disagree with the first — and deriving it means
signing in with a personal address still gets an ordinary personal account,
which is what lets one person hold a company account and use the consumer
product. Nothing is granted on an unverified domain or an unverified address:
either one is a string somebody typed.

Entitlement on fleet hardware refuses everyone for now, with a reason that says
so. What grants a run on metered hardware is a plan, and there is nothing to
ask yet, so it fails closed rather than giving the expensive case away. The
branch is written out so the plan check has one obvious place to land.

Routes are read-only, and nothing seeds an organisation. Creating one grants
membership to everyone who can receive mail at a domain, so it is an operator
action against the database — a migration that inserted one would insert it
into every deployment, including ones we have nothing to do with. See
docs/deploy.md.
This commit is contained in:
Wanjohi
2026-09-18 23:16:07 +03:00
parent 4a2a4412c2
commit 15631f5d25
20 changed files with 3889 additions and 65 deletions

View File

@@ -112,6 +112,45 @@ binding names a script that has to exist. The custom domains in the config are
what create the DNS records — there is no separate step, and no separate tool
holding the other half of that fact.
## Organisations, and why none are created for you
An organisation owns hardware outright — a host that serves other people's
workloads rather than its registrant's — and gathers the teams whose members
sign in with its email domain. Membership is derived from that domain, so the
`domain_verified` flag is the whole of the access decision: an address on a
verified domain *is* membership, and nothing grants anything on an unverified
one.
**Nothing seeds one, deliberately, and it must stay that way.** A migration
that inserted a row here would insert it into every deployment, including
somebody else's — handing every account on that domain membership of a
deployment its owners have nothing to do with. Seeding business data is what
makes a schema migration a back door.
So it is an operator action, run once against the database, by whoever is
allowed to decide that a domain is really theirs:
```sql
INSERT INTO organisation (id, name, slug, domain, domain_verified)
VALUES (
'org_' || substr(replace(gen_random_uuid()::text, '-', ''), 1, 26),
'Example',
'example',
'example.com',
true
);
```
Two things to get right, because nothing checks them for you. The domain is
lower-cased and has no `@` — it is compared literally against the domain half
of an address. And `domain_verified` should be `true` only for a domain you
control: everyone who can receive mail at it becomes a member the next time
they sign in, with no further step.
Hardware is then registered to it by a member, with `organisationId` instead of
a team on `POST /machine/register`. Such a host has no owner and no team, which
is the point — it outlives the account of whoever ran the command.
## Containers
```sh