teecup/ARCHITECTURE_DECISIONS.md

8.5 KiB

TeeCup — Arkitektur-beslutningslogg (ADR)

Dette dokumentet er den autoritative kilden til hva som er bestemt og hvorfor. Det leses av mennesker og av AI-modeller (Claude, Gemini) i starten av hver økt. Endre aldri en beslutning uten å legge til en ny ADR som erstatter den — historikken skal bevares.

Status: Levende dokument Sist oppdatert: 2026-07-15


Kontekst

TeeCup er en kommersiell (SaaS) webapplikasjon for å opprette, administrere og gjennomføre golfturneringer i «Ryder Cup»-format. Den driftes på samme VPS som teeoff.no, men skal fungere som et isolert økosystem med eget subdomene (teecup.teeoff.no) og egen database (teecup_db).

Målgruppe: klubber, bedrifter og vennegjenger som arrangerer turneringer over tid.


ADR-001 — Tenant = Organisasjon

Beslutning: Isolasjonsenheten («tenant») er en organisasjon, ikke en turnering.

En organisasjon er et bevisst nøytralt begrep som dekker klubb, bedrift og vennegjeng. Ved å ikke kalle den «klubb» i datamodellen unngår vi refaktorering den dagen første bedriftskunde kommer.

Hierarki:

Organisasjon        (tenant — det som isoleres og faktureres)
  ├── Medlemmer/spillere   (gjenbrukbare på tvers av turneringer)
  ├── Egendefinerte baner
  └── Turneringer
        ├── Lag
        └── Matcher
              └── Scores

Begrunnelse: En turnering er en hendelse med start og slutt, ikke en kunde. Gjenbrukbare ting (spillere, baner, historikk) må leve over turneringens levetid. Turnering er derfor en entitet inne i en organisasjon.

Konsekvens: tenant_id i alle skjemaer heter organization_id.


ADR-002 — Bruker og organisasjonsmedlemskap er adskilt

Beslutning: Identitet (innlogging) og medlemskap i en organisasjon er to forskjellige ting. Én user kan ha flere organization_memberships.

Begrunnelse: Samme person spiller ofte både klubbturneringen og jobbturneringen. En bruker må kunne krysse organisasjoner med samme innlogging. Dette er lett å bygge inn fra start og smertefullt å legge til senere.

Konsekvens: Utelukker «database-per-tenant» (se ADR-003), fordi en bruker som krysser organisasjoner da måtte eksistere i flere databaser samtidig.


ADR-003 — Isolasjonsstrategi: Shared schema + Row-Level Security

Beslutning: Én database (teecup_db), delte tabeller, isolert på organization_id-kolonne, håndhevet av PostgreSQL Row-Level Security (RLS).

Vurderte alternativer:

Strategi For Mot Valgt
Shared schema + organization_id + RLS Enkel drift, billig, skalerer til mange org. Krever disiplin; RLS må settes riktig Ja
Database/schema per tenant Sterk isolasjon Tung migrering; bryter med ADR-002 Nei

Begrunnelse: RLS flytter isolasjonen fra applikasjonskoden (der én glemt WHERE organization_id = ... lekker data mellom kunder) ned til databasen, som håndhever den uansett hva koden gjør. Kombinert med ADR-002 er dette det eneste praktiske valget.

Konsekvens / oppgave: Hver økt/tilkobling må sette SET app.current_org (eller tilsvarende) slik at RLS-policyen kan filtrere. Dette må inn i tilkoblingslaget tidlig, ikke ettermonteres.


ADR-004 — Banedata fra teeoff_db via enveis, lesende API-kontrakt

Beslutning: TeeCup henter offisielle banedata fra hovedplattformen gjennom et veldefinert, lesende API — ikke via direkte databasekobling på tvers.

Begrunnelse: En direkte kobling ville låst TeeCup til hovedplattformens skjemaendringer, og et brudd ett sted ville tatt ned begge produktene. teeoff_db er «master» for offisielle banedata; alt brukergenerert innhold (egendefinerte baner, turneringer, brukere) forblir strengt i teecup_db.

Konsekvens: API-kontrakten mot TeeOff må versjoneres og behandles som en ekstern avhengighet, selv om den kjører på samme server.


ADR-005 — Handicap-motoren er et frittstående, testet bibliotek

Beslutning: All handicap- og score-logikk bygges som en ren Python-modul uten avhengigheter til database, API eller web-rammeverk. Den testes grundig i isolasjon.

Begrunnelse: Dette er produktets hjerte og den mest risikofylte biten. Feil her koster mest troverdighet. Ved å isolere den kan reglene enhetstestes mot kjente fasitverdier uavhengig av resten av systemet.

