store.Venue (internal/store/venue.go:13-24) has no parent and no containment: ID, Name, DistrictID, OrganizationID, Status, Address, Description, BandwidthMbps, CreatedAt, UpdatedAt. The only nesting is INSIDE a venue, Venue -> Floor -> Space -> Asset (internal/store/layout.go:15-40). grep for parent, within, contains or building across hydravenues' Go files returns nothing.
So there is no way to express what is actually true of the FTI Experience Zone: the Nekkerhal is a permanent venue, and the zone is a temporary venue inside it.
Venue is matched by string equality in seven places across four services, and a Space reaches none of them:
| Where | Decides |
|---|---|
hydracluster/pkg/api/handlers_api.go:958,965,968 |
whether a head may use a body at all |
hydraexperiencelibrary/internal/api/handlers.go:716-728 |
whether an experience is visible to a body |
hydraexperiencelibrary/internal/client/hydracluster.go:88 |
node lookup |
hydraneck/pkg/store/store.go:133 |
which router serves the venue |
hydracluster/.../handlers_web.go:1062, server.go:519 |
venue network status |
Make the zone a Space and every FTI head and body carries nekkerhal, so the 14 stands cannot be isolated from the rest of the hall, and the router, the body pairing and the experience catalog all stop distinguishing the zone from the building.
ParentVenueID string yaml:"parent_venue_id,omitempty" on Venue.
nekkerhal: permanent, no dates, owns address, power and uplink.fti-experience-zone: parent_venue_id: nekkerhal, carries starts_on/ends_on, owns stands, panel copy and its own bandwidth need.Everything that matches on venue ID keeps matching exactly, so nothing in the table above changes and nothing in production breaks. The parent earns its keep through inheritance (a child with no address or router uses the building's), nesting in listings (GET /api/v1/districts/{id}/venues returns both flat today), and lifecycle (retire the child, keep the building).
Partners are organizations, and they attach at either level: a VENUE partner or a ZONE partner. Citymesh partners with the Nekkerhal, so a new experience zone in three months inherits them without anyone retyping it, while the event team is replaced wholesale.
This is the test the model must pass: relationships have different anchors and different lifetimes.
| Anchor | Lifetime | |
|---|---|---|
| Partner org | the building, or a zone | survives events, ends with the contract |
| Event team | the zone | expires with the zone |
A zone's effective stakeholder list is its own plus the parent's, with inherited entries read-only from inside the zone: nobody should edit Citymesh's contact from within an event that ends in November.
Worth recording, because it shows the gap is real and already load-bearing. hydraneck models the partnership on the ROUTER, not the venue: Tier ("partner routers (Citymesh-managed) are read-only by us") and Access ("Partner-managed routers (e.g. Citymesh-operated MikroTiks at cloud-seven and rupelmonde) MUST stay on read"), both at hydraneck/internal/cli/config.go:91-104. HasWriteAccess() fails closed at :120-125. So a real permission boundary is governed by a partner named only in a Go comment. The one nearby OrganizationID (:131) is hydraneck's copy of the venue's OWNING org, which is visit-flanders, not Citymesh.
Do NOT move tier/access out of hydraneck. That field protects the service that actually talks to the hardware and its fail-closed default should stay next to what it guards. The venue partner record names the organization and the relationship; hydraneck keeps deciding what may be done to its routers.
Three things that look separate are one concept, a grant on a venue: (subject, venue, role), where the subject is an individual user or an organization.
All three need per-venue rather than per-organization authorization, which does not exist: Pantheon's finest grain is (user_id, organization_slug) with no venue column (pantheon/internal/store/store.go:72-78), and hydravenues cannot authenticate a person at all (every write is behind a single shared token, internal/api/server.go:161-173,201-209). Building three mechanisms would be a mistake. See #763 section 2 and #764.
sameVenue on a hot path, interacting with #663. Experiences should probably NOT traverse: exact match is what makes venue scoping predictable.
Decided: where the grants live, and two approaches ruled out
1. No Pantheon schema change. Pantheon has seven tables: realms, users, organizations, memberships, membership_invitations, platform_links, org_integrations (
internal/store/store.go:50-97). There is no resource, scope or venue concept anywhere; a grep for resource/scope/venue/project across its store and pkg returns only realm and org-integration matches. The only thing it can express is "this user is in this org".Teaching it about venues would be the real mistake. Pantheon serves the whole NimsForest realm, not just Hydra, so Hydra's venues have no business in its schema.
2. Overlapping or throwaway organizations are NOT the mechanism. This was the tempting shortcut and it does not work. Venue access derives from
venue.organization_id, and a venue has exactly ONE (hydravenues/internal/store/venue.go:17). The Nekkerhal needs at least three simultaneous relationships: Citymesh as network partner, Visit Flanders operating there, an FTI team running a zone inside it. One slot, three relationships. No number of extra orgs fixes a cardinality of one.You can half-fake it by making the ZONE its own org, since a zone's slot is free, but the building's slot stays contested and you have created a Pantheon org per event, each with one admin derived from
organizations.admin_email(pantheon/internal/store/membership.go:47-50), abandoned when the event ends. That is using identity as a storage trick for something that is not an identity fact.This also resolves open question 6 in #763's proposal: the 12 to 13 providers do NOT each become an organization.
3. The grants live in hydravenues. The resource owns its own grants; identity stays in Pantheon.
A grant is
(subject, venue, role)where the subject is a user id or an org slug, which is many-to-many, which is what the real world is. hydravenues resolves the person through iamnim and expands an org subject through that person's memberships.This is the split the tree already uses: hydraperforceprovision holds the Perforce admin credential and authorizes against its own domain while iamnim only says who someone is. It is also already the decided approach on #545.
What this means in practice
internal/api/server.go:161-173,201-209) andgrep -rn iamnim --include=*.goover hydravenues returns nothing.Separate, not required for this
memberships.roleexists in the DDL (pantheon/internal/store/store.go:75) and is dead:Grantinserts three columns (membership.go:24-28), the projection selects four (:47-50), andpantheon.Membershiphas no Role field (pkg/pantheon/types.go:23-31). Graded roles INSIDE an org are a field away. Useful, but it is not what per-venue access needs and should not be conflated with it.