Technische documentatie

GraphQL

/api/graphql bevat publieke leesfuncties. /api/graphql/manage bevat beheerfuncties voor rollen, bronnen, invoer en controles.

Publiek
/api/graphql
Beheer
/api/graphql/manage
Publiek contract
2026-10-23
Beheerlogin
Supabase-token of persoonlijke sleutel
Maximale pagina
100 items

Startvolgorde

Gebruik voor een nieuwe frontend deze volgorde. De interactieve schema's zijn het volledige contract; deze pagina beschrijft de samenhang en de belangrijkste werkstromen.

  1. 1Lees authConfiguration via het publieke endpoint en initialiseer Supabase Auth.
  2. 2Log in met een vooraf uitgenodigd e-mailadres en stuur het access-token als Bearer-token.
  3. 3Lees viewer en administrationOverview om de actuele rollen en Dorpspleinen te bepalen.
  4. 4Lees per Dorpsplein managementOrganizations, managementSources en managementReviewQueue.
  5. 5Gebruik de publieke queries news en events uitsluitend voor goedgekeurde frontendinhoud.

Inloggen voor beheer

Gebruik de publieke Supabase-instellingen uit authConfiguration. Alleen vooraf uitgenodigde gebruikers kunnen inloggen. Na de magic link stuur je het tijdelijke access-token als Bearer-token mee. De Supabase-SDK bewaart en vernieuwt de sessie.

De lokale callback moet op dezelfde origin draaien als waar de login begon. http://localhost:3000 en http://127.0.0.1:3000 zijn toegestaan. Een andere origin moet eerst aan de API- en Supabase-allowlist worden toegevoegd.

Voor lokale servercode en scripts kun je op /manage een persoonlijke dpk_...-sleutel maken. Die wordt één keer getoond en gebruikt altijd je actuele rechten. Gebruik zo'n sleutel nooit in een browserbundel of Git.

import { createClient } from "@supabase/supabase-js";

// Deze publieke query heeft nog geen login nodig.
const bootstrap = await fetch("https://dorpsplein.app/api/graphql", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    query: "query { authConfiguration { supabaseUrl publishableKey } }"
  })
}).then(response => response.json());

const { supabaseUrl, publishableKey } = bootstrap.data.authConfiguration;
const supabase = createClient(supabaseUrl, publishableKey, {
  auth: {
    flowType: "pkce",
    persistSession: true,
    autoRefreshToken: true,
    detectSessionInUrl: false
  }
});

// Start op dezelfde lokale origin waarop ook /auth/callback bestaat.
await supabase.auth.signInWithOtp({
  email,
  options: {
    emailRedirectTo: location.origin + "/auth/callback",
    shouldCreateUser: false
  }
});

// Voer dit op /auth/callback uit.
const code = new URL(location.href).searchParams.get("code");
if (!code) throw new Error("De loginlink bevat geen code.");
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (error) throw error;
const { data: { session } } = await supabase.auth.getSession();

const response = await fetch("https://dorpsplein.app/api/graphql/manage", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "authorization": "Bearer " + session.access_token
  },
  body: JSON.stringify({
    query: `query Beheer {
      viewer { email platformRole communities { slug name role } }
      administrationOverview {
        platformRole
        communities { id slug name placeName myRole }
      }
      managementOverview(communitySlug: "marknesse") {
        organizations { id name sourceCount pendingReviewCount }
        sources { id kind displayName state enabled statusMessage }
        reviewQueue { counts { news events links total } }
      }
    }`
  })
});

Rollen en grenzen

Platformrollen en Dorpspleinrollen zijn afzonderlijk. De beheer-API controleert bij iedere aanvraag de rol voor het gekozen Dorpsplein.

