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.
- 1Lees authConfiguration via het publieke endpoint en initialiseer Supabase Auth.
- 2Log in met een vooraf uitgenodigd e-mailadres en stuur het access-token als Bearer-token.
- 3Lees viewer en administrationOverview om de actuele rollen en Dorpspleinen te bepalen.
- 4Lees per Dorpsplein managementOrganizations, managementSources en managementReviewQueue.
- 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.
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.
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.
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.
| Code | Betekenis | Clientactie |
|---|---|---|
| UNAUTHENTICATED | Geen, ongeldig of verlopen Bearer-token. | Vernieuw de Supabase-sessie of laat opnieuw inloggen. |
| FORBIDDEN | De gebruiker is bekend maar mist de vereiste rol. | Verberg de actie en wijzig alleen via bevoegd rollenbeheer. |
| BAD_USER_INPUT | Een waarde heeft een ongeldig formaat of ontbreekt. | Toon de melding bij het betreffende formulier. |
| NOT_FOUND | Het object bestaat niet of valt buiten het toegestane Dorpsplein. | Ververs de lijst; maak geen aannames over bestaan buiten de scope. |
| CONFLICT | De toestand of revisie is sinds het lezen gewijzigd. | Haal het dossier opnieuw op en laat de gebruiker opnieuw beslissen. |
| INTERACTIVE_SESSION_REQUIRED | Deze actie mag niet met een persoonlijke API-sleutel. | Gebruik een ingelogde Supabase-browsersessie. |
| ORIGIN_NOT_ALLOWED | De browser-origin staat niet op de beheerallowlist. | Gebruik een toegestane origin of laat die server-side toevoegen. |
| RATE_LIMITED | De aanvraaglimiet voor de huidige minuut is bereikt. | Wacht tot x-ratelimit-reset en probeer met back-off opnieuw. |
| REQUEST_TOO_LARGE | URL of requestbody overschrijdt de limiet. | Verklein de query, variabelen of aangeleverde tekst. |
| OPERATION_LIMIT_EXCEEDED | De GraphQL-operatie is te diep of bevat te veel velden. | Splits de operatie in kleinere queries. |
| INTERNAL_ERROR | Onverwachte 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.