Repository navigation
Conversation
The first persisted layer of the Integrator/Partner -> Client -> Domain hierarchy: - partners (uuidv7; management flag for the platform operator's own org; nullable self-parent reserving a 2-tier channel) and clients (nullable partner_id, NULL = client-owned; payer flag records who is invoiced, partner-pays default). All delete paths RESTRICT - teardown must be orchestrated, never a silent DB cascade. - authority.client_id plus client_id on zone/sys/mod/trigger/edge/broker (nullable; ownership ground truth for the query enforcement that follows). mass_assignment: false everywhere - ownership is not writable via untrusted payloads. - authority.domain gains a real UNIQUE index (self-signup depends on it), preceded by a quarantine step that renames legacy duplicate domains so the index always builds. - dormant partial UNIQUE (client_id, name) indexes on zone/sys/edge/broker, activated per-row as ownership is assigned. - idempotent backfill: management partner, one client-owned Client per authority, estate inference via org_zone resolved per zone TREE (contested or subtree-only claims left NULL for a human), collision-safe against the new unique indexes. Verified: full spec suite green (660 examples; the one error is the known flaky user_spec concurrency test, passes in isolation); backfill exercised against realistic + adversarial fixtures via tasks/PPT-526/backfill-verify.sh (24 assertions incl. duplicate names, contested trees, operator-created partner rows, domain quarantine); ameba clean.
The models foundation of the authorization layer: a Zanzibar-lite `grants` tuple that gives a user a Permissions bitmask over a Partner, Client, or Authority scope. Authorization resolves by walking UP from the touched resource (authority -> its client -> that client's partner) and OR-ing every live grant on the chain, so a single partner-scope grant covers all of that partner's clients, current and future (decided 2026-08-19). Decisions applied: reuse the existing Permissions flags enum (Int32 bitmask, same shape as group_users); partner grants auto-apply to all the partner's clients via the walk-up resolution; scope_id is polymorphic (no FK, inert when its scope is deleted); user_id FK CASCADE. Grant.resolve(user_id, authority) returns effective Permissions; for_user / for_scope back the future admin-console and audit views. The management-partner "support sees all" rule and rest-api enforcement are the enforcement layer's job, not the model's. Verified: grant_spec 11 examples green (authority/client/partner resolution, cross-partner isolation, permission OR, expiry, user-delete cascade); full models suite 671 examples 0 failures; ameba clean. expires_at is TIMESTAMPTZ mapped as a plain Time (no epoch converter).
Steve's multi-tenancy-template calls the customer tenant "Organization"; we align on that name (decided 2026-08-19). The Partner tier above it is unchanged. Since nothing is shipped, the migrations are rewritten in place rather than adding a rename migration. - clients table -> organizations; client_id -> organization_id on authority/zone/sys/mod/trigger/edge/broker; the Client model -> Organization (payer value 'client' -> 'organization'; client_owned? -> self_managed?). - grants scope_type value 'client' -> 'organization'; Grant.resolve walk now authority -> organization -> partner. - migrations, backfill, generator, specs, helper, and backfill-verify.sh all updated; the OAuth/credential `client_id` fields (oauth_strat, tenant credentials JSON) are deliberately left as-is. Verified: full models suite 671 examples, 0 failures (the 1 error is the pre-existing flaky admin_destroy_lock concurrency test); organization/partner/ grant specs 28 green; backfill-verify.sh all assertions pass; ameba clean.
…ons above master Cam's call on 7 Oct 2026: the entity is Organisation everywhere (table organisations, column organisation_id, grant scope organisation). The three migrations move from 20260818/19 ids to 20261007100500000, 20261007100600000 and 20261007100700000 so they sort after 20261001100500000, the newest migration on master.
…and last-admin guard organisations.partner_staff marks a partner's own staff organisation, whose admins and support users reach every organisation under that partner. Zone, system, edge and broker names are unique within an organisation rather than across the cluster, and the last-admin guard counts admins within the organisation of the user being removed.
This was referenced Oct 7, 2026
Draft
A per-organisation guard blocks deleting a customer's whole domain, since the organisation's last admin goes with it. The organisation-level rule belongs in the API, where the caller's reach is known.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Data layer for multi-tenancy (PPT-526). Records who owns what; enforcement comes separately in rest-api (PPT-2701).
What this adds
Two tables above Domain and one ownership column below it.
partnersandorganisations. A Partner is whoever sells to and manages a customer (NTT), or the PlaceOS management partner. An Organisation is the customer.organisations.partner_idis nullable for self-managed customers.payerrecords whether the partner or the organisation is billed.organisation_idonauthority,zone,sys,mod,trigger,edgeandbroker. It ismass_assignment: falseon every model, so no API payload can reassign ownership. Every FK isON DELETE RESTRICT: deleting a partner or organisation with anything under it fails rather than cascading through an estate.grants: a(user, scope_type, scope_id, permissions, expires_at, granted_by)tuple.Grant.resolve(user, authority)walks authority, organisation, partner and ORs the live grants. Permissions reuse the existingPermissionsflags enum. Nothing reads it yet.UNIQUEindex onauthority.domain. Legacy duplicates are renamed<domain>.duplicate.<id>first, oldest row keeps the name, so the index can never half-apply.(organisation_id, name)for zone, system, edge and broker. They only bite once rows are owned and enforcement flips model uniqueness from global to per organisation.org_zonetree assigning zones, systems, modules and triggers. A tree claimed by more than one organisation is left NULL rather than guessed. Brokers are left NULL on purpose (cluster infrastructure).Added since opening
organisations.partner_staff: a partner's own staff organisation, whose admins and support users reach every organisation under that partner (rest-api#451 reads it).Rollout notes
tasks/PPT-526/rollout/dev-dryrun-2026-10-07.mdin the workspace.20261007100500000,20261007100600000,20261007100700000. If anything later than that merges first, renumber before merging this. micrate autocommits per statement.Verification
isolated-compose run --rm test, fresh database).crystal tool format --checkand ameba clean.tasks/PPT-526/backfill-verify.sh: 24 assertions against realistic and adversarial fixtures (shared trees, duplicate names, dangling references, double run).Design notes and the review history are in the workspace under
tasks/PPT-526/(STAGE2-PLAN.md, STAGE3-DESIGN.md). The plan this belongs to: https://gist.github.com/camreeves/3fabfff92bcdef77a1dcb4b8d8ebfe7b