The open-source apply client for schema databases. Run it on any machine that can reach your own PostgreSQL, pair once, and it keeps that database converged with your enrichments — bootstrapping from a snapshot, then applying a live delta feed over a single outbound WebSocket. Your connection string never leaves that machine.
Every statement is revision-guarded, so a redelivered batch converges to the same rows. A SQL error rolls the batch back and halts — a poison delta is never silently skipped.
The client pulls state, not operations: each delta carries the full current row(s) for a changed entity as an idempotent INSERT … ON CONFLICT … DO UPDATE, so the target converges even if a batch was missed.
Database syncs can be consumed several ways — n8n, Make.com, MCP, raw webhooks, or the REST delta feed. The sync client is the fully-automated path: the least to build and the least to leak.
No n8n scenario, no cron, no glue code. Pair once and it bootstraps from the snapshot, then applies every delta as it arrives.
The connection string is passed on the command line or stored mode-600 locally — it is never sent to Entity Enricher. The client connects outward only.
Every delta is an idempotent, revision-guarded upsert. If the client dies mid-batch, the batch is redelivered after its lease expires and re-applying converges to the same rows.
A SQL error rolls the batch back and reports the failing delta. The server quarantines that enrichment's whole batch and re-pushes the queue without it, so the client stays connected and keeps applying — one bad row cannot stall everything behind it, and the quarantined work stays listed until you deal with it.
Register a database on a schema first, then pair a client and run it on a machine that can reach your database.
On the Database Sync page, register a database on the schema you want to mirror and review its database keys. See Database Sync for the full model. This step declares the target dialect the client will apply.
Paste this in a terminal. The script verifies a cosign signature before installing.
curl -fsSL https://entityenricher.ai/install-eedatabase.sh | sh
Windows: iwr -useb https://entityenricher.ai/install-eedatabase.ps1 | iex. Or download a signed binary from Releases, or build from source (Go ≥ 1.23): go build -o ee-database .
Source and signed releases live at TOT-Concept/ee-database (MIT).
Run ee-database pair. A browser tab opens at /database/connect with a short code — confirm it, and pick which database this client should sync.
ee-database pair --server https://entityenricher.ai Open this URL in your browser to confirm pairing: https://entityenricher.ai/database/connect?code=7QX-KP2 Code: 7QX-KP2 Waiting for confirmation...
Prefer a token? Issue one on the Database Sync page (Sync client → Pair a client) and pass it directly: ee-database pair --server … <refresh-token>.
On first run the client fetches the .sql snapshot and applies it, then connects and streams deltas. --save-dsn stores the connection string locally so later runs need no arguments.
Each run also self-checks the login's provisioning rights (create database, DDL, DML) and reports the result to the Sync client card, so a missing grant is visible before deltas fail to apply. If no linked schema is published yet, the client stays connected and waits — the first publish starts the feed on its own, no restart needed.
ee-database run --dsn "postgres://user:pass@localhost:5432/mydb" --save-dsn
“Next to” means network-adjacent, not on the database server: any machine or container that can reach the DSN works — including cloud-managed PostgreSQL (Azure, OVHcloud, AWS RDS…), which usually enforces TLS: …/mydb?sslmode=require.
Several databases landing on one machine? A sync host moves the pairing ceremony one level up: pair the machine once, and every database sync you assign to it is claimed, provisioned and kept in sync automatically — registering a new sync never needs another terminal session. Requires client 1.5.0 or later, which pairs once per server rather than once per machine — so one host can serve several Entity Enricher instances side by side.
On the Database Sync page, click the Sync hosts toolbar button and add a host named after the machine. A one-time pairing token appears exactly once, embedded in a copy-paste host pair command with guided setup steps.
Run the command on the machine that can reach your database server. The --dsn is a base connection string naming the server, with no database name — each assigned sync derives its own database from it. Like every DSN it is stored mode-600 locally and never sent to Entity Enricher.
ee-database host pair --server https://entityenricher.ai \ --dsn "postgres://user:pass@host:5432/" <token>
Pairing self-checks the login’s provisioning rights (create database, DDL, DML) and fails fast on a missing grant. Prefer a least-privilege login? Add --admin-dsn and provisioning creates each missing role and database through the admin connection instead — the admin DSN is used at provision time only, never stored.
ee-database host run
The host holds one control-plane WebSocket and reacts to assignments made in the UI: pick the host when registering a database, or later on the database’s Overview tab. Each assigned sync is claimed, its database created if missing (snake_cased from the sync’s name; override per sync via database_names in the host’s config.json), then synced by the ordinary loop below.
A database already paired with another client is reported and skipped — never taken over. Revoking the host in the UI cuts the machine off instantly, including every per-database credential it claimed; assignments and already-synced data stay, so a re-paired host resumes where the old one stopped.
Deltas leave Entity Enricher through a strict per-database FIFO outbox. The server leases the visible window for 120 seconds and pushes it as one batch; the client applies the whole batch in a single transaction and replies ack , which advances the cursor and triggers the next window immediately. A client that dies mid-batch is covered by lease expiry and a server-side re-push — nothing is lost or double-committed.
Bootstrap and steady-state share one code path. Skip the bootstrap with --skip-bootstrap if your database is already seeded.
Each statement carries a _sync_revision so an older row never overwrites a newer one, even out of order.
A SQL error rolls the batch back and reports the failing delta with the whole offending statement. The server quarantines that enrichment's batch and re-pushes the queue without it — the client keeps applying the rest. Only a failure that names no delta exits non-zero.
Every applied window reports the shape it wrote, per table — so sizing a nightly re-enrichment never needs log archaeology on deltas that are already acked and gone.
applying 12 delta(s) (10831 .. 10842) in one transaction applied 12 delta(s) in 84ms — 38 statement(s): mushroom 4 upserts, mushroom_common_names 12 upserts + 4 prunes, mushroom_human_uses 14 upserts + 4 prunes acked up to delta 10842
One upsert is one row, so the counts are row counts; a prune is the single revision-guarded DELETE that clears the child or junction rows a new payload no longer claims. Children are reconciled in place — never wiped and re-inserted. Add --verbose for one line per delta, with its entity type, wall time and its own shape.
The target dialect is fixed by the schema-database registration in Entity Enricher — the client applies whatever SQL the server renders. PostgreSQL is the launch dialect; MySQL / MariaDB, SQL Server and Oracle renderers are planned (the MySQL driver is already bundled). Multi-statement application is handled per driver (pgx simple protocol, MySQL multiStatements).
Assuming the target database already exists, the login needs only CONNECT on the database and USAGE + CREATE on the target schema. (Since PostgreSQL 15, public no longer grants CREATE to everyone by default.)
Everything else follows from ownership: the client creates the replica tables itself, so it owns them, and ownership implies the reads and writes the data deltas need. Ownership is not optional — the feed also ships migration statements (ALTER TABLE …, CREATE INDEX …) that PostgreSQL restricts to the table owner, and no combination of grants substitutes for it.
If the replica tables already exist under a different owner, the run's rights preflight still passes — the login can create new tables — but the first migration delta fails. Transfer them with ALTER TABLE … OWNER TO <login> (or grant the login membership in the owning role) rather than adding grants.
A statement your database refuses — a pre-existing duplicate under a new unique index is the usual cause — does not stop the feed. The batch rolls back, the client reports the failing delta together with the whole offending statement (never truncated), and the server quarantines that enrichment's batch and re-pushes the queue without it. Your client keeps applying everything that follows.
Quarantined work stays listed in the Quarantine tab of the Database Sync page until you deal with it: fix the cause in your database and reinject — which re-projects the entity from its current state rather than replaying the stale statement — or drop it if the row no longer matters.
A failed bootstrap is different: the snapshot is one transaction, so nothing is partially applied, and the client saves it to the pairing's profile directory as snapshot-failed.sql (mode 0600, replaced on each attempt, removed on the next success) so you can inspect or replay it with psql -f.
The client initiates the WebSocket over :443/wss. Your database host never accepts inbound connections — no ports to open, no ingress to configure.
A credential is bound to a single database sync. Pairing again rotates it and instantly evicts the previous live connection.
The 365-day refresh token (stored mode-600) is exchanged for 15-minute access tokens that authenticate the WebSocket. Revoking in the UI disconnects a live client within ~1 second.
A managed host pairs with a short eeh_… key rather than a JWT: the server keeps only its hash, it never expires, and revoking the host in the UI is what ends it.
A per-profile lock stops two processes from running the same pairing at once — otherwise they would evict each other's WebSocket session in a loop.
Run the client as a dedicated role scoped to the synced schema, so a compromised token can touch nothing else — but let that role create the replica tables so it owns them. Ownership, not grants, is what the migration statements require.
| Command | What it does |
|---|---|
| ee-database pair --server URL | Browser-confirmed device-code pairing. Pick which database to sync. |
| ee-database pair --server URL <token> | Pair with a token issued on the Database Sync page (headless-friendly). |
| ee-database run --dsn DSN [--save-dsn] [--skip-bootstrap] | Bootstrap from the snapshot (unless skipped), then connect and apply deltas. |
| ee-database run … --create-missing | Create the target database first when it does not exist, using the DSN's own credentials (postgres needs CREATEDB, mysql the CREATE privilege). |
| ee-database run … --create-missing --admin-dsn DSN | Bootstrap everything the target DSN names via an admin connection: the missing role/user (with the DSN's password) and the database owned by it. The target DSN then needs no create rights; the admin DSN is never stored. |
| ee-database run --all | Sync every paired database concurrently from one process (each needs a saved DSN). |
| ee-database run … --verbose | Log every delta's write shape and wall time, not just the per-window summary. Also accepted by host run. |
| ee-database host pair --server URL --dsn BASE_DSN [--admin-dsn DSN] <token> | Pair this machine once as a managed sync host — the base DSN names your database server (no database name) and never leaves the machine; the token comes from the Sync hosts dialog (the Sync hosts toolbar button on the Database Sync page). One pairing per server: pair against several Entity Enricher servers side by side. |
| ee-database host run [--server URL] | Managed mode: every database sync assigned to this host is claimed, created if missing and kept in sync automatically — no per-database pairing, across every paired server at once (--server restricts to one). A database already paired with another client is reported, never taken over. |
| ee-database host status / host disconnect [--server URL] | Show or forget this machine's host pairings. Revoke server-side from the Sync hosts card. |
| ee-database status | Show pairing state, server URL and the paired databases. |
| ee-database disconnect | Forget one pairing's local credentials. Revoke server-side from the UI. |
| ee-database version | Print version. |
Credentials are stored mode-600, one profile per paired database, under ~/.config/ee-database/profiles/ — pair once per database, and --database NAME selects one when several are paired. Prefer no automation at all? The same feed is plain REST: GET /api/databases//changes then POST /api/databases//ack — see Database Sync.
The client is MIT-licensed and lives in a public repository so anyone can audit exactly what runs against their database.
Source: github.com/TOT-Concept/ee-database
Releases: github.com/TOT-Concept/ee-database/releases — each binary is signed with cosign before publication.
Audit the installer: curl -fsSL https://entityenricher.ai/install-eedatabase.sh | less