mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
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:
@@ -0,0 +1,57 @@
|
||||
-- Hardware an organisation owns, rather than a person.
|
||||
--
|
||||
-- Two kinds of machine were being 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 until
|
||||
-- now it had to be registered under some employee's personal team -- so the
|
||||
-- company's card was that employee's personal property, and their account
|
||||
-- going away took it. ref(d-0048)
|
||||
--
|
||||
-- So ownership becomes an either/or. `team_id` for a host somebody brought,
|
||||
-- `organisation_id` for one a company owns outright, exactly one of them set,
|
||||
-- and a check constraint rather than a convention -- because 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 would decide who pays.
|
||||
--
|
||||
-- `owner_user_id` becomes nullable so that fleet hardware can have no person
|
||||
-- behind it at all. Its ON DELETE CASCADE is deliberately left alone: it now
|
||||
-- only ever fires for a host somebody brought, where a box dying with its
|
||||
-- owner's account is what that owner expects, and it cannot reach fleet
|
||||
-- hardware because the column it follows is null there.
|
||||
--
|
||||
-- Note the check is safe to apply in one step. Every existing row has a team
|
||||
-- and no organisation, so all of them already satisfy it -- which is only true
|
||||
-- because `team_id` was NOT NULL before this migration relaxed it.
|
||||
--
|
||||
-- `organisation.domain` is what makes someone a member, and membership is
|
||||
-- derived from it 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. `domain_verified` defaults false because an unverified claim
|
||||
-- is a string somebody typed, and nothing may be granted on one.
|
||||
--
|
||||
-- The organisation deliberately has no plan or subscription columns. It says
|
||||
-- who owns the metal, not who owes money; a team pays for what it uses whether
|
||||
-- it sits under an organisation or not.
|
||||
|
||||
CREATE TABLE "organisation" (
|
||||
"id" char(30) PRIMARY KEY NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"name" text NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"domain" text NOT NULL,
|
||||
"domain_verified" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "machine" ALTER COLUMN "owner_user_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "machine" ALTER COLUMN "team_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "machine" ADD COLUMN "organisation_id" char(30);--> statement-breakpoint
|
||||
ALTER TABLE "team" ADD COLUMN "organisation_id" char(30);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "organisation_slug_unique" ON "organisation" USING btree ("slug");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "organisation_domain_unique" ON "organisation" USING btree ("domain");--> statement-breakpoint
|
||||
ALTER TABLE "machine" ADD CONSTRAINT "machine_organisation_id_organisation_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisation"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "team" ADD CONSTRAINT "team_organisation_id_organisation_id_fk" FOREIGN KEY ("organisation_id") REFERENCES "public"."organisation"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "machine_organisation_idx" ON "machine" USING btree ("organisation_id");--> statement-breakpoint
|
||||
CREATE INDEX "team_organisation_idx" ON "team" USING btree ("organisation_id");--> statement-breakpoint
|
||||
ALTER TABLE "machine" ADD CONSTRAINT "machine_one_owner" CHECK (("machine"."team_id" is null) != ("machine"."organisation_id" is null));
|
||||
2989
packages/core/migrations/meta/0015_snapshot.json
Normal file
2989
packages/core/migrations/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,13 @@
|
||||
"when": 1789680491539,
|
||||
"tag": "0014_machine_public_label",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1789762221718,
|
||||
"tag": "0015_organisation_owns_fleet_hardware",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user