# 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);

    -- Team-transitivitet: arv tilgang fra teamet.
    -- Når en bruker melder seg inn i et team, arver brukeren
    -- all tilgang teamet allerede har til andre noder.
    INSERT INTO node_access (subject_id, object_id, access, via_edge)
    SELECT p_subject_id, na.object_id, na.access, na.via_edge
    FROM node_access na
    WHERE na.subject_id = p_root_node_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
