Federation Hierarchy Management
At a glance
Federation Hierarchy Management is the spine of the platform: it models every petanque body in the world — FIPJP, continental confederations, national federations, districts and clubs — as either a standalone tenant or an OrgNode inside a tenant, and lets each federation configure its own depth, capabilities and cross-tenant interactions.
How it works
Each federation is a Federation document at the global, non-tenant-scoped layer with a type (NATIONAL, CONTINENTAL, WORLD), a lifecycle status (ACTIVE, SUSPENDED, PROVISIONAL, EXPELLED) and an audited recognition_history of FederationRecognitionEvent entries. When a federation goes operational it is linked to a tenant; everything beneath it — districts and clubs — lives inside that tenant as OrgNodes whose levels and capabilities are driven by the tenant's TenantConfig. A Norwegian federation runs flat (depth=0, clubs hang directly under the federation), Sweden uses depth=1 with distrikt as the only intermediate level, France uses depth=2 with ligue→comité→club, and Deutschland models a configurable Bezirk so DPV's currently-flat Landesverbände can subdivide later without a schema change.
District-level capabilities (can_sanction, can_set_license_price, has_discipline_body, can_issue_licenses, has_own_league + sanction_levels) override the tenant default through DistrictCapabilities, resolved as effective = override if override is not None else tenant_default. Cross-tenant traffic never reads sideways into another tenant's database — it only flows over the public API surface: GET /public/federations/directory for discovery, GET /public/federations/map for GeoJSON visualization, POST /public/squads/submit for national→continental/world squad routing with license + nationality validation and SHA-256 IP hashing, and the public clubs/license endpoints. Operators with rights in several tenants are listed via GET /auth/me/tenants and switch contexts through POST /auth/switch-tenant which mints a fresh JWT after assert_user_can_access_tenant authorises the move.
District admins are scoped down with the X-District-Scope header (BFS subtree resolver, capped at five levels). The same machinery also powers standalone clubs (TenantType.CLUB) which can later self-migrate into a federation tenant via the migrate-to-federation flow, and affiliate organisations (TenantType.AFFILIATE) such as Korpen or SPF that operate with hardcoded restrictions.
Key capabilities
- Provision a federation as a standalone tenant with its own branding, statutes, founding date and recognition history.
- Configure OrgNode hierarchy depth and per-level capabilities to match each federation's real-world structure.
- Override district capabilities individually via /districts/{id}/capabilities while keeping tenant defaults as fallback.
- Expose a public, GDPR-friendly cross-tenant surface for license verification, ITC and squad submission.
- Render the global federation directory and a GeoJSON map of geographic boundaries for embedding in any client.
- Switch operator context across multiple federations from a single login through tenant-aware JWT minting.
- Spin up standalone club tenants and migrate them into a parent federation later without losing history.
In practice
It is 09:14 on a Tuesday. A newly elected secretary of a regional ligue in France logs into the admin app. Her JWT carries her home tenant FFPJP and a RoleAssignment scoped to her ligue OrgNode.
The avatar dropdown lists FFPJP first; she stays put. She opens the OrgNode tree, navigates ligue → comité → club, and notices that one of her comités needs to start issuing its own competition sanctions. She opens DistrictCapabilities, flips can_sanction from inherit to true, picks sanction_levels=[regional], and saves.
The PUT writes an audit entry. Five minutes later she submits a national squad for the upcoming European Championship: POST /public/squads/submit routes from FFPJP to CEP, license numbers and nationalities are validated cross-tenant, and she receives reference SQD-FRA-CEP-20260428-1a2b3c4d.
Features in this subsystem
23| ID | Status | Features |
|---|---|---|
| F01.01.01 | Shipped | Standalone tenant per federation (national, continental, world) ✅ PL-F0101a |
| F01.01.02 | Shipped | Tenant creation, configuration, branding ✅ PL-F0101a |
| F01.01.03 | Shipped | Federation profile pages (logo, address, contacts, statutes, history) ✅ PL-F0101a |
| F01.01.04 | Shipped | Federation type classification (continental, national, world) ✅ PL-F0101a |
| F01.01.05 | Shipped | Public API for cross-tenant interactions (license verification, ITC, squad submission) ✅ PL-F0101a |
| F01.01.06 | Shipped | Federation status tracking (active, suspended, provisional, expelled) ✅ PL-F0101a |
| F01.01.07 | Shipped | Federation founding date, affiliation date, recognition history — founding_date, affiliation_date och recognition_history (FederationRecognitionEvent-kronologi) exponerat på admin-CRUD + publik directory ✅ PL-F0101b |
| F01.01.08 | Shipped | Federation directory (public listing of known federations and their API endpoints) — lean GET /public/federations/directory med filter på typ/status/kontinent/with_api_endpoint/fipjp_only ✅ PL-F0101b |
| F01.01.09 | Shipped | Federation map visualization (geographic boundaries) — GET /public/federations/map returnerar GeoJSON Polygon/MultiPolygon + Point-center + map_zoom_default, expelled-federationer exkluderade ✅ PL-F0101b |
| F01.01.10 | Shipped | Federation statistics dashboard (members, clubs, competitions per level) — GET /federations/{id}/statistics aggregerar klubbar/distrikt/medlemmar/tävlingar per nivå, zero-valued för federationer utan tenant-länk (aldrig cross-tenant) ✅ PL-F0101b |
| F01.01.11 | Shipped | Clubs as OrgNodes under their district in the OrgNode hierarchy — Club.district_id kopplar klubbar till distrikt-subtree, verifierat via publik GET /public/clubs ✅ PL-F0101b |
| F01.01.12 | Shipped | District management (OrgNodes with authority — sanction, DM, boards) — DistrictCapabilities overrides layered on tenant OrgNodeTypeConfig defaults, GET/PUT /districts/{id}/capabilities ✅ PL-F0101b / PL-F0101c |
| F01.01.13 | Shipped | Configurable district hierarchy depth per tenant (0 levels: Norway; 1 level: Sweden/Germany; 2+ levels: France ligue→comité) — exposed via GET /org-nodes/hierarchy ✅ PL-F0101c |
| F01.01.14 | Shipped | District-based scope filtering (district admin sees clubs in their OrgNode subtree) — X-District-Scope header / scope_district_id query on /clubs and /districts, BFS subtree resolver ✅ PL-F0101c |
| F01.01.15 | Shipped | Configurable OrgNode capabilities per level — each OrgNode level can be granted: can_sanction (+ sanction_levels), can_set_license_price, has_discipline_body, can_issue_licenses, has_own_league. Exposed via POST /org-nodes/check-capability. ✅ PL-F0101c |
| F01.01.16 | Shipped | OrgNode type registry per tenant — CRUD under /org-nodes/tenant-configs/{id}/types, atomic TenantConfig-validator re-run, localized names via Accept-Language ✅ PL-F0101c |
| F01.01.17 | Shipped | Multi-tenant admin access — operatörer med åtkomst till flera federationer listas via GET /auth/me/tenants (home-tenant först, övriga från RoleAssignment/scope), växling via POST /auth/switch-tenant som mintar ny JWT. UI: avatar-dropdown i admin top-nav (specs/admin/views/tenant-switcher.md). ✅ PL-1902 |
| F01.01.18 | Shipped | Standalone club tenant — TenantType.CLUB enum, self-service POST /public/tenants/club skapar tenant + klubb + admin-användare med magic-link, platt hierarki (0-1 nivåer), migration_source={"created_as":"standalone_club"}. ✅ PL-T001 |
| F01.01.19 | Shipped | Club-to-federation migration — POST /admin/tenants/{id}/migrate-to-federation flyttar Club/License/Match/Membership/Ranking/CalendarEvent till mål-tenant, arkiverar käll-tenant, GET .../migration-preflight ger räknar-rapport + duplikat-detektering. Admin-UI: 3-stegs-wizard i TenantMigration.tsx. ✅ PL-T001 |
| F01.01.20 | Shipped | Affiliate tenant type — TenantType.AFFILIATE för icke-förbundsorganisationer (Korpen, SPF, handisport, skolidrott). €999/år, platt hierarki (depth=0), fritt definierade medlemskategorier, hårdkodade restriktioner (ej licenser, ej rankade tävlingar, ej ITC). POST /admin/tenants/affiliate, CapabilityProfile med hårdkodade+konfigurerbara fält. PL-T302: hybrid signup-flöde — public intake /contact-sales?topic=affiliate → POST /public/contact-sales/affiliate med Turnstile + rate-limit 5/h/IP, sys_support kvalificerar via /customer-base/affiliate-signups innan Stripe Payment Link skickas, webhook driver provisionering via existerande PL-T003-handler. ✅ PL-T003 / PL-T302 |
| F01.01.21 | Shipped | Affiliate bridge link — AffiliateBridgeLink kopplar affiliate-medlem till förbundslicens via opt-in per spelare. POST /affiliate/{id}/bridge verifierar licensnummer mot förbundets publika licens-API. Status: pending→verified→expired/revoked. PL-T302: vid signup med bridge_to_federation=true skickas notifikation + audit-event till nationella federationens sys_engineer post-provisionering. ✅ PL-T003 / PL-T302 |
| F01.01.22 | Shipped | Affiliate consolidated history — GET /affiliate/{id}/members/{user_id}/consolidated-history slår ihop matcher från affiliate- och förbundstenant, sorterade kronologiskt, taggade med source_tenant_id och source_type. Kräver aktiv brygglänk för förbundsdata. ✅ PL-T003 |
| F01.01.23 | Shipped | Multi-discipline club support — Club.disciplines array (min 1 required, default [petanque]). GET/PUT /clubs/{id}/disciplines sub-resource. ?discipline= query param on GET /clubs. Removal blocked by 409 if active competitions/licenses exist. Admin toggle UI. ✅ PL-T013 |
Related subsystems
Stakeholders who need this subsystem
Surfaces in 5 stakeholder analyses