HydraIssues

Venues cannot nest, and stakeholders have nowhere to live: model the building, the zone, and partner organizations
open feature Project: hydravenues Reporter: cederik 2 Oct 2026 20:58

Description

The gap

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.

Why the zone cannot just be a Space

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.

Proposed: one nullable field

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).

Stakeholders: owner ruling

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.

Citymesh already exists, as a comment

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.

The unification worth noticing

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.

  • a partner organization on a building or a zone (this issue)
  • the event team on a zone (#763's people.yaml)
  • hospitality staff at the venue they work at (#764)

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.

Open

  • Does a partner's reach cascade to child venues automatically, or is each attachment explicit? The owner's ruling says partners attach as venue OR zone partners, which implies explicit attachment with the zone INHERITING the building's. Confirm that inheritance is read-only at the zone.
  • Does body selection traverse the parent? An FTI head using a body parked elsewhere in the Nekkerhal is plausible and would change sameVenue on a hot path, interacting with #663. Experiences should probably NOT traverse: exact match is what makes venue scoping predictable.

Session Context

Venue
mobile-kit

Comments (1)

api 2 Oct 2026 21:21

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.

Service Answers
Pantheon who exists, which organizations they belong to
hydravenues who may do what AT THIS VENUE

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

  • Citymesh SHOULD become a Pantheon organization. Not as a mechanism, as ordinary use: it is a real company whose engineers should eventually log in. Same for any partner with more than one person.
  • Do not create an org per zone or per provider. With venue grants you name the individual directly. An org earns its keep only when several people should inherit the same access.
  • The prerequisite is unchanged and unavoidable: hydravenues needs a person-subject before any grant can be enforced. Today every write is behind one shared token (internal/api/server.go:161-173,201-209) and grep -rn iamnim --include=*.go over hydravenues returns nothing.

Separate, not required for this

memberships.role exists in the DDL (pantheon/internal/store/store.go:75) and is dead: Grant inserts three columns (membership.go:24-28), the projection selects four (:47-50), and pantheon.Membership has 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.