From 2af6f41df61e3720cd9ebaca1fe5b27a55f00e13 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 20 Oct 2026 08:15:57 -0600 Subject: [PATCH 1/6] Document super_user-only replication directives and the unconfirmed-commit outcome X-Replicate-To and the operations API replicateTo/replicatedConfirmation fields require super_user (harper#3084). A confirmation failure after a local commit is reported with replicationUnconfirmed: true and retryable: false (harper#3187). Dispatch-Task: documentation-replicate-to-superuser-and-unconfirmed Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01KEQo4qTSsfyXTJf7JNQVsz --- reference/replication/sharding.md | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index 02d46fb56..ce221f713 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -43,6 +43,8 @@ This ensures each record is stored on three nodes total (the node that first sto With the REST interface, you can specify replication targets and confirmation requirements per request using the `X-Replicate-To` header: + — Only a principal with the `super_user` permission can send `X-Replicate-To`. Other callers receive HTTP 403 whenever the header has a value, including on read requests and for values such as `0`, a node list, or `*`. Application code can still set replication options on a trusted context. + ```http PUT /MyTable/3 X-Replicate-To: 2;confirm=1 @@ -64,6 +66,8 @@ The `confirm` parameter can be combined with explicit node lists. Specify `replicateTo` and `replicatedConfirmation` in the operation body: + — These operation fields require the `super_user` permission. A non-super-user caller receives HTTP 403 when an operation body includes either field with a non-null value, including `replicateTo: 0` and an empty node list. + ```json { "operation": "update", @@ -103,6 +107,34 @@ class MyTable extends tables.MyTable { } ``` +### Confirmation Failure After a Local Commit + + + +A replication confirmation failure can occur after the write has committed locally. When that happens, the rejected commit error and the REST or operations API error body include `replicationUnconfirmed: true` and `retryable: false`. Terminal JSON, NDJSON, and SSE stream error records also carry these fields. Every local database write in that commit's linked transaction has finished committing, but the requested peer confirmation was not obtained. This provides the same local durability as a successful write response; it does not add an fsync guarantee. + +For example, a REST error for a confirmation timeout can contain: + +```json +{ + "type": "error:ServerError", + "code": "ServerError", + "title": "Timed out after 900000ms waiting for replication confirmation from 2 node(s) for database \"data\" (received 1)", + "status": 504, + "instance": "/MyTable/3", + "replicationUnconfirmed": true, + "retryable": false +} +``` + +The existing error status, code, and message remain available. An operations error has its usual `error` field plus `code`, `replicationUnconfirmed: true`, and `retryable: false`. Programmatic errors wrap the original rejection in `cause`; text-only protocol errors use the new `ReplicationConfirmationError` class name. Check this flag before compensating or retrying a non-idempotent write, because replaying the request could repeat an insert or apply an update twice. The flag does not return auto-generated record keys; use client-selected identifiers when you need to inspect the written records after a failed response. + +The flag covers writes issued to the reported commit, not later application steps that were never reached after its rejection. If an enclosing scope rolls back its pending writes, its error omits the flag while preserving `retryable: false` because an independent inner write may already have committed. + +Pre-commit refusals, including a confirmation count that cannot be met, do not carry this flag. Its absence does not establish that nothing was written: independent database commits can fail partially. A rejected commit ends its transaction scope; start a fresh transaction for subsequent writes. + +Confirmation currently applies to the first database a transaction writes to, and only when it uses RocksDB. This outcome flag covers the linked local commits; it does not enable confirmation for later databases or the LMDB storage engine. + ## Static Sharding ### Basic Static Shard Configuration From 4a55c34c954af1cd99ef15f548e683bea5ab9b0c Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Tue, 20 Oct 2026 08:24:35 -0600 Subject: [PATCH 2/6] Scope the operations 403 note to operations that accept the fields The sql operation never reads a body replicateTo or replicatedConfirmation, so it is outside the verifyPerms guard by design; keep the claim to the operations that consume them. Dispatch-Task: documentation-replicate-to-superuser-and-unconfirmed Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01KEQo4qTSsfyXTJf7JNQVsz --- reference/replication/sharding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index ce221f713..03c660ef4 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -66,7 +66,7 @@ The `confirm` parameter can be combined with explicit node lists. Specify `replicateTo` and `replicatedConfirmation` in the operation body: - — These operation fields require the `super_user` permission. A non-super-user caller receives HTTP 403 when an operation body includes either field with a non-null value, including `replicateTo: 0` and an empty node list. + — These operation fields require the `super_user` permission. For operations that accept these fields, a non-super-user caller receives HTTP 403 when either field has a non-null value, including `replicateTo: 0` and an empty node list. ```json { From 925481ceec6b2c2119ef36755c627f7df3805336 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 23 Oct 2026 11:27:50 -0600 Subject: [PATCH 3/6] Link the trusted-context example and warn against copying request input The 403 notes now point to Programmatic Replication Control, whose example shows the application deciding replication itself, and a sentence says that copying client input into context.replicateTo or replicatedConfirmation hands that client the control the super_user check removes. Dispatch-Task: fix-kriszyp_documentation_740-efd46db4 Co-Authored-By: Claude Opus 5.5 --- reference/replication/sharding.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index 03c660ef4..9bb0e9700 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -43,7 +43,7 @@ This ensures each record is stored on three nodes total (the node that first sto With the REST interface, you can specify replication targets and confirmation requirements per request using the `X-Replicate-To` header: - — Only a principal with the `super_user` permission can send `X-Replicate-To`. Other callers receive HTTP 403 whenever the header has a value, including on read requests and for values such as `0`, a node list, or `*`. Application code can still set replication options on a trusted context. + — Only a principal with the `super_user` permission can send `X-Replicate-To`. Other callers receive HTTP 403 whenever the header has a value, including on read requests and for values such as `0`, a node list, or `*`. Application code can still set replication options on a trusted context; see [Programmatic Replication Control](#programmatic-replication-control). ```http PUT /MyTable/3 @@ -66,7 +66,7 @@ The `confirm` parameter can be combined with explicit node lists. Specify `replicateTo` and `replicatedConfirmation` in the operation body: - — These operation fields require the `super_user` permission. For operations that accept these fields, a non-super-user caller receives HTTP 403 when either field has a non-null value, including `replicateTo: 0` and an empty node list. + — These operation fields require the `super_user` permission. For operations that accept these fields, a non-super-user caller receives HTTP 403 when either field has a non-null value, including `replicateTo: 0` and an empty node list. Application code can still set them; see [Programmatic Replication Control](#programmatic-replication-control). ```json { @@ -94,19 +94,21 @@ Or specify explicit nodes: ### Programmatic Replication Control -Set `replicateTo` and `replicatedConfirmation` programmatically in a resource method: +Set `replicateTo` and `replicatedConfirmation` programmatically in a resource method. Here the application, not the client, decides how widely an order is replicated and whether to wait for a peer: ```javascript -class MyTable extends tables.MyTable { +export class Order extends tables.Order { put(record) { const context = this.getContext(); context.replicateTo = 2; // or an array of node names - context.replicatedConfirmation = 1; + if (record.total >= 10000) context.replicatedConfirmation = 1; return super.put(record); } } ``` +Values your code sets on the context apply for every caller, including users without the `super_user` permission. Derive them from application logic, not from request input such as headers, query parameters, or the record body: copying a client-supplied value into `context.replicateTo` or `context.replicatedConfirmation` gives that client the control the `super_user` check removes. + ### Confirmation Failure After a Local Commit From 0a1870675465e2805377821733a4d0e8ee84e626 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 23 Oct 2026 11:33:21 -0600 Subject: [PATCH 4/6] Show replicatedConfirmation through a new transaction, not a resource method transaction() copies context.replicatedConfirmation onto the transaction when it starts, and a REST request starts its transaction before the resource method runs, so setting the count in put() was silently ignored. The example now sets only replicateTo (read at write time) in the method and shows the count passed to a new transaction() context. Dispatch-Task: fix-kriszyp_documentation_740-efd46db4 Co-Authored-By: Claude Opus 5.5 --- reference/replication/sharding.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index 9bb0e9700..9b2134783 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -94,20 +94,29 @@ Or specify explicit nodes: ### Programmatic Replication Control -Set `replicateTo` and `replicatedConfirmation` programmatically in a resource method. Here the application, not the client, decides how widely an order is replicated and whether to wait for a peer: +Set `replicateTo` on the context in a resource method. It applies to the writes made after you set it. Here the application, not the client, decides how widely an order is replicated: ```javascript export class Order extends tables.Order { put(record) { const context = this.getContext(); - context.replicateTo = 2; // or an array of node names - if (record.total >= 10000) context.replicatedConfirmation = 1; + context.replicateTo = record.total >= 10000 ? 3 : 1; // or an array of node names return super.put(record); } } ``` -Values your code sets on the context apply for every caller, including users without the `super_user` permission. Derive them from application logic, not from request input such as headers, query parameters, or the record body: copying a client-supplied value into `context.replicateTo` or `context.replicatedConfirmation` gives that client the control the `super_user` check removes. +`replicatedConfirmation` is read when a transaction starts. A request's transaction has already started when your resource method runs, so setting `replicatedConfirmation` on its context there has no effect. To wait for peer confirmation from code, pass it in the context of a new [transaction](../database/api.md#transactioncontext-callback): + +```javascript +await transaction({ replicatedConfirmation: 1 }, async () => { + await tables.Order.put(order); +}); +``` + +That transaction commits on its own, separately from any request transaction that is running. + +Values your code sets on a context apply for every caller, including users without the `super_user` permission. Derive them from application logic, not from request input such as headers, query parameters, or the record body: copying a client-supplied value into `replicateTo` or `replicatedConfirmation` gives that client the control the `super_user` check removes. ### Confirmation Failure After a Local Commit From d6d8f3bd07586a281c572d37a63a16b4c012ab70 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 23 Oct 2026 11:41:16 -0600 Subject: [PATCH 5/6] Say the confirming transaction needs a new context and commits on its own Dispatch-Task: fix-kriszyp_documentation_740-efd46db4 Co-Authored-By: Claude Opus 5.5 --- reference/replication/sharding.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index 9b2134783..743245699 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -106,15 +106,17 @@ export class Order extends tables.Order { } ``` -`replicatedConfirmation` is read when a transaction starts. A request's transaction has already started when your resource method runs, so setting `replicatedConfirmation` on its context there has no effect. To wait for peer confirmation from code, pass it in the context of a new [transaction](../database/api.md#transactioncontext-callback): +`replicatedConfirmation` is read when a transaction starts. A request's transaction has already started when your resource method runs, so setting `replicatedConfirmation` on its context there has no effect. To wait for peer confirmation from code, pass it in a new context object to [`transaction()`](../database/api.md#transactioncontext-callback). Do not pass the request's context, or a copy of it: it already holds the request's transaction, so `transaction()` joins that transaction and the count is ignored. ```javascript -await transaction({ replicatedConfirmation: 1 }, async () => { +import { tables, transaction } from 'harper'; + +await transaction({ replicatedConfirmation: 1, replicateTo: 2 }, async () => { await tables.Order.put(order); }); ``` -That transaction commits on its own, separately from any request transaction that is running. +That transaction commits on its own, separately from any request transaction that is running: if the request fails afterwards, this write stays committed. Set `replicateTo` on the same object when you need both. Values your code sets on a context apply for every caller, including users without the `super_user` permission. Derive them from application logic, not from request input such as headers, query parameters, or the record body: copying a client-supplied value into `replicateTo` or `replicatedConfirmation` gives that client the control the `super_user` check removes. From a0c95c0afa4e79bb43f17eec0361cfe2cb078df3 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 2 Nov 2026 15:15:58 -0700 Subject: [PATCH 6/6] Drop the RocksDB-only limit on replication confirmation harper#3392 (in v5.4.0-alpha.1) makes replicatedConfirmation wait for peers on LMDB too, through the same ReplicationConfirmationError path. Dispatch-Task: fix-kriszyp_documentation_740-1169ad29 Co-Authored-By: Claude Opus 5.5 --- reference/replication/sharding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/replication/sharding.md b/reference/replication/sharding.md index 743245699..b75ba525f 100644 --- a/reference/replication/sharding.md +++ b/reference/replication/sharding.md @@ -146,7 +146,7 @@ The flag covers writes issued to the reported commit, not later application step Pre-commit refusals, including a confirmation count that cannot be met, do not carry this flag. Its absence does not establish that nothing was written: independent database commits can fail partially. A rejected commit ends its transaction scope; start a fresh transaction for subsequent writes. -Confirmation currently applies to the first database a transaction writes to, and only when it uses RocksDB. This outcome flag covers the linked local commits; it does not enable confirmation for later databases or the LMDB storage engine. +Confirmation currently applies to the first database a transaction writes to. This outcome flag covers the linked local commits; it does not enable confirmation for later databases. ## Static Sharding