# Noder er sentrum **Status: Besluttet.** > Alt er noder. En bruker er en node. Et team er en node. Et møte er > en node. Sidelinja er en node. Relasjoner mellom dem er edges. > Tilgangskontroll via materialisert tilgangsmatrise beregnet fra > edge-grafen. ## Beslutningen 1. **Alt er noder.** Brukere, team, prosjekter, innhold, møter — alt er rader i `nodes`-tabellen. En bruker er en node som tilfeldigvis kan logge inn. 2. **Relasjoner er edges.** Vegard → Sidelinja (`owner`). Trond → Sidelinja (`member`). Møtereferat → møte (`belongs_to`). 3. **Ingen containere.** Hva du ser er summen av dine edges. 4. **Samlings-noder gir struktur** — de er vanlige noder som fungerer som gravitasjonspunkt. 5. **Privat er default** — en node uten edges til andre er kun din. 6. **Tilgangskontroll via `node_access`-matrise**, oppdatert ved edge-endring, brukt av RLS ved lesing. ## Visibility Visibility er en egenskap på noden som definerer hva som gjelder for alle *uten* eksplisitt edge. Eksplisitte edges overrider alltid oppover. ```sql CREATE TYPE visibility AS ENUM ('hidden', 'discoverable', 'readable', 'open'); ``` | Nivå | Oppdagbar | Lesbar | Interagerbar | |------|-----------|--------|-------------| | `hidden` | Nei | Nei | Nei — kun via eksplisitt edge | | `discoverable` | Ja (søk/katalog) | Nei | Kontaktforespørsel | | `readable` | Ja | Ja | Nei — krever edge | | `open` | Ja | Ja | Ja | Eksempler: - Spøkelsesbruker: `hidden` — usynlig, kun invitasjon - Katalogbruker: `discoverable` — finnes i søk, profil krever edge - Podcast-episode: `readable` — alle kan lytte, bare teamet kan redigere - Kunnskapsgraf-entitet: `open` — alle kan se og bidra ### Traverseringsregelen **Visibility er en hard grense ved traversering.** Når du følger edges i grafen, kan du bare se noder hvis deres visibility tillater det — eller du har en eksplisitt edge. Eksempel: Episode #42 (`readable`) har en `host`-edge til Peter (`hidden`). Anonym bruker leser episoden, følger `host`-edge → Peter er `hidden` → **stopp, usynlig.** Trond (som har edge til Peter) følger samme edge → **ser Peter.** Ingen transitivitet bryter denne regelen. Uansett hvor mange offentlige noder som har edges til en `hidden` node, forblir den skjult for de uten eksplisitt edge. ## Brukere er noder En brukernode er en node med en kobling til Authentik for autentisering. Alt annet — navn, preferanser, roller, relasjoner — er noden og dens edges. `users`-tabellen krymper til autentisering: ```sql CREATE TABLE auth_identities ( node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, authentik_sub TEXT UNIQUE NOT NULL, -- Authentik subject ID email TEXT UNIQUE NOT NULL ); ``` Brukerens profil, innstillinger og metadata lever på noden (JSONB) eller som edges. Autentiseringstabellen er en tynn bro mellom "denne HTTP-sesjonen" og "denne noden i grafen." ## Aliaser En bruker kan opprette aliaser — separate noder som representerer en offentlig persona eller anonym identitet. ``` Aleks (hidden) ──alias──→ Bjørn (readable) ``` Bjørn er en egen node med eget navn, egen profil, egen visibility. Omverdenen ser bare Bjørn. `alias`-edgen er en **systemedge** — usynlig ved traversering, aldri eksponert utad. Ellers ville "hvem kontrollerer Bjørn?" lekke Aleks' identitet. ### Én flate, kontekst bestemmer identitet Aleks ser *alt* i sin mottaksflate — egne noder og alt som kommer til Bjørn. Han svarer fra sin egen flate. Systemet bestemmer `created_by` basert på konteksten: - NN skrev til Bjørn → Aleks svarer → meldingen stemplet med Bjørn som `created_by`. Automatisk, ingen bytte. - Vegard skrev til Aleks direkte → svaret kommer fra Aleks. **Ingen manuell identitetsbytte.** Aleks trenger aldri å "logge inn som Bjørn" eller bytte modus. Konteksten (hvilken samtale, hvilken kanal, hvem mottakeren kjenner) bestemmer hvilken node som er avsender. ### Identitetsvalg ved tvetydighet Noen ganger kjenner mottakeren *både* personen og aliaset. Vegard kjenner Aleks og vet hvem Bjørn er. Hvis Vegard sender en melding til Bjørn under en episode, og Aleks svarer: - **Default:** svaret kommer fra Bjørn (konteksten er Bjørn). - **Valg:** systemet vet at Vegard har edge til Aleks. Aleks kan velge "svar som Aleks" — da startes en ny, separat samtale mellom Vegard og Aleks. **Alias-grensen brytes aldri automatisk.** Bare med eksplisitt handling fra personen bak aliaset. ### Flere aliaser Ingenting stopper en bruker fra å ha flere aliaser — Bjørn for podcast, et anonymt alias for publisering, seg selv for privat. Hver er en node med en `alias`-edge fra brukernoden. Alle samles i én mottaksflate. ### Alias vs. owner | Edge | Betydning | Eksempel | |------|-----------|----------| | `alias` | Jeg *er* denne noden. Usynlig systemedge. | Aleks → Bjørn | | `owner` | Jeg *forvalter* denne noden. Synlig for medlemmer. | Vegard → Sidelinja | `alias` = identitet. `owner` = ansvar. Aleks *er* Bjørn. Vegard *eier* Sidelinja, men han *er* ikke Sidelinja. ## Team er noder Et team er en node med `member`-edges til brukernoder. Gi teamet tilgang til en samlings-node → alle teammedlemmer arver tilgang via transitiv traversering. Ingen egen team-mekanisme. ``` Vegard (node) ──member──→ Podcastteamet (node) ──member──→ Sidelinja (node) Trond (node) ──member──→ Podcastteamet (node) ``` Trond får tilgang til alt under Sidelinja fordi han er medlem av Podcastteamet som er medlem av Sidelinja. Tilgangsmatrisen beregner dette transitivt. ## Samlings-noder En samlings-node er en vanlig node som fungerer som gravitasjonspunkt. "Sidelinja" er en samlings-node Vegard oppretter og eier. Trond kobles på — direkte eller via et team. Andre inviteres inn. Det kan finnes mange samlings-noder — eller få. Et podcast-prosjekt, en research-samling, en vennegjeng. De er ikke noe spesielt i datamodellen — bare noder med edges til andre noder. En innholdsnode kan ha edge til flere samlings-noder. En faktoid om en gjest er relevant for både podcast-prosjektet og research-samlingen. Ikke kopi — samme node, to edges. ### Felles kontekst En samlings-node bærer kontekst (i JSONB) som arves av tilknyttede noder: - **Pruning-profil** — hvor aggressivt slettes binærdata? - **Tema** — visuelt uttrykk - **AI-konfigurasjon** — prompts, modell, regler - **Default synlighet** — for nye noder opprettet i denne konteksten - **Kapasitet** — ressursgrenser for maskinrommet Konflikter (node med edge til to samlings-noder med ulike pruning-profiler) løses med: mest konservativ vinner, eller eier-edge bestemmer. ## Tilgangsroller | Rolle | Hva den gir | |-------|------------| | `owner` | Full kontroll — slette, endre tilgang, endre innstillinger | | `admin` | Kan invitere/fjerne andre, endre konfigurasjon | | `member` | Kan gi input og motta | | `reader` | Kan kun motta (observatør, lytter) | Roller er en egenskap på edgen, ikke på noden. Vegard har `owner`-edge til Sidelinja og `member`-edge til Research-gruppa. Samme node, ulike roller i ulike kontekster. ## Tilgangsmatrise — spesifikasjon ### Skjema ```sql CREATE TYPE access_level AS ENUM ('reader', 'member', 'admin', 'owner'); CREATE TABLE node_access ( subject_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, object_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, access access_level NOT NULL, via_edge UUID REFERENCES edges(id) ON DELETE CASCADE, PRIMARY KEY (subject_id, object_id) ); CREATE INDEX idx_na_subject ON node_access (subject_id); ``` `subject_id` er noden som har tilgang (bruker eller team). `object_id` er noden det gis tilgang til. `via_edge` peker på edgen som ga tilgangen — for debugging og deterministisk revokering. ### RLS-policy ```sql CREATE POLICY node_read ON nodes FOR SELECT USING ( created_by = current_node_id() OR id IN ( SELECT object_id FROM node_access WHERE subject_id = current_node_id() ) OR visibility >= 'discoverable' ); ``` `current_node_id()` returnerer brukernodens id fra sesjonen. Tre sjekker i prioritert rekkefølge: 1. Egne noder (`created_by`) — instant 2. Eksplisitt tilgang via matrisen — indeksert lookup 3. Offentlig synlige noder — kolonne-sjekk Merk: `discoverable` gir kun at noden *finnes* i søkeresultater. Innholdet filtreres i applikasjonslaget basert på visibility-nivå. ### Matrise-oppdatering Matrisen oppdateres **når edges endres**, ikke ved lesing. Én transaksjon: edge + matrise-oppdatering. Alltid synkront — ingen vindu med stale tilgang. **Transitiv tilgang:** Når Trond får `member`-edge til Sidelinja, beregnes hans tilgang til alle noder med edge til Sidelinja. ``` Brukernode → samlings-node → innholdsnoder: 2 hopp Brukernode → team → samlings-node → innholdsnoder: 3 hopp Brukernode → team → samlings-node → komm.node → noder: 4 hopp ``` **Beregningsfunksjon:** ```sql CREATE OR REPLACE FUNCTION recompute_access( p_subject_id UUID, -- bruker- eller teamnode p_root_node_id UUID, -- noden det gis tilgang til p_access access_level, p_via_edge UUID ) RETURNS void AS $$ BEGIN -- Direkte tilgang til roten INSERT INTO node_access (subject_id, object_id, access, via_edge) VALUES (p_subject_id, p_root_node_id, p_access, p_via_edge) ON CONFLICT (subject_id, object_id) DO UPDATE SET access = GREATEST(node_access.access, p_access); -- Transitiv: noder som tilhører roten INSERT INTO node_access (subject_id, object_id, access, via_edge) SELECT p_subject_id, e.source_id, p_access, p_via_edge FROM edges e WHERE e.target_id = p_root_node_id AND e.edge_type = 'belongs_to' ON CONFLICT (subject_id, object_id) DO UPDATE SET access = GREATEST(node_access.access, p_access); -- Hvis subject er et team: propager til alle teammedlemmer INSERT INTO node_access (subject_id, object_id, access, via_edge) SELECT e.source_id, na.object_id, na.access, na.via_edge FROM node_access na JOIN edges e ON e.target_id = p_subject_id AND e.edge_type = 'member_of' WHERE na.subject_id = p_subject_id ON CONFLICT (subject_id, object_id) DO UPDATE SET access = GREATEST(node_access.access, EXCLUDED.access); END; $$ LANGUAGE plpgsql; ``` Detaljer (hvilke edge-typer som gir transitiv tilgang, maks dybde) avklares ved implementering. ### Matrisestørrelse Sparse matrise. Typisk bruker med tilgang til 2-3 samlings-noder med noen tusen noder hver: ~10k rader per bruker. Med 50 brukere: ~500k rader. Trivielt for PG. ## Brukeropplevelse Når du logger inn ser du: - **Dine aktive samtaler** — kommunikasjonsnoder med edge til deg - **Dine noder** — alt du har skapt eller er koblet til - **Dine samlings-noder** — grupperer kontekst, filtrering er frivillig - **Din mottaksflate** — det som er relevant for deg nå Å filtrere etter samlings-node ("vis bare podcast-prosjektet") er et filter, ikke en modebytte. ## Forhold til andre retninger - [Rom, ikke forum](rom_ikke_forum.md) — "rommet" er summen av dine edges, ikke en container du går inn i - [Universell input og mottak](universell_input.md) — mottaksflaten er "noder med edge til *meg*", filtrert og vektet - [Maskinrommet](maskinrommet.md) — eier matrise-oppdatering og leser samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig) - [Datalaget](datalaget.md) — matrisen lever i PG, indeksert for RLS-ytelse