PlatformeigenaarKan meerdere Dorpspleinen aanmaken en andere platformeigenaren uitnodigen.
Dorpsplein-eigenaarVolledig beheer binnen toegewezen Dorpspleinen, inclusief lokale rollen.
BeheerderOrganisaties en nieuwe bronnen toevoegen; bronnen, invoer en controles beheren.
ModeratorBestaande bronnen synchroniseren, inhoud invoeren en controles afhandelen.
Publieke bezoekerKan alleen goedgekeurde inhoud via de publieke API lezen.

Beheerfuncties

Beschikbare mutations in /api/graphql/manage:

  • Platform- en Dorpspleineigenaren uitnodigen en rollen toekennen
  • Meerdere zelfstandige Dorpspleinen aanmaken en beheren
  • Organisaties aanmaken en bronnen onderzoeken, toevoegen, pauzeren en synchroniseren
  • Bronstromen selecteren en de centrale controlewachtrij lezen
  • Nieuws, evenementen, herhaalreeksen, samenvoegingen en koppelingen beoordelen
  • Handmatig nieuws, evenementen en herhaalreeksen via dezelfde matchingroute invoeren
  • Authenticeren met een Supabase access-token of persoonlijke API-sleutel

Bron toevoegen en verwerken

probeSource onderzoekt een URL zonder te schrijven. addSource activeert alleen een brontype waarvoor al een connector bestaat. Instagram en Facebook starten doorgaans direct een import; bij een website roep je na activering syncSource aan voor de teruggegeven bron-ID's.

De verwerking is idempotent. Nieuwe of gewijzigde broninhoud doorloopt normalisatie, tekst/OCR, AI-extractie, centrale matching en daarna de controlewachtrij. Een nieuwe connector of websiteadapter kan niet via GraphQL worden gemaakt.

# 1. Onderzoek de URL zonder iets op te slaan.
query BronOnderzoeken($url: String!) {
  probeSource(communitySlug: "marknesse", url: $url) {
    kind title normalizedUrl note
    website {
      publicationLookbackDays
      candidates {
        kind url label canActivate probeError adapterKey
        streams { key label description sampleCount selectable }
      }
    }
  }
}

# 2. Activeer een ondersteunde bron.
mutation BronToevoegen($input: AddSourceInput!) {
  addSource(communitySlug: "marknesse", input: $input) {
    kind organizationId sourceIds ingestionRunIds
  }
}

# 3. Start synchronisatie wanneer addSource geen import startte.
mutation BronSynchroniseren($sourceId: ID!) {
  syncSource(communitySlug: "marknesse", sourceId: $sourceId) {
    sourceId ingestionRunId state
  }
}

Resultaten controleren

Nieuwe pipeline-uitkomsten verschijnen eerst in managementReviewQueue. De publieke API toont uitsluitend goedgekeurde dossiers. Clusters en voorstellen zijn afzonderlijke beslissingen: zij voorkomen dat waarschijnlijk gelijke kandidaten zonder controle als losse dossiers worden gepubliceerd.

Bronstatus, statusMessage, lastSyncedAt, latestRunId en postsImported staan op ManagedSource. Een gedetailleerde stap-voor-stap runlog is nog geen onderdeel van het API-contract.

query CentraleControle {
  managementReviewQueue(communitySlug: "marknesse") {
    counts { news events links total }
    news {
      id title summary publishedAt organizationName sourceName sourceKind
      category { slug name }
      suggestedNewsItem { id title }
      suggestedEvent { id title }
      matchConfidence matchReasonCodes
    }
    events {
      id title description startsOn startsAt endsAt locationName
      organizationName suggestedEventId matchConfidence matchReasonCodes
      source { name kind permalink publishedAt mediaUrls }
    }
    dossiers { id kind targetNewsItemId targetEventId members { kind entityId preferred } }
    proposals {
      id kind confidence reasonCodes actionable blockedReason
      subject { id kind title href }
      target { id kind title href }
    }
  }
}

Wat betekenen de gegevens?

Domeinbegrippen in beide schema's. De GraphQL-verkenners en SDL-schema's bevatten beschrijvingen voor ieder type, veld, argument, invoerveld en iedere enumwaarde.