Konsekvens / viktig presisering: Prosentbaserte «allowances» (f.eks. 75 %, 90 %, 3/4) er konfigurasjon, ikke hardkodet logikk. Motoren vet ikke at «fourball = 90 %»; den mottar tildelingen som parameter. De konkrete standardprosentene per format må verifiseres mot gjeldende WHS/lokale regler før produksjon — de skal ikke antas fra hukommelse.


ADR-006 — Teknologistack

Beslutning:

  • Backend: Python + FastAPI (gjenbruker stacken fra TeeOff → enklere vedlikehold).
  • Database: PostgreSQL (+ PostGIS der banegeometri trengs).
  • Frontend: React (Vite) som PWA, offline-first (Service Workers + IndexedDB).

Begrunnelse: Offline-first er kjernefunksjonalitet, ikke luksus — golfbaner har ofte dårlig mobildekning, og score må kunne registreres uten nett og synkes senere.


ADR-007 — Miksede formater via økter, og spiller-pool

Beslutning: En turnering er en ordnet sekvens av økter (sessions), ikke ett enkelt format. Hver økt bærer format, hullomfang og allowance. Ryder Cup = foursome/18 → fourball/18 → singles/18 er tre økter.

Hvert lag har en pool (team_roster) som kan være større enn antall matchplasser. Deltakelse avgjøres per match via match_participant. En reserve er en spiller i poolen uten deltaker-rad i en gitt økt; en spiller kan spille kun enkelte økter (f.eks. bare singelen).

Begrunnelse: Dette er selve Ryder Cup-strukturen. Å modellere format på turneringsnivå ville gjort miksede formater umulig; å modellere deltakelse på turneringsnivå ville gjort reserver og delvis deltakelse umulig.

Konsekvens: Handicap-snapshot fryses per turnering i team_roster (handicap_index_snapshot) for reproduserbare resultater.


ADR-008 — 9-hulls-slag: "slagene som faller på 18-hulls-kortet"

Beslutning: For 9-hulls-økter (front/back) brukes spillerens fulle 18-hulls-tildeling, og slagene fordeles over hele 18-hulls stroke index — deretter tas kun de spilte hullene ut. Spilleren mottar slag på de spilte hullene der SI ≤ mottatte slag.

Vurdert alternativ: WHS' formelle 9-hulls course handicap (eget 9-hulls rating, grovt sagt halvparten). Kan gi et litt annet totaltall. Ikke valgt som standard fordi ikke alle baner publiserer 9-hulls-ratinger, og den valgte metoden er den vanlige i vennegjeng-/klubbmatch-spill.

Kritisk implementasjonsdetalj: Man må IKKE sende bare de ni spilte hullene inn i slagfordelingen med et 18-hulls slagtall — det gir feil ved høye slagtall (12 slag på back-9 blir da 10 i stedet for riktige 6). Motoren har derfor allocate_over_played_holes(...) som fordeler over alle 18 og så tar ut de spilte. Dekket av test test_nine_hole_twelve_strokes_is_six_not_ten.

Konsekvens: tee_rating beholder likevel front/back-omfang, slik at WHS' alternativ kan tilbys senere som konfig per turnering uten skjemaendring.


Åpne spørsmål (ikke besluttet ennå)

Disse må avklares før eller under de relevante fasene:

  1. Sesjons-secret: TeeOff lar PUBLIC_SESSION_SECRET falle tilbake på JWT_SECRET. TeeCup bør bruke separate, uavhengige secrets. (Sikkerhet)
  2. Cache over flere prosesser: In-memory dict-cache på app.state deles ikke mellom flere workers/containere. Ved skalering trengs Redis. (Skalering)
  3. Prising: Per organisasjon (abonnement) eller per turnering? Påvirker ikke isolasjonsmodellen, men påvirker fakturerings-/kvotemodell.
  4. Scramble-grensesnitt: Arkitekturen skal ta høyde for formatet; eksakt UI-løsning spesifiseres senere.
  5. Individuell-vs-delt-ball i hole_score: Håndheves i app-laget, ikke av databasen (CHECK når ikke opp til session.format). Motoren/API-et må passe på at f.eks. et foursome ikke får per-spiller-scorer.

Utviklingsplan (rekkefølge)

  1. Land tenant-modell → Organisasjon (ADR-001/002/003)
  2. Denne beslutningsloggen (dette dokumentet)
  3. Handicap-motor som frittstående, testet bibliotek (ADR-005) — 24 tester, R&A-verifisert
  4. Databaseskjema (001_initial_schema.sql) — RLS, miksede formater, pool
  5. Backend-API + regelmotor-integrasjon (neste)
  6. Frontend (admin + score-registrering)
  7. PWA & offline-synk