Endpoint role and replication lag
BurnLedger has always checked that a customer-supplied credential cannot write, and it now reports what that credential may read (granted read scope). Neither of those says anything about which endpoint answered.
No connector asserted it was reading a writer. Several integration guides actively recommend the opposite — MongoDB ("ideally a secondary, which is what a read-only credential should point at"), Redis ("Point it at a read-only replica so your word is good"), Neo4j ("pointing BurnLedger at a read replica the credential cannot write to") — because a credential that physically cannot write is the strongest form of the read-only guarantee.
That advice stands. This document is the other half of it.
Why it matters, and why the exposure is not symmetric
A count taken from a copy can differ from the same count taken from the writer, in two directions that are worth very different amounts:
- The copy is behind. It still holds records the writer has already deleted. The count comes back too high, the erasure is not certified, the customer re-runs it. This fails closed: nothing untrue is signed.
- The copy is not a copy of the current data at all. A replica that was detached, re-seeded, restored from an older backup, or that filters what it replicates can answer zero where the writer would not. So can any eventually consistent read. That is a signed "no records found" over data that still exists, and no verifier downstream can detect it — the certificate is authentic, the count is a real number, and the number is wrong.
The second one is what this discloses.
What it is not
It is not enforcement. No connection is refused for reading a replica, and none ever will be by this mechanism. Refusing replicas would push customers towards write-capable credentials on primaries, which is a worse outcome than reading a possibly stale copy. See ADR-028.
It is not attested. Like transport_security, read_only_enforcement and
read_scope, this is operator-facing diagnostics. It is not bound into the
signed attestation payload, so a relying party holding a certificate cannot
check it, and it describes the endpoint at the moment of the check.
Nothing was granted to produce it. Every probe below is answerable by the
credential the connector's own integration guide already tells a customer to
create. Where the answer needs more — MySQL's SHOW REPLICA STATUS wants
REPLICATION CLIENT, MongoDB's replSetGetStatus wants clusterMonitor —
the field says unknown and names the grant instead.
A blank field is not "primary". Role "" means this connector does not ask.
Role unknown means it asked and could not get an answer, and carries a detail
saying why. The two are deliberately different values.
Where it surfaces
| Surface | Field |
|---|---|
POST /v1/systems/test-connection |
replication (role, lag, detail), and appended to the connectivity step's detail |
| Enclave health check | HealthCheckResponse.replication |
Audit log, system.registered |
replication in the entry's metadata |
It is deliberately not a column on the systems table, for the same reason
read_scope is not: a column would claim to describe the endpoint as it is now,
and would be wrong the moment somebody repointed the DSN. An audit entry claims
only what was true when the system was admitted.
Coverage
Reports the endpoint's role
| Connector | Asked | Under which grant | Reports |
|---|---|---|---|
| PostgreSQL | pg_is_in_recovery(), and pg_last_xact_replay_timestamp() for lag |
Any role. These are recovery information functions, unrestricted; the superuser-gated ones are the recovery control functions (pg_wal_replay_pause and friends), and none is used |
primary, or replica with e.g. 3.2s since the last transaction replayed from the primary. That figure is the AGE OF THE LAST REPLAYED TRANSACTION, so it also grows while the primary is idle: an upper bound, not a measured delay, and the detail says so. Verified against the PostgreSQL 17 container |
| Redshift | The same statement | Any user | unknown on a live cluster, and that is the expected answer: Redshift descends from PostgreSQL 8.0, which predates pg_is_in_recovery(), and no Redshift function reports whether a cluster is a data-sharing consumer of another one. The probe is issued anyway so the answer comes from the endpoint rather than from an assumption. Verified against the PostgreSQL container the Redshift suite runs on, not against a live Redshift cluster |
| MySQL | @@global.read_only, @@global.super_read_only |
Any account; system variables need no privilege | primary when both are OFF, replica when read_only is ON. Lag is never reported: Seconds_Behind_Source comes from SHOW REPLICA STATUS, which needs REPLICATION CLIENT — a privilege the MySQL guide lists among those that mark a credential write-capable and get it refused. A server with no super_read_only (MariaDB) reports unknown. Verified against MySQL 8.0 |
| Redis | INFO replication |
+info, which the Redis guide's ACL recipe already grants for the mandatory INFO cluster check |
primary for role:master; replica for role:slave, with master_last_io_seconds_ago as the lag and a note that this is time since the last contact, not a replication delay — Redis publishes no offset-based lag a read-only ACL user can read. A master_link_status other than up is named. Verified against redis:7-alpine, including a real detached replica under the guide's exact ACL |
| MongoDB | hello |
None. hello is answered before access control is considered |
primary (standalone, replica-set primary, or a mongos router), replica for a secondary, unknown for a starting/recovering/rollback member or an arbiter. Lag is never reported, and asking for it would break the connector: replSetGetStatus needs clusterMonitor, and the MongoDB guide says a credential holding anything from clusterMonitor is refused with WRITE_ACCESS_DETECTED. Verified against mongo:7 |
| Neo4j | SHOW DATABASES |
Any user with ACCESS to the database, which the built-in reader role grants |
primary for standalone, leader, or 5.x primary with writer: true; replica for follower, read_replica, secondary, or 5.x primary with writer: false. unknown when the database is hosted by more than one server — the Bolt result does not say which one answered, and the driver routes each transaction independently. dbms.cluster.role() is deliberately not used: Enterprise-only in 4.x, removed in 5. No lag; Neo4j publishes it only through metrics endpoints a query credential cannot reach. Verified against neo4j:5-enterprise |
Discloses a read-consistency choice instead
These have no primary to ask about, or the answer is decided by the request this connector sends rather than by the endpoint. What they disclose is the choice this product made on the customer's behalf, which was previously visible only by reading the source.
| Connector | Role | Discloses |
|---|---|---|
| Cassandra | no_primary |
Every replica is a peer. Reads run at LOCAL_QUORUM — set in buildClusterConfig, unchanged by this work — so a write acknowledged at a quorum of the same datacenter cannot be missed, but a keyspace replicated across datacenters can still answer from a local quorum that has not received a remote write. A unit test pins the sentence to the driver's configured level, so the two cannot drift |
| Elasticsearch | no_primary |
Elasticsearch 7.0 removed the _primary search preference; a search is answered by whichever copy of each shard the cluster picks, and no request can pin it to the primaries. A preference value is a routing hint, not a consistency guarantee, so none is sent. What the connector checks instead is the _shards block on every response, and a search whose shards did not all report is refused |
| DynamoDB | primary |
Base-table queries now set ConsistentRead (see below). A query template naming an index reads a global secondary index, which DynamoDB does not allow to be read consistently: a GSI lags its table and can answer zero for a subject the table still holds |
Does not report it yet, but could
Listed so a blank field is not read as "there is no replica here".
| Connector | What would answer it |
|---|---|
| SQL Server | An Always On readable secondary is visible to a plain login through DATABASEPROPERTYEX(DB_NAME(), 'Updateability'). This is a gap, not a limit |
| Azure Blob | A read-access geo-redundant account exposes a -secondary endpoint that is an eventually consistent copy, and the guide neither recommends nor forbids pointing at it |
Cannot report it
| Connector | Why |
|---|---|
| Oracle | Data Guard publishes the standby role in V$DATABASE, which the guide's credential cannot select — it grants SELECT on the subject tables plus the USER_*/ROLE_*/SESSION_PRIVS views every user already holds. Widening a customer's grant to produce a diagnostic is the trade this work refuses |
| Teradata, Snowflake, Databricks, BigQuery | No customer-selectable replica endpoint: the service routes the query itself and publishes no role for whatever answered it |
| S3, GCS | Both are strongly consistent for the reads this connector issues, so there is no copy for a count to be behind |
| HBase | No role introspection over the wire API the connector uses — the same reason its read-only status can only ever be operator-asserted |
| MarkLogic | Role and replication state live behind the Management REST API, which the query credential cannot reach — the same boundary that blocks its read scope |
connector/replication_pin_test.go parses each connector's source and fails when
one changes category, so this table cannot silently drift away from the code.
The one behaviour change: DynamoDB reads consistently
Every other part of this work is disclosure. DynamoDB is not.
QueryInput.ConsistentRead is now true for a query against the base table. It
was unset, which means DynamoDB's default: an eventually consistent read,
which may be answered by a replica that has not yet received a write the table
already acknowledged. Against a subject's records, that produces a certified
"no records found" over data that exists — the exact false zero described at the
top of this document, reachable without any replica being involved at all.
It costs one extra read capacity unit per 4 KB and gives up availability during a partition. Both are the right trade for evidence.
It is not set when the query template names an index: DynamoDB rejects
ConsistentRead on a global secondary index, and this connector cannot tell a
global index from a local one without a DescribeTable round trip on every
query. An indexed template therefore keeps the semantics it always had, and the
disclosure says so.