Legger til steg 4 i recompute_access: når en bruker melder seg inn i et team (member_of-edge), arver brukeren all tilgang teamet allerede har. Tidligere håndterte funksjonen kun retningen "team får ny tilgang → propager til eksisterende medlemmer" (steg 3). Nå håndteres begge retninger: - Steg 3: Team får tilgang → alle eksisterende medlemmer arver - Steg 4: Ny bruker melder seg inn → arver teamets eksisterende tilgang Testet med scenario: Trond → Podcastteamet → Sidelinja → Episode 42. Trond arver member-tilgang til alle tre noder via team-transitivitet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 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
- 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. - Relasjoner er edges. Vegard → Sidelinja (
owner). Trond → Sidelinja (member). Møtereferat → møte (belongs_to). - Ingen containere. Hva du ser er summen av dine edges.
- Samlings-noder gir struktur — de er vanlige noder som fungerer som gravitasjonspunkt.
- Privat er default — en node uten edges til andre er kun din.
- 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 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:
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:
- Egne noder (
created_by) — instant - Eksplisitt tilgang via matrisen — indeksert lookup
- 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);
-- 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 — "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