The S3 door — your existing S3 client, governed

Relata serves a real S3-compatible API. Your existing boto3 / aws CLI / rclone / MinIO client works unchanged — point it at Relata's S3 port and use the bearer token as the access key (or set up SigV4). Every PutObject lands as a governed S3Object row with PROV-O provenance, bi-temporal history, and Cedar ACL — so you can write via S3 and read the same data back over SQL, with the audit chain recording every object event.

Why this matters: MinIO/S3 give you blob storage. Relata's S3 door gives you blob storage plus provenance, bi-temporal versioning, cell-level ACL, and cross-protocol reads — your SQL analytics, your graph traversal, and your hybrid search all see the same objects.

Connect

The door auto-enables when RELATA_BEARER_TOKEN is set (see Compatibility & Doors).

RELATA_BEARER_TOKEN=change-me relata serve
# S3 door auto-starts on port 9191
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",        # = RELATA_BEARER_TOKEN
    aws_secret_access_key="unused",       # SigV4 secret defaults to the bearer token
    config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)

The SigV4 secret defaults to RELATA_BEARER_TOKEN unless you set RELATA_S3_SECRET_KEY to a dedicated secret. When a secret is configured, the door requires verified SigV4 and rejects plaintext bearer auth (set RELATA_S3_ALLOW_PLAINTEXT=true only for dev — cleartext credentials are forgeable).

Supported operations

OperationNotes
ListBucketsgoverned — only buckets the caller's ACL permits
CreateBucket / HeadBucket / DeleteBucketbucket must be empty to delete
ListObjectsV2with prefix + delimiter
PutObject / GetObject / DeleteObject / HeadObjectbodies ≥ RELATA_S3_BLOB_THRESHOLD_MB (default 4 MiB) spill to content-addressed blob store
ListObjectVersionsbi-temporal versioning for free — see below
Multipart uploadparts in-memory only (lost on restart)

ETag is SHA-256.

Bi-temporal object versioning

Because every S3Object is a governed bi-temporal row, S3 object versioning comes for free — no separate versioning flag, no separate store. PUT creates a new row version (the prior one's valid_to closes); DELETE is a delete-marker; history is AS OF-queryable.

# List every version of an object (the bi-temporal history)
curl 'http://127.0.0.1:9191/cases/exhibit-1.txt?versions' \
  -H 'Authorization: Bearer <token>'
 
# Fetch a specific version by its system_from_ns
curl 'http://127.0.0.1:9191/cases/exhibit-1.txt?versionId=1735490000000000000' \
  -H 'Authorization: Bearer <token>'

Honest caveat: ListObjectVersions is not wired for cluster fan-out yet (s3_server.rs follow-up). On RELATA_PROFILE=cluster it returns a clear typed error rather than silently returning partial results. Use it on free/server today; cluster support lands in a future release.

Cross-protocol — read your S3 objects over SQL

The killer feature: the same objects you wrote via boto3 are queryable over SQL, joinable to your typed rows, and searchable via hybrid search:

-- What's in the 'cases' bucket?
SELECT key, size, content_hash, system_from
FROM S3Object
WHERE bucket = 'cases'
ORDER BY system_from DESC LIMIT 20;
 
-- Join objects to typed investigation rows
SELECT s.key, s.size, p.name AS suspect
FROM S3Object s
JOIN Person p ON s.metadata->>'case_id' = p.case_id
WHERE s.bucket = 'cases';
 
-- Object version history at a point in time
SELECT key, size FROM S3Object
WHERE bucket = 'cases' AND key = 'exhibit-1.txt'
AS OF '2026-01-15T00:00:00';

Governed identically: ACL, purpose, tenant isolation, and the audit chain apply on every S3 read/write just as they do on SQL.

Event notifications

Wire S3 events to downstream pipelines (SOAR, Lambda, dgrep) via RELATA_S3_NOTIFY_URL:

RELATA_S3_NOTIFY_URL=https://soar.example.com/relata-s3-events relata serve

After each mutating S3 operation, a JSON payload {"event":"<put|delete>","bucket":"…","key":"…"} is POSTed fire-and-forget — errors are logged and discarded (non-blocking, won't slow the write path).

Tips & takeaways

  • Use a dedicated SigV4 secret in production. RELATA_S3_SECRET_KEY=$(openssl rand -hex 32) keeps your S3 door credential separate from the master bearer token — blast-radius containment if the S3 key leaks.
  • Tune RELATA_S3_BLOB_THRESHOLD_MB for your workload. Small default (4 MiB) inlines objects for fast SQL reads; raise it for media-heavy workloads to keep the row store lean.
  • Versioning is always on. Unlike AWS S3 where you opt in per bucket, every Relata S3 object is bi-temporal from the first PUT?versions works everywhere.
  • Join objects to your graph. Metadata you attach at PUT (s3.put_object(Metadata={"case_id": "case-7"})) is queryable in SQL — link exhibits to suspects without a separate join table.
  • Audit the S3 door by principal. Per-door ACL (s3-client principal) means every S3 write is forensically attributable — see Per-Door ACL.

See also