DorpspleinDe openbare omgeving van een plaats, bijvoorbeeld Marknesse.
BronmomentEén concrete publicatie, zoals een Instagram-post, webpagina of handmatige bijdrage.
NieuwsdossierHet openbare nieuwsbericht. Meerdere bronmomenten kunnen hetzelfde dossier voeden.
EvenementdossierEén activiteit in de agenda, eventueel onderbouwd en bijgewerkt door meerdere bronnen.
RelatieEen nieuwsbericht kan over een evenement gaan; dossiers kunnen ook aan verwante dossiers zijn gekoppeld.

Belangrijkste objecten

Deze objecten vormen het contract met de frontend. De onderliggende databasetabellen zijn bewust geen onderdeel van de API.

CommunityPubliek Dorpsplein met plaatsgegevens, contactinformatie en huisstijlvelden.
NewsItemGoedgekeurd nieuwsdossier. Meerdere NewsSourceMoment-objecten kunnen hetzelfde dossier voeden.
EventGoedgekeurd evenementdossier met datum, tijd, locatie, status, revisie en bronmomenten.
ManagedSourceExterne bron met connectorstatus, synchronisatiegegevens en instelbare deelstromen.
ManagementReviewQueueCentrale controlewachtrij voor kandidaten, reeksen, clusters en relatievoorstellen.
QueuedContentTechnisch resultaat van handmatige invoer met de identifiers van matching en controle.

Herkomst van velden

Niet ieder veld heeft dezelfde herkomst. Gebruik deze indeling bij labels, formulieren en foutafhandeling.

BrondataLetterlijk opgehaald of handmatig ingevoerd, zoals publicatiemoment, bron-URL en brontekst.
RedactioneelDoor een beheerder gekozen of gecorrigeerd, zoals de actuele titel en categorie.
AfgeleidDoor Dorpsplein berekend, zoals PAST, isUpdated, aantallen en matchvoorstellen.
OptioneelKan null zijn wanneer de bron het gegeven niet vermeldt; de frontend hoort daar niet zelf voor te gaan raden.

Eerste aanvraag

Stuur JSON naar het endpoint. Variabelen zijn getypeerd, datum-tijden gebruiken ISO 8601 en evenementdatums JJJJ-MM-DD.

query NieuwsEnAgenda($dorpsplein: String!, $na: Cursor) {
  news(communitySlug: $dorpsplein, first: 20, after: $na) {
    nodes {
      id
      title
      summary
      imageUrl
      canonicalUrl
      sourceLabels
      category { slug name }
      publishedAt
    }
    pageInfo { endCursor hasNextPage }
  }

  events(
    communitySlug: $dorpsplein
    filter: { startsFrom: "2026-10-20" }
    first: 20
  ) {
    nodes {
      id
      title
      startsOn
      startsAt
      endsAt
      locationName
      status
      sourceLabels
    }
  }
}

Alle goedgekeurde nieuwsitems

Gebruik cursorpaginering; maximaal 100 items per aanvraag. totalCount is het totale aantal binnen het filter. Herhaal de query met pageInfo.endCursor totdat hasNextPage false is.

query AlleNieuws($after: Cursor) {
  news(communitySlug: "marknesse", first: 100, after: $after) {
    totalCount
    nodes {
      id title summary imageUrl canonicalUrl sourceLabels newsKind
      publishedAt activityAt isUpdated sourceMomentCount
      category { slug name }
      primarySource { sourceLabel sourceKind externalUrl publishedAt }
    }
    pageInfo { endCursor hasNextPage }
  }
}

# Zolang hasNextPage true is, stuur je endCursor als de volgende waarde van after.

Van browser of server

Publieke leesaanvragen zijn cross-origin toegestaan en gebruiken geen cookies. Controleer zowel de HTTP-status als de GraphQL-lijst errors.

