Koppel een database aan een schema en Entity Enricher onderhoudt je verrijkte entiteiten als relationele tabellen die je zelf bezit: een echte company-tabel met een revenue-kolom — geen exportbestand. Download eenmalig een kant-en-klare SQL-snapshot en houd je database vervolgens gesynchroniseerd met een incrementele delta-feed van idempotente upserts.
Eén keer opslaan, on-demand projecteren. Verrijkingen schrijven de huidige entiteitsstatus naar de entiteitslaag; elke gekoppelde database is een projectie van die status, geleverd als een eenmalige snapshot plus een delta-feed die je bevestigt. Een schema bewerken kost een herprojectie, nooit een datamigratie.
database_sync: false door); verrijkingen van andere gebruikers worden normaal gerouteerd..sql-snapshot (tabellen + data) en pas deze toe op je PostgreSQL. De DDL bevat join-indexen en foreign keys (child- en link-rijen cascaderen wanneer een entiteit wordt verwijderd). De bestandsheader vertelt je vanaf welke delta-cursor je moet hervatten.Elk entiteitstype heeft een Databasesleutel — de set kolommen die je tabellen gebruiken als unieke index en als conflictdoel voor upserts. Wanneer je een database koppelt, stelt een AI-classificatieronde het volledige databasemodel voor (sleutels, kolomtypen, indexen, eigenaarschap) zodat jij het kunt beoordelen, met een eenvoudige terugvalvolgorde: de semantische ID van het object als het die heeft, anders een Id-achtig veld (id, product_id, …), anders de natuurlijke sleutels van het object. In je tabellen komt een semantische ID binnen als een semantic_id-kolom — de naam id blijft vrij voor je eigen gebruik. Je kunt ze altijd aanpassen op het tabblad “Model” van de database — eenmaal gepubliceerd is dat een wijziging op migratieniveau: download daarna de snapshot opnieuw. Er komt niets in je database terecht totdat het schema vanaf dat tabblad is gepubliceerd (alleen koppelen levert geen tabellen en geen rijen op).
Een database key is nooit nullable. Een null-waarde komt met geen enkele rij overeen, dus in plaats van de entiteit bij te werken zou het bij elke verrijking een nieuw duplicaat invoegen — daarom wordt een verrijking waarvan de key leeg terugkomt afgewezen in plaats van opgeslagen. De editor houdt de twee vlaggen gescheiden, en een schema dat beide instelt wordt geweigerd wanneer je het opslaat of een database koppelt, met vermelding van de twee manieren om het op te lossen: maak de eigenschap altijd aanwezig, of gebruik een andere eigenschap als key voor het type.
Als een databasesleutel een verrijkt tekstveld is in plaats van een semantische ID, is de opgeslagen waarde het antwoord van het model — niet de tekst die je hebt verstuurd. Een bedrijf in je verzoek aanduiden als Embraer legt dat veld niet vast: de verrijking kan Embraer S.A. antwoorden — een gecorrigeerde spelling, een uitgeschreven rechtsvorm, een weggelaten verduidelijking — en dát bepaalt de sleutel van de rij. De rij opzoeken met de waarde die je hebt verstuurd kan dus misgaan (gebruik de entity_keys die bij elke opgeslagen verrijking worden teruggegeven), en een latere run met een andere formulering levert een andere sleutel op — die voegt een tweede rij toe in plaats van de jouwe bij te werken. Dat hoort zo bij elke sleutel die uit verrijkte tekst bestaat. De oplossing is een semantische ID op dat type: varianten van één naam worden herleid tot één stabiele identiteit, zodat de rij blijft bestaan hoe het model het ook spelt. Zet het aan wanneer je het schema genereert — later toevoegen betekent dat je elk object moet aanpassen.
Objecten die in arrays zijn genest, worden hun eigen tabellen met junction-rijen die de volgorde behouden; waardeobjecten zonder identiteit blijven platgeslagen in de kolommen van hun parent. Eigenschapsnamen worden letterlijk (met aanhalingstekens) gebruikt — je kolomnamen zijn de eigenschapsnamen van je schema, alleen ingekort waar een genest pad langer zou worden dan PostgreSQL kan benoemen.
De projectie is deterministisch — hetzelfde schema wordt altijd toegewezen aan dezelfde tabellen en kolommen. Eigenschapsnamen worden letterlijk kolomnamen (met aanhalingstekens); typenamen worden in snake_case omgezet tot tabelnamen (VideoGame → video_game). De enige uitzondering is lengte: PostgreSQL kan een identifier van meer dan 63 bytes niet bevatten, dus een diep genest pad verkort zijn bovenliggende objecten om te passen (morphological_description_ → morpdesc_) — hetzelfde voorvoegsel voor elke kolom van dat object, weergegeven en bewerkbaar op het tabblad Model voordat je koppelt. Een eigenschap kan ook zijn voorvoegsel volledig laten vallen om te passen bij een kolom die je database al heeft (product_identifiers.stock_keeping_unit → sku) — de koppelschakelaar naast de naam op het tabblad Model. Elke tabel heeft een _sync_revision-kolom die wordt gebruikt om replays convergent te houden.
| In je schema | In je database |
|---|---|
| Object met identiteit (semantische ID of sleutels) | Een eigen tabel; database-sleutels worden de unieke index & het upsert-doel |
| Scalair veld (string, getal, boolean) | Een getypeerde kolom (TEXT, BIGINT, NUMERIC, BOOLEAN) |
| Gesloten set (een veld dat beperkt is tot een lijst met waarden) | Een gewone TEXT-kolom — geen CHECK, geen database-enumtype. De lijst wordt afgedwongen wanneer de AI antwoordt, dus een waarde later toevoegen migreert je database nooit. Voeg je eigen constraint toe als je die wilt — de sync raakt die nooit aan |
| Nullable of niet-nullable veld | Een veld dat niet nullable is, wordt standaard een NOT NULL-kolom, samen met de kwaliteitspoort hieronder — bij de strengste poort krijgen verplichte referenties ook NOT NULL-foreign keys; zet de handhaving uit — bij registratie of later: een wijziging na de eerste sync komt als bewaakte migratie in de feed — om elke kolom nullable te houden en de volledigheid alleen door de poort te laten bewaken |
| Meertalig veld | Eén JSONB-kolom met alle talen |
| Ingebed waarde-object (geen identiteit) | Afgevlakt tot kolommen met voorvoegsel (dimensions_width) |
| Array van waarde-objecten | Een onderliggende tabel met sleutel op de ouder, geordend, cascaderend bij verwijderen |
Array van entiteiten / $ref-relatie | Een koppeltabel die bron- en doelrijen verbindt, met behoud van volgorde |
Sleutelveld (identifying) | Een secundaire index voor snelle lookups |
| Query-vormige index (geordende veldenlijst) | Eén index over meerdere kolommen per gedeclareerde vorm, geordend zoals de query van het lijstscherm die hij bedient — eerst facetten en gesloten reeksen, als laatste de sorteer- of bereikkolom, meertalige velden inbegrepen (zo'n vorm wordt één keer uitgeleverd per taal die je database heeft ontvangen); voorgesteld door de classificatiestap met een reden, samengesteld op het tabblad Model, meerdere per entiteit |
| Zoekveld (indexdoel) | Een trigram-index (pg_trgm) op tekst waarin je zoekvelden op fragmenten matchen — per taal en zonder limiet op meertalige kolommen; nooit op een keuzelijstwaarde, die hoort thuis in een index op maat van de query (meertalige inbegrepen — zo'n index wordt één keer uitgeleverd per taal die je database heeft ontvangen); overgeslagen (geen effect) op replica's zonder de extensie, totdat een database-eigenaar die installeert |
| Coördinatenpaar (breedtegraad + lengtegraad) | Eén ruimtelijke index over het paar (native PostgreSQL GiST, geen extensie) — queries voor straal, dichtstbijzijnde buren en kaartweergave |
| Intervalpaar (begin- + eindgrens) | Eén bereikindex over het paar — overlap en "welke waarde gold op deze datum"-queries |
Een database kan meer dan één schema synchroniseren. Entiteitstypen met dezelfde naam in de gekoppelde schema's komen in dezelfde tabel terecht, samengevoegd op hun databasesleutel — de verrijkingen van elk schema werken alleen hun eigen kolommen bij, dus een bedrijf dat door twee schema's wordt verrijkt, wordt één rij met beide kolomsets. Typen die uniek zijn voor een schema voegen simpelweg hun eigen tabellen toe, geleverd via een automatische migratiedelta in de feed — geen nieuwe download nodig.
Wanneer je een schema koppelt, laat een vergelijkingsstap precies zien welke tabellen worden samengevoegd (met hun sleutels en toegevoegde kolommen) en welke nieuw zijn; een schema dat een tabel deelt, neemt de bestaande databasesleutels van die tabel over, die je ter controle te zien krijgt. Als de schema's niets delen, stelt de flow in plaats daarvan een aparte database voor. Het ontkoppelen van een schema raakt je database nooit aan — de gesynchroniseerde tabellen blijven staan.
Als je een schema ontkoppelt of een sync verwijdert, blijven er aan onze kant twee dingen achter: de opgeslagen entiteitstatus waar niets meer naartoe schrijft, en de database-eigenschappen die het schema draagt (database keys, kolomtypen, indexen, eigenaarschap). Beide bevestigingen bieden aan om ze te verwijderen, en alleen voor schema's die helemaal geen database meer hebben — een schema dat elders nog gesynchroniseerd wordt, behoudt alles. Het schema zelf, de verrijkingsrecords en de kosten worden nooit beïnvloed.
Een gekoppeld schema heeft een gepubliceerd contract: de versie die je verrijkingen en je database daadwerkelijk gebruiken. Als je het schema bewerkt, raak je alleen een werkkopie aan — tekstwijzigingen worden automatisch doorgevoerd, terwijl structurele wijzigingen (nieuwe velden, type- of sleutelwijzigingen) wachten tot je op Publiceren drukt. Publiceren toont een voorbeeld van de exacte impact en stuurt de juiste migratie naar de delta-feed: nieuwe kolommen komen aan als ALTER TABLE-delta's, en zwaardere wijzigingen (een nieuwe databasesleutel, een typewijziging) worden uitgevoerd als bewaakte migraties tegen je eigen database — als data ze blokkeert (een ontbrekende of dubbele sleutelwaarde), pauzeert de feed met het exacte probleem en probeert het automatisch opnieuw zodra je het oplost.
Publiceren gebeurt op het tabblad Model van de database (de Workflow Editor toont een banner die daarnaar verwijst zolang het schema gekoppeld is). Voordat je publiceert, toont het beide kanten: het contract waarop je database vandaag draait, en een diff van alles wat je werkkopie zou wijzigen. Niet overtuigd door een bewerking? Terug naar gepubliceerd zet het contract terug — ongedaan te maken, en het concept dat je opzij hebt gezet blijft 24 uur herstelbaar.
Een schema opnieuw koppelen dat is bewerkt terwijl het ontkoppeld was, werkt op dezelfde manier: de synchronisatie onthoudt wat je database al heeft en stuurt alleen het verschil, plus een snapshot-vernieuwing voor de rijen die er tussendoor zijn geschreven. Nooit handmatig DROP-en.
Dezelfde belofte geldt voor onze eigen upgrades. Wanneer een nieuwe release verbetert hoe schema's op tabellen worden gemapt, wordt je sync automatisch gemigreerd — additieve wijzigingen komen vanzelf in de feed terecht. Als een upgrade tabellen zou hervormen die je al hebt, raken we je gegevens nooit onaangekondigd aan: de levering pauzeert en je Database Sync-pagina vraagt je om hem toe te passen, waarbij eerst precies wordt getoond wat er verandert.
Meertalige enrichment is hier ook eersteklas: gelokaliseerde waarden komen binnen als JSONB-kolommen die elke taal bevatten van de enrichment — {"en": "Headache", "fr": "Céphalée"} — zodat één database al je talen tegelijk bedient. Kies een taal direct in je query's (name->>'fr'), en de JSON-deltapayloads dragen dezelfde per-taal-gesleutelde objecten.
Elke database beantwoordt bij registratie één vraag: wat wordt er geschreven als een verrijking terugkomt met hiaten — niet-nullable velden die leeg blijven? De drie antwoorden vormen een ladder. Niets: één hiaat waar dan ook, ook binnen een genest object, en de entiteit wordt afgewezen. De entiteit, zonder haar onvolledige onderliggende objecten (de standaard): de rij van de entiteit zelf moet volledig zijn, maar een gebroken onderliggend object wordt overgeslagen en gerapporteerd in plaats van de hele verrijking te laten sneuvelen. Alles: hiaten belanden als NULL en er wordt niets afgewezen — maar bij de entiteitsstatus wint de laatste schrijfactie, de nieuwste verrijking ís de rij, dus een latere onvolledige run wist wat een eerdere had ingevuld. Precies dat wissen voorkomen de twee strenge treden.
Verrijkingen die de controle niet doorstaan, worden alsnog als record opgeslagen en sturen alsnog de record.created-webhook — met database.saved op false — die precies aangeeft welke verplichte velden ontbraken, zodat onvolledige data nooit stilletjes verdwijnt. Bij elk ontbrekend veld staat ook of een model het als onbekend heeft gedeclareerd of het simpelweg heeft weggelaten: in het eerste geval helpt een sterker model, websearch of een brondocument, in het tweede geval een blik op het schema of de invoer. Alleen de velden van de databasesleutel zijn altijd verplicht: een verrijking zonder sleutelwaarde wordt afgewezen, ongeacht het niveau.
Op de strenge treden spiegelt een selectievakje hetzelfde contract in je eigen database als NOT NULL-kolommen op elk altijd aanwezig veld. Op de strengste trede worden ook de foreign keys van verplichte referenties beperkt — geen enkele toegelaten rij kan ze missen. Bij onvolledige onderliggende objecten overslaan blijven ze bewust nullable: een lijstitem dat zelf een waarde mist, wordt weggelaten, en een gedeelde één-op-één-referentie met een onvolledig doel (het stadion waarvan niemand het openingsjaar kent) wordt losgekoppeld — dat doel wordt niet geschreven en niet bijgewerkt, en de opgeslagen rij verwijst daar naar niets, wat precies in die foreign-keykolommen NULL schrijft. Beide worden gemeld in het antwoord van de verrijking, en hiaten in velden op het hoogste niveau leiden altijd tot afwijzing. Het beleid wijzigen na de eerste sync is nooit verloren werk: het komt als bewaakte migratie in de feed, gevalideerd tegen de rijen die je database al bevat.
Een tweede controle vangt dubbele identiteiten op: wanneer twee items uit dezelfde lijst tot dezelfde databasesleutel leiden — een model dat één id verzint voor twee verschillende bedrijven, of een sleutel die ze niet uit elkaar houdt — kan er maar één rij bestaan, dus wordt de laatste weggeschreven en vallen de eerdere af, volgens dezelfde last-write-wins-regel als overal elders. Elke botsing wordt gemeld met de identificerende waarden van beide items en een oordeel: een duplicaat herhaalde dezelfde waarden en verloor niets; bij een conflicterende afwijzing gingen de genoemde waarden wél verloren — ofwel herhaalde het model één ding met ruis, ofwel zijn dit verschillende dingen en heeft de sleutel een onderscheidende eigenschap nodig (een regio, een jaar, een versie). De lijst blijft op het record bewaard, zodat een gedeeltelijke schrijfactie nog lang na het verdwijnen van het antwoord vertelt wat er verloren is gegaan.
Eén ding dat je moet weten voordat je opnieuw verrijkt: voor een lijst die bij zijn parent hoort, geldt dat de lijst van de nieuwste enrichment de lijst is. Een child-rij die het laatste antwoord niet herhaalt, wordt uit je database verwijderd — zo bereikt een echte verwijdering je, en de response meldt dit niet. Dat is van belang wanneer het een lijst is die het model ophaalt uit het geheugen in plaats van opsomt: vraag twee keer naar de isotopen van een element of de prijzen van een persoon en het tweede antwoord kan korter zijn, waardoor rijen verdwijnen die wél klopten. Houd je eigen historie bij als je de vereniging van elke run nodig hebt.
Verrijkingen komen vanzelf in een gekoppelde database terecht. Al het andere — een resultaat dat de poort weigerde voordat je het schema herstelde, een run die je bewust buiten hield, of uitvoer die je eerst wilt controleren of corrigeren — gaat via records naar de database verzenden: selecteer ze op de pagina Geschiedenis of roep de API aan vanuit een workflow.
De heen-en-terugweg. Verrijk met database sync uitgeschakeld, pas het resultaat aan of keur het goed in je eigen workflow en verstuur het daarna. Wat je verstuurt wordt opnieuw gevalideerd tegen het gepubliceerde contract van het schema en gaat door dezelfde toelatingspoort als een verrijking — een injectie kan nooit schrijven wat een verrijking niet kon.
Twee details om te weten. Een record ongewijzigd versturen slaat het op onder dat record. Een gewijzigde uitvoer versturen maakt een nieuw record dat terugverwijst naar het origineel, want records vormen een audittrail: ze veranderen nooit onder de data die ernaar verwijst, zodat wat je database bevat altijd herleidbaar is tot een record met precies die waarden. En validatie gebruikt het contract zoals het vandaag is — als het schema sinds het record is gemaakt gewijzigd is, meldt de pagina Geschiedenis dat voordat je verstuurt.
De pagina Geschiedenis laat per record ook zien of het de database heeft bereikt: verstuurd, gedeeltelijk verstuurd of geweigerd met reden. Beschikbaar via de webapp, de API, MCP, n8n en Make.
De feed is een strikte FIFO-wachtrij per database: haal een venster op (optioneel geleased, zodat de batch van een gecrashte worker opnieuw wordt geleverd vóór iets nieuwers), pas toe, bevestig. Webhook-meldingen worden gedebounced — elke nieuwe delta reset een stiltetimer, zodat een reeks verrijkingen één keer wordt aangekondigd, een instelbare maximale vertraging begrenst de wachttijd, en een volle ophaalpagina wordt meteen verstuurd. Twee opruimopties bepalen wat Entity Enricher bewaart: geleverde deltakopieën verwijderen bij bevestiging, en — voor dataminimalisatie — de entiteitsstatus zelf verwijderen zodra elke database die aan het schema is gekoppeld deze heeft ontvangen. Beide accepteren een optionele vertraging in dagen: geleverde kopieën blijven dan zo lang na de bevestiging bestaan (een replayvenster), en een geleverde entiteit wordt bewaard tot ze zo lang zonder update is gebleven, waarbij een opruiming per uur verwijdert wat verlopen is. Let op: het opruimen van de status is minimalisatie, geen wissing: verrijkingsrecords blijven bestaan tot je ze verwijdert, en het schakelt het samenvoegen over verrijkingen heen uit voor de opgeruimde entiteiten.
Met een checksum-endpoint per tabel kun je op elk moment controleren of je replica is geconvergeerd, zonder iets opnieuw te downloaden.
Alles zit op één plek in de app — Database Sync, direct onder Geschiedenis in de zijbalk: registreer een database op elk schema (met de beoordeling van de databasesleutel), koppel of ontkoppel schema's, pauzeer de verrijkingsfeed van een gekoppeld schema met de schakelaar (geen nieuwe data of meldingen tot je hem weer inschakelt — schemapublicaties versturen nog steeds hun DDL, en verrijkingen die tijdens de pauze draaien bereiken de replica alleen via een nieuwe snapshot-pull), bewerk de opties, bekijk het webhook-endpoint en toon de ondertekeningssleutel, download de snapshot, blader door de huidige entiteitsstatus, inspecteer de wachtrij met openstaande delta's (alleen-lezen — de cursor van je workflow blijft onaangeroerd), en bekijk een entiteit-relatiediagram van de gegenereerde tabellen met hun sleutels en koppeltabellen. Bij een sync met meerdere databases kan het diagram op één schema focussen: tabellen, kolommen en koppelingen die door de andere schema's worden gevoed, worden grijs — nog steeds zichtbaar op hun plek — zodat je precies ziet wat elk schema bijdraagt aan de gedeelde tabellen.
Wanneer meerdere databases op dezelfde machine terechtkomen, neemt de werkbalkknop Sync-hosts het koppelritueel per database weg: koppel die machine één keer en ga er daarna registraties aan toewijzen. De host claimt ze stuk voor stuk, maakt de fysieke database aan als die nog niet bestaat en start de synchronisatie — zo wordt een database registreren een beslissing die je hier neemt, niet een terminalsessie op de server. Koppelen gebeurt per Entity Enricher-server, zodat één machine meerdere instanties naast elkaar kan bedienen.
Een statement dat je database weigert — meestal door al bestaande duplicaten onder een nieuwe unieke index — blokkeert de wachtrij erachter niet. De batch van die verrijking gaat in quarantaine, de feed blijft doorlopen, en de batch verschijnt op het tabblad Quarantaine met het statement dat je database heeft geweigerd. Verhelp de oorzaak en kies opnieuw injecteren — dat projecteert de entiteit opnieuw vanuit de huidige staat in plaats van een verouderd statement af te spelen — of gooi hem weg.
GET /api/databases//changes?since=…&format=sql en dan POST /api/databases//ack.Gebruik je Supabase? Onze Supabase MCP-vergelijking laat zien hoe de relatie- en sync-regels van EE een productcatalogus beschermen, met JSON en kleine tabeldiagrammen.
Niets in de synchronisatie draait ooit op je databaseserver — elk pad hierboven is een uitgaande consumer die verbinding maakt met welke DSN je ook opgeeft. Azure Database for PostgreSQL, OVHcloud, AWS RDS, Supabase of elke andere beheerde instantie werkt precies als een zelf-gehoste: wijs de consumer naar de cloud-DSN (beheerde providers dwingen meestal TLS af, dus voeg sslmode=require toe) en pas toe.
--dsn naar de beheerde instantie wijst, is alles wat je nodig hebt.delta_available webhook wekt een Azure Function / AWS Lambda / OVHcloud-functie die de REST-feed ophaalt, de SQL uitvoert en bevestigt.Twee regels houden elke zelfgebouwde consumer veilig: voer de statements van elke batch op volgorde uit, binnen één transactie, en bevestig pas na de commit. Delta's zijn idempotent en revisie-beveiligd, dus een crash vóór de bevestiging betekent simpelweg dat de batch opnieuw wordt afgeleverd en dat opnieuw toepassen convergeert.
Databases zijn beschikbaar in betaalde abonnementen (het abonnement bepaalt hoeveel je er kunt registreren). PostgreSQL is het startdialect; elke database geeft zijn eigen dialect op, met MySQL / MariaDB, SQL Server en Oracle in de planning.