From 4 databases to 1 — consolidating the polyglot stack

Most data-driven teams end up running four databases without ever deciding to. Postgres for the relational system-of-record. MongoDB for the flexible document store. Redis for cache and sessions. Neo4j for the relationship graph. Each was the right tool for one job — and each added a full operational tax: replication, backups, security review, schema migration, monitoring, on-call rotation. They share data by ETL that loses identity, history, and provenance at every hop, and governance becomes a fifth product bolted on top.

RelataDB collapses all four into one binary that speaks every one of those wire protocols. Your Postgres app keeps using psql/psycopg2. Your Mongo app keeps using the official driver. Your Redis calls keep working. Your graph queries still run over Bolt. But they all read and write one governed store — with identity fusion, bi-temporal history, PROV-O provenance, and cell-level ACL built in.

This is the story of how that consolidation actually happens — the before, the after, and the phased path between them. Every capability is shipped today; the maturity table at the end is honest about which migration connectors are live vs. stubbed.

The "before" — a typical 4-database stack

Meet a fictional but realistic team: Sentinel Watch, a mid-market fraud-and-comms analytics SaaS. Their platform ingests call records, transactions, sanctions feeds, and customer profiles, and serves investigators a linked view of "who knows whom, who paid whom." Their stack grew organically:

        ┌───────────────┐   ETL   ┌───────────────┐
 app ─► │   Postgres    │ ──────► │    Neo4j      │ ◄─ graph queries
        │ customers,    │         │ relationships │
        │ transactions  │         │ rebuilt nightly│
        └───────────────┘         └───────────────┘
        ┌───────────────┐   sync  ┌───────────────┐
 app ─► │   MongoDB     │ ◄─────► │    Redis      │ ◄─ cache + sessions
        │ event logs,   │         │               │
        │ flexible docs │         └───────────────┘
        └───────────────┘
                 │
                 ▼  nightly identity-resolution batch
        ┌───────────────┐
        │  identity tool │  (lossy, LLM-guessed, expensive)
        └───────────────┘

The pain they live with:

  • Same entity, four copies. Alice is customer_id=42 in Postgres, {_id: "a91f…"} in Mongo, user:alice in Redis, and a disconnected node in Neo4j. Reconciling them is a nightly batch job that's lossy and never quite current.
  • History is gone. Postgres keeps "latest value wins." Mongo overwrites. When an auditor asks "what did we know on March 15?", the answer is "restore a backup and guess."
  • No provenance. Who put a fact there? When? From which feed? Nobody knows — created_at columns are half-populated.
  • Governance is a separate product. Cell-level access (redact SSN for team X, mask PII for country Y) is enforced in application code, inconsistently, across four read paths.
  • Operational tax ×4. Four replication setups, four backup schedules, four security reviews, four schema-migration pipelines, four on-call pages.

The "after" — one binary, four protocols, one governed store

                                                          ┌── psql / psycopg2 / pgvector (pgwire :5433)
                                                          ├── Mongo driver       (Mongo wire :27017)
        ┌────────────────────────────────────┐            ├── redis-cli / resp   (Redis    :6379)
 app ─► │           RelataDB (1 binary)       │ ◄─ all four├── Neo4j driver / Bolt (Bolt     :7687)
        │                                    │   protocols└── HTTP / gRPC / Flight / MCP / SPARQL
        │  one bi-temporal governed store    │
        │  + identity fusion (deterministic) │
        │  + provenance on every row         │
        │  + Cedar cell-level ACL            │
        └────────────────────────────────────┘

Same app. Same drivers. Same query shapes. Four databases become one fronted by thirteen wire doors — and behind those doors is one store where identity is auto-merged, history is bi-temporal, every fact is provable, and access is enforced in the query path.

What Sentinel Watch stops doing on day one:

  • Stop running the nightly identity-resolution batch — SmartIngest canonicalizes 76 identifier kinds on write and auto-links as records land.
  • Stop rebuilding the Neo4j graph nightly — edges derive from rows (and GraphTrigger makes the graph self-building from typed columns).
  • Stop hand-cleaning +44 7700… vs 07700… — deterministic phone/IBAN/email/crypto canonicalization, byte-identical every run.
  • Stop bolting governance on each read path — Cedar ABAC fires once, in the planner, on every door.
  • Stop operating four databases — one binary, one backup, one audit chain, one on-call.

What you keep unchanged

  • Drivers & ORMspsycopg2, the official Mongo driver, redis-py, the Neo4j driver, boto3, psql, mongosh, redis-cli, cypher-shell, DBeaver, TablePlus, LangChain PGVector… all work unchanged. Repoint host/port and use the bearer token as the password.
  • Query shape — your SQL keeps being SQL. Your Cypher keeps being Cypher (MATCH auto-routes). Your Mongo queries keep working (within the supported subset).
  • App code — zero rewrite. The doors are wire-compatible servers.

A worked migration — Sentinel Watch, phased

Phase 1 — stand up Relata in parallel (week 1)

Run Relata alongside the existing four DBs. No app changes. Enable the doors you need:

RELATA_BEARER_TOKEN=<token> \
RELATA_PG_ENABLE=true      RELATA_PG_BIND=0.0.0.0 \
RELATA_MONGO_ENABLE=true   RELATA_MONGO_BIND=0.0.0.0 \
RELATA_REDIS_ENABLE=true   RELATA_REDIS_BIND=0.0.0.0 \
RELATA_BOLT_ENABLE=true    RELATA_BOLT_BIND=0.0.0.0 \
RELATA_PROFILE=server relata serve

Point one read-only copy of each app at Relata and confirm queries return correct shape. No data has moved yet — this is a smoke test that your clients are wire-compatible. See Compatibility & Doors for the per-protocol quickstarts.

Phase 2 — migrate the historical data (weeks 2–3)

Migrate data out of the old DBs into Relata's governed store. Honest maturity:

SourceMigration pathStatus
Postgresrelata import --from postgres --dsn "postgresql://..." --table <T> --type <Type> — server-side cursor, streaming pages, type-faithfulLive
CSV / NDJSONrelata import --from csv --file <path> (or --from ndjson)Live
MongoDBExport to NDJSON (mongoexport) → relata import --from ndjson; the live door handles ongoing traffic meanwhile🟡 Import stub documented; use the export workaround
Neo4jExport nodes/edges to CSV/NDJSON → relata import; the live Bolt door handles ongoing traffic🟡 Import stub documented; use the export workaround
RedisRe-hydrate from your snapshot by writing through the Redis door (keys land as governed KvEntry rows)✅ Door path

Important nuance: the doors (Mongo/Bolt/ClickHouse) are production-grade — your app's ongoing reads and writes through them work today. The one-time relata import --from <source> connector for Mongo/Neo4j/ClickHouse is the part that's still an honest stub; until it lands, export-then-import gets the historical rows in. See Connectors & Extensions.

Every migrated row lands through the governed write path (governed_upsert_many) — so SmartIngest identity detection, ACL, tenant-ownership, and audit logging all apply automatically.

Phase 3 — cut the app over, door by door (weeks 3–4)

One door at a time, repoint the app's connection string from the old DB to Relata's door. Roll back is trivial — point back at the old DB. Each cutover is independent:

  • Postgres apppsql -h relata-vip -p 5433 -U relata (password = token)
  • Mongo appmongodb://relata-vip:27017 (auth relata / token)
  • Redis appredis-cli -h relata-vip -p 6379 -a <token>
  • Neo4j appbolt://relata-vip:7687 (auth neo4j / token)

Writes now land in the governed store. Reads over any door see them — the Mongo app's write is visible to the Postgres app's SELECT, the Neo4j app's graph traversal, the S3 door's objects, and the SQL search.

Phase 4 — decommission the old DBs (week 5+)

Once traffic is stable on Relata, retire the old databases one at a time. The operational tax collapses from ×4 to ×1: one backup (object-store-native), one audit chain, one schema model, one on-call rotation.

The payoff — what becomes possible that wasn't before

Consolidation alone is a win. But because all four data shapes now live in one governed store, capabilities that were impossible in the polyglot stack become a query:

  • Cross-source identity fusion — the Mongo event log's user_id resolves to the Postgres customer's email resolves to the Neo4j node. LOOKUP_IDENTITY / RESOLVE_IDENTITY / PATHS_BETWEEN.
  • Bi-temporal "what did we know when"AS OF '<ts>' across types that came from different original DBs.
  • Cross-shape joinsSELECT … FROM MongoDocument JOIN Person ON … — impossible when they lived in separate engines.
  • One audit chain — a forensic query spans what used to be four separate logs.
  • Governance once — Cedar ABAC enforces cell-level access uniformly across every protocol a request came in on.

Honest maturity table

CapabilityStatus
Postgres / pgvector door✅ Production
MongoDB wire door✅ Production (subset: SCRAM-SHA-256, no transactions/change-streams/$push/$pull/$unset)
Redis RESP door✅ Production (no MULTI/EXEC, scripting, cluster commands)
Neo4j HTTP Cypher + Bolt doors✅ Production (Cypher subset; read + governed write)
ClickHouse HTTP / native doors✅ Production (read-only)
S3-compatible door✅ Production (with bi-temporal ?versions; cluster fan-out pending)
relata import --from postgres✅ Live
relata import --from {csv,ndjson}✅ Live
relata import --from {mongo,neo4j,clickhouse}🟡 Honest stubs — use export-then-import today

See Limits & Caveats for the full per-protocol status.

When NOT to do this

  • Your data is clean, public, and low-stakes → a single Postgres is simpler than consolidation.
  • You only need one database shape (pure document, pure graph, pure vector) → a specialty DB is lighter.
  • Your polyglot stack is already paid-down and stable with no governance pain → don't migrate for migration's sake.

Relata earns its keep when the four-DB tax is real and the data is messy, sensitive, connected, and needs proving.

How to start

  1. Try one door against a running Relata in 60 seconds — Compatibility & Doors.
  2. Migrate one Postgres tablerelata import --from postgres --dry-run to preview the mapping.
  3. Cut over one app as a pilot, measure, expand.

See also