synops/docs/retninger/bruker_ikke_workspace.md
vegard 00bf5d27ce Arkitekturbeslutninger: noder er sentrum, edges definerer alt
Grunnleggende arkitekturbeslutninger tatt og dokumentert:

- Alt er noder (brukere, team, innhold, mediefiler, samlings-noder)
- Edges definerer hva en node er (freeform typer, metadata i JSONB)
- Materialisert tilgangsmatrise (node_access) erstatter workspace-RLS
- Visibility (hidden/discoverable/readable/open) på noder
- Aliaser via usynlige system-edges
- Maskinrommet eier all skriving (SpacetimeDB først, PG asynk)
- SpacetimeDB holder hele grafen, PG er persistent backup
- Node- og edge-skjema spesifisert (docs/primitiver/)

Fjernet workspace-konseptet fra hele dokumentasjonen (~40 filer).
Fem retninger besluttet, én åpen (rom, ikke forum).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:29:54 +01:00

11 KiB

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.

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 hiddenstopp, 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:

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

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

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:

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 — "rommet" er summen av dine edges, ikke en container du går inn i
  • Universell input og mottak — mottaksflaten er "noder med edge til meg", filtrert og vektet
  • Maskinrommet — eier matrise-oppdatering og leser samlings-node-edges for kontekst (pruning, kapasitet, AI-konfig)
  • Datalaget — matrisen lever i PG, indeksert for RLS-ytelse