Protocol Compatibility
RelataDB speaks 8 compatibility doors plus 5 native protocols (13 wire surfaces total) from one binary and one governed store. Any existing client library that speaks one of the compat doors works without the Relata SDK.
All compat doors bind to 127.0.0.1 and share one credential: RELATA_BEARER_TOKEN. Doors auto-enable when RELATA_BEARER_TOKEN is set (any tier); an explicit RELATA_<DOOR>_ENABLE=true|false overrides. With no token and no explicit enable, doors stay off by default (an unauthenticated port is never auto-exposed). pgwire refuses to start without a token. On non-free profiles, an explicit =true without a token fails closed, and RELATA_TENANCY_MODE=multi refuses the tenant-less shared-token doors (use pgwire, which carries per-connection org).
Cross-protocol consistency: write via any door and read the same data back over SQL, pgwire, or any other surface. ACL, org isolation, and the audit log apply on every door uniformly. See the cross-door data visibility table below for the exact type mapping.
Protocol matrix
| Door | Port env var (default) | Read | Write | Stored as |
|---|---|---|---|---|
| S3 (AWS / boto3 / rclone / MinIO) | RELATA_S3_PORT (9191) | ✅ GET / LIST / HEAD | ✅ PUT / DELETE | S3Object, S3Bucket |
| Postgres + pgvector | RELATA_PG_PORT (5433) | ✅ SELECT | ✅ INSERT / COPY / UPDATE / DELETE | User-defined type |
| ClickHouse HTTP | RELATA_CLICKHOUSE_PORT (8123) | ✅ SELECT | ✅ INSERT FORMAT JSONEachRow | Table name = type |
| ClickHouse native TCP | RELATA_CH_NATIVE_PORT (9000) | ✅ SELECT | — | Read-only |
| Neo4j HTTP Cypher | RELATA_NEO4J_PORT (7474) | ✅ MATCH / RETURN | ✅ CREATE / MERGE | Cypher label = type |
| Neo4j Bolt | RELATA_BOLT_PORT (7687) | ✅ MATCH / RETURN | ✅ CREATE / MERGE | Cypher label = type |
| Redis RESP | RELATA_REDIS_PORT (6379) | ✅ GET / KEYS | ✅ SET / DEL | KvEntry |
| MongoDB wire | RELATA_MONGO_PORT (27017) | ✅ find / count | ✅ insert / update / delete | MongoDocument |
Native Relata protocols (always on):
| Door | Default port | Notes |
|---|---|---|
| HTTP REST | 9090 | /query, /ingest, /search, /memory/*, /mcp, /health, /status, /metrics, /types, /specs, /sparql, /watch/stream |
| gRPC | 50051 | gRPC door |
| Arrow Flight | 8815 | Zero-copy columnar streaming; enable with RELATA_FLIGHT_ENABLE=true |
| MCP | /mcp on HTTP | Model Context Protocol tools |
| SPARQL | /sparql on HTTP | Single Basic Graph Pattern over KnowledgeTriple + optional LIMIT |
Cross-door data visibility
All 13 wire surfaces read and write through one governed ObjectStore. Data written through any door is immediately queryable through any other door — the same rows, the same types, the same governance (ACL, provenance, audit, tenant isolation).
Two type patterns
User-defined types (HTTP, ClickHouse, Neo4j/Bolt, pgwire, Flight): you choose the type name. Ingest via ClickHouse → query via Neo4j → read via pgwire. Same type, same rows.
Door-specific types (S3, Redis, Mongo): the door maps to a fixed internal type. The data is in the store and queryable via SQL from any door — through the internal type name.
Type mapping table
| Write via | Stored as type | Example write | Read via another door |
|---|---|---|---|
HTTP /ingest | User-defined (Person) | POST /ingest?object_type=Person | SELECT * FROM Person via pgwire; MATCH (n:Person) via Neo4j |
| ClickHouse INSERT | Table name = type (Person) | INSERT INTO Person FORMAT JSONEachRow | SELECT * FROM Person via HTTP; MATCH (n:Person) via Bolt |
| Neo4j/Bolt CREATE | Cypher label = type (Person) | CREATE (n:Person {id:'x'}) | SELECT * FROM Person via ClickHouse; COPY Person FROM STDIN via pgwire |
| pgwire COPY/INSERT | User-defined (Person) | COPY Person FROM STDIN CSV | SELECT * FROM Person via HTTP; HYBRID_SEARCH FROM Person |
| Arrow Flight do_put | Table from Arrow schema | FlightClient.do_put("Person", batches) | SELECT * FROM Person via any door |
| Redis SET | KvEntry | SET foo bar | SELECT value FROM KvEntry WHERE _pk = 'foo' via HTTP |
| Redis PUBLISH | PubSubMessage | PUBLISH ch hello | SELECT message FROM PubSubMessage WHERE channel = 'ch' via HTTP |
| Mongo insertOne | MongoDocument | db.users.insertOne({name:'x'}) | SELECT * FROM MongoDocument WHERE collection = 'users' via HTTP |
| S3 PUT | S3Object + S3Bucket | aws s3 cp file s3://cases/key | SELECT key, size FROM S3Object WHERE bucket = 'cases' via HTTP |
Concrete cross-door examples
Ingest via ClickHouse, query via HTTP:
# Write through ClickHouse
curl -X POST http://127.0.0.1:8123/ -H "X-ClickHouse-Key: $TOKEN" \
--data-binary "INSERT INTO Person FORMAT JSONEachRow" \
--data-binary '{"id":"ch1","name":"from-clickhouse","company":"Acme"}'
# Read through HTTP SQL — same row, same type
curl -X POST http://127.0.0.1:9090/query -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT name FROM Person WHERE company = '\''Acme'\''","purpose":"analytics"}'Write via Neo4j Bolt, query via pgwire:
# Write through Bolt
from neo4j import GraphDatabase
d = GraphDatabase.driver("bolt://127.0.0.1:7687", auth=("neo4j", "$TOKEN"))
with d.session() as s:
s.run("CREATE (n:Person {id:'neo1', name:'Alice'})")# Read through psql — same row, same type
psql -h 127.0.0.1 -p 5433 -U relata -c "SELECT * FROM Person WHERE id = 'neo1'"Ingest via HTTP, search via Neo4j graph:
# Ingest Person + CallEvent via HTTP
curl -X POST http://127.0.0.1:9090/ingest?object_type=CallEvent -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/x-ndjson" \
-d '{"id":"c1","caller":"+14155550100","callee":"+14155550101"}'
# Query via HTTP PATHS_BETWEEN (graph operator over the same data)
curl -X POST http://127.0.0.1:9090/query -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sql":"PATHS_BETWEEN('\''+14155550100'\'','\''+14155550101'\'', MAX_HOPS => 3)","purpose":"analytics"}'Write via Redis, read via HTTP SQL:
redis-cli -h 127.0.0.1 -p 6379 -a $TOKEN SET config:timeout "30"SELECT value FROM KvEntry WHERE _pk = 'config:timeout';
-- → "30"Start the doors
export RELATA_BEARER_TOKEN=<your-strong-token>
RELATA_S3_PORT=9191 \
RELATA_PG_PORT=5433 \
RELATA_CLICKHOUSE_PORT=8123 \
RELATA_NEO4J_PORT=7474 \
RELATA_REDIS_PORT=6379 \
RELATA_MONGO_PORT=27017 \
relata serveA single end-to-end smoke test for all six compat doors: scripts/protocol_smoke_test.py.
S3 — boto3 / aws CLI / rclone / MinIO client
Supported operations: ListBuckets, CreateBucket / HeadBucket / DeleteBucket, ListObjectsV2, GetBucketLocation, PutObject / GetObject / DeleteObject / HeadObject, and multipart upload.
import boto3
from botocore.config import Config
s3 = boto3.client(
"s3",
endpoint_url="http://127.0.0.1:9191",
aws_access_key_id="change-me",
aws_secret_access_key="unused",
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
s3.create_bucket(Bucket="cases")
s3.put_object(Bucket="cases", Key="exhibit-1.txt", Body=b"hello world")
body = s3.get_object(Bucket="cases", Key="exhibit-1.txt")["Body"].read()
print(body)When RELATA_S3_SECRET_KEY is set the door requires verified SigV4 and rejects plaintext bearer auth.
Read S3 objects over SQL
SELECT key, size, content_hash FROM S3Object WHERE bucket = 'cases';Limits
- Buckets must be empty to delete.
- Multipart parts are in-memory only (lost on restart).
- ETag is SHA-256.
- Object bodies ≥
RELATA_S3_BLOB_THRESHOLD_MB(default 4 MiB) spill to the content-addressed blob store.
Postgres + pgvector — psql / psycopg2 / LangChain PGVector
psql -h 127.0.0.1 -p 5433 -U relata relata
# password = RELATA_BEARER_TOKENCREATE EXTENSION vector;
CREATE TABLE docs (id text PRIMARY KEY, embedding vector(3));
INSERT INTO docs VALUES ('a', '[1,0,0]'), ('b', '[0.9,0.1,0]');
-- Cosine KNN — auto-routed to Relata's HNSW index
SELECT id FROM docs ORDER BY embedding <=> '[0.9,0.1,0]' LIMIT 2;INSERT / UPDATE / DELETE and ordinary SELECT work. GUI clients (TablePlus, DBeaver, pgAdmin, DataGrip) connect and browse schema via the catalog intercept.
| KNN operator | Metric | Note |
|---|---|---|
<=> | cosine distance | Preferred — ANN index is cosine-only |
<-> | L2 distance | Metric-correct via over-fetch + re-rank |
<#> | negative inner product | Metric-correct via over-fetch + re-rank |
pgwire is fail-closed: refuses to start when
RELATA_BEARER_TOKENis unset.
ClickHouse — HTTP or native TCP
# HTTP
curl -X POST "http://127.0.0.1:8123/?query=SELECT+1" \
-H "X-ClickHouse-Key: change-me"
curl -X POST http://127.0.0.1:8123/ \
-H "X-ClickHouse-Key: change-me" \
--data-binary "SELECT name FROM Person FORMAT JSONEachRow"from clickhouse_driver import Client
ch = Client(host="127.0.0.1", port=9000, password="change-me")
rows = ch.execute("SELECT name FROM Person LIMIT 5")
print(rows)Read + write. The door routes governed SELECTs through the planner and accepts INSERT INTO <table> FORMAT JSONEachRow writes that go through the same governed pipeline (ACL, ontology, WAL, provenance, audit) as HTTP /ingest.
# Write
curl -X POST "http://127.0.0.1:8123/" \
-H "X-ClickHouse-Key: change-me" \
--data-binary "INSERT INTO Person FORMAT JSONEachRow" \
--data-binary '{"id":"ch1","name":"from-clickhouse"}'
# Read back over HTTP SQL (cross-door)
curl -X POST http://127.0.0.1:9090/query \
-H "Authorization: Bearer change-me" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT * FROM Person WHERE id = '\''ch1'\''","purpose":"analytics"}'Neo4j — HTTP Cypher or Bolt
# HTTP Cypher
curl -X POST http://neo4j:change-me@127.0.0.1:7474/db/neo4j/tx/commit \
-H "Content-Type: application/json" \
-d '{"statements":[{"statement":"MATCH (n) RETURN n LIMIT 5"}]}'from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://127.0.0.1:7687", auth=("neo4j", "change-me"))
with driver.session() as s:
print(s.run("MATCH (n) RETURN n LIMIT 5").data())Typed labels (n:Person) map to governed object types. Read and write Cypher supported — CREATE / MERGE go through the governed write door (run_protocol_cypher_write) with the same ACL, ontology validation, WAL, and audit as HTTP /ingest.
Cypher subset supported:
MATCH/OPTIONAL MATCH/WITH(read)CREATE/MERGE(write — superseded by governed upsert)- Relationship path patterns including bounded
-[r*1..5]-> - Single-identifier
RETURN [AS alias] - Whitelisted property predicates
# Write via Bolt, read back over SQL (cross-door)
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://127.0.0.1:7687", auth=("neo4j", "change-me"))
with driver.session() as s:
s.run("CREATE (n:Person {id:'neo1', name:'from-cypher'})")-- Read the same node over HTTP SQL
SELECT * FROM Person WHERE id = 'neo1';Redis — any RESP client
redis-cli -h 127.0.0.1 -p 6379 -a change-me SET foo bar
redis-cli -h 127.0.0.1 -p 6379 -a change-me GET fooGoverned keys persist as KvEntry rows. Read back over SQL:
SELECT key, value FROM KvEntry WHERE key = 'foo';Not supported: MULTI/EXEC, BLPOP, scripting, cluster commands. Pub/Sub (PUBLISH / SUBSCRIBE) persists messages as governed PubSubMessage rows so subscribers can replay on reconnect.
MongoDB — any Mongo wire client
const { MongoClient } = require("mongodb");
const c = new MongoClient("mongodb://localhost:27017", {
auth: { username: "relata", password: "change-me" },
});
const db = c.db("cases");
await db.collection("exhibits").insertOne({ _id: "ex1", body: "hello" });
console.log(await db.collection("exhibits").findOne({ _id: "ex1" }));Governed docs persist as MongoDocument rows. Read back over SQL:
SELECT * FROM MongoDocument WHERE collection = 'exhibits';| Limit | Detail |
|---|---|
| Auth | SCRAM-SHA-256 only |
maxWireVersion | 17 |
| Transactions / change streams | Not supported |
$push / $pull / $unset | Not supported |
| Nested equality | Via flattened columns only |
Arrow Flight — zero-copy streaming
# Enable on the server
RELATA_FLIGHT_ENABLE=true relata serveConnect any Arrow Flight client (Python pyarrow.flight, etc.) to grpc://localhost:8815:
import pyarrow.flight as fl
client = fl.connect("grpc://localhost:8815")
reader = client.do_get(fl.FlightDescriptor.for_command(
b"PURPOSE 'analytics' SELECT * FROM Person"))
for batch in reader:
print(batch.data.num_rows, "rows")Arrow IPC format — no JSON intermediate, no string serialisation. Use for high-throughput columnar reads.
SPARQL
# GET
curl "http://localhost:9090/sparql?query=SELECT+%3Fs+%3Fp+%3Fo+WHERE+%7B+%3Fs+%3Fp+%3Fo+%7D+LIMIT+10" \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN"
# POST
curl -X POST http://localhost:9090/sparql \
-H "Content-Type: application/sparql-query" \
-H "Authorization: Bearer $RELATA_BEARER_TOKEN" \
-d "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"Single Basic Graph Pattern over KnowledgeTriple. Optional LIMIT. Both GET and POST verify bearer auth.
Security notes
- All doors bind to
127.0.0.1. - All doors share
RELATA_BEARER_TOKEN. - pgwire is fail-closed (refuses to start without a token).
- All other doors default to open dev mode when the token is unset.
- Egress filtering applies uniformly on every door.
- Cross-protocol reads and writes go through the same planner with the same ACL, org isolation, and audit chain.
See also
- SQL Reference — the underlying query plane
- MCP Tools Reference — agent-native surface
- Limits — per-protocol status and known caveats