const response = await fetch("https://dorpsplein.app/api/graphql", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "accept": "application/graphql-response+json"
  },
  body: JSON.stringify({
    query: NIEUWS_EN_AGENDA,
    variables: { dorpsplein: "marknesse", na: null }
  })
});

const resultaat = await response.json();
if (!response.ok || resultaat.errors) {
  throw new Error(resultaat.errors?.[0]?.message ?? ("HTTP " + response.status));
}

Wat is beschikbaar?

Publieke queries:

  • Openbare dorpspleinen en publieke contactinformatie
  • Chronologisch nieuws met categorieën en bronvermelding
  • Agenda met datum, tijd, locatie, status en herhaalreeks
  • Nieuws- en evenementdetails met alle actieve bronmomenten
  • Relaties tussen nieuws en evenementen
  • Buurten met contactinformatie en eerstvolgend evenement
  • Filters, zoeken en cursorpaginering
  • Afgeleide status Afgelopen zonder een verzonnen eindtijd

Technische grenzen

Limieten voor 2026-10-23:

  • Maximaal 120 aanvragen per IP per minuut
  • Maximaal 8 selectieniveaus
  • Maximaal 200 velden en 20 aliassen per operatie
  • Eén operatie per HTTP-aanvraag; batching staat uit
  • Cursorpaginering is begrensd op 100 items
  • Alleen gepubliceerde inhoud van openbare dorpspleinen
  • Geen mutations, concepten, controles, gebruikers of pipeline-internals
  • Onverwachte fouten worden afgeschermd en krijgen een request-ID

Compatibiliteit en fouten

Compatibiliteit

Nieuwe optionele velden mogen worden toegevoegd zonder bestaande clients te breken. Een veld wordt eerst als verouderd gemarkeerd. Een brekende wijziging krijgt een nieuwe API-versie.

Foutafhandeling

Invoer- en limietfouten bevatten een stabiele extensions.code. SQL, stacktraces en geheimen worden nooit teruggestuurd. Vermeld bij een probleem de header x-request-id.

CodeBetekenisClientactie
UNAUTHENTICATEDGeen, ongeldig of verlopen Bearer-token.Vernieuw de Supabase-sessie of laat opnieuw inloggen.
FORBIDDENDe gebruiker is bekend maar mist de vereiste rol.Verberg de actie en wijzig alleen via bevoegd rollenbeheer.
BAD_USER_INPUTEen waarde heeft een ongeldig formaat of ontbreekt.Toon de melding bij het betreffende formulier.
NOT_FOUNDHet object bestaat niet of valt buiten het toegestane Dorpsplein.Ververs de lijst; maak geen aannames over bestaan buiten de scope.
CONFLICTDe toestand of revisie is sinds het lezen gewijzigd.Haal het dossier opnieuw op en laat de gebruiker opnieuw beslissen.
INTERACTIVE_SESSION_REQUIREDDeze actie mag niet met een persoonlijke API-sleutel.Gebruik een ingelogde Supabase-browsersessie.
ORIGIN_NOT_ALLOWEDDe browser-origin staat niet op de beheerallowlist.Gebruik een toegestane origin of laat die server-side toevoegen.
RATE_LIMITEDDe aanvraaglimiet voor de huidige minuut is bereikt.Wacht tot x-ratelimit-reset en probeer met back-off opnieuw.
REQUEST_TOO_LARGEURL of requestbody overschrijdt de limiet.Verklein de query, variabelen of aangeleverde tekst.
OPERATION_LIMIT_EXCEEDEDDe GraphQL-operatie is te diep of bevat te veel velden.Splits de operatie in kleinere queries.
INTERNAL_ERROROnverwachte afgeschermde backendfout.Log x-request-id en meld die identifier zonder geheime gegevens.

Een GraphQL-respons kan HTTP 200 hebben en toch een errors-lijst bevatten. Controleer daarom altijd zowel de HTTP-status als errors. De actuele veld-, argument- en enumdocumentatie staat in de Docs-zijbalk van beide GraphQL-verkenners en in de downloadbare SDL-schema's.