About Anchor Forwarding
Introduction
Anchor forwarding lets a ledger delegate anchor operations to a Bridge. This is useful when anchors are managed by an external system (for example, an external alias directory) while the ledger remains the public API surface and source of authorization decisions.
Before diving in, a brief note on alias directories. An alias directory is a service that maps human‑readable identifiers (like phone numbers or emails) to payment credentials. In deployments where an external alias directory (Bridge) owns anchors, anchor forwarding allows the ledger to forward create/update/sign/drop/read operations to that Bridge while still enforcing access rules and signing responses.
What gets forwarded
Ledger can forward the following anchor actions:
- Create
- Update
- Sign (add a proof)
- Drop (delete)
- Get (read one)
- FindAll (get many with filters)
Forwarding is controlled by configuration per action and, optionally, by a forwarding strategy.
Configuration
Set the Bridge handle that should receive forwarded requests. You can update the active ledger config with CLI, for example using minka ledger update -e.
{
"forward.anchor.create": "MyBridgeHandle",
"forward.anchor.update": "MyBridgeHandle",
"forward.anchor.drop": "MyBridgeHandle",
"forward.anchor.sign": "MyBridgeHandle",
"forward.anchor.get": "MyBridgeHandle",
"forward.anchor.findAll": "MyBridgeHandle"
}Optionally configure a global or per‑action strategy:
{
"forward.anchor.strategy": "proxy | fallback | validate | none",
"forward.anchor.create.strategy": "proxy | validate | none",
"forward.anchor.update.strategy": "proxy | validate | none",
"forward.anchor.drop.strategy": "proxy | validate | none",
"forward.anchor.get.strategy": "proxy | fallback | none",
"forward.anchor.sign.strategy": "proxy | validate | none",
"forward.anchor.findAll.strategy": "proxy | fallback | none"
}- proxy: Forward the request and do not persist or mutate the record locally (proxy‑only).
- fallback: For reads/lists, return local data when present; otherwise call the Bridge.
- validate: Attempt the local DB operation first (without committing) to ensure it would succeed, forward to the Bridge, then persist locally if the Bridge succeeds. If the Bridge fails, do not mutate locally.
- none: Do not forward for this action.
If a per‑action strategy is not set, the global forward.anchor.strategy is applied. If no strategy is provided, sensible defaults are used internally: reads default to fallback; writes default to validate.
Bridge endpoints
The Bridge must expose the same API surface as the ledger for anchors. Requests are forwarded as‑is (with some headers filtered and a ledger authorization added), and responses are expected to match the ledger Anchor API.
Required endpoints:
- POST
/v2/anchors - PUT
/v2/anchors/:id - DELETE
/v2/anchors/:id - GET
/v2/anchors/:id - POST
/v2/anchors/:id/proofs(sign) - GET
/v2/anchors(findAll with filters)
Responses must conform to the ledger Anchor API. See the Anchor section in the API reference.
What the ledger forwards
- Request body/query params: Forwarded unchanged.
- Headers: Custom client headers are forwarded except a forbidden set. The original client Authorization (if present) is moved to
x-forwarded-authorizationand is not sent asauthorizationto the Bridge. - Ledger authorization: The ledger adds its own Bearer JWT in
Authorization, signed by the ledger. The JWT contains claims identifying the ledger and the intended Bridge audience.
Forbidden headers that are not forwarded include: authorization, x-ledger, content-type, content-length, accept, accept-encoding, caller, connection, host.
Responses and errors
Success responses
The ledger returns the response from the Bridge after verifying hash and proofs are consistent:
- Proxy mode: Returns the Bridge response directly to the client.
- Validate mode: Returns the result of the local operation to the client (after confirming the Bridge call succeeded).
- FindAll queries: The entire page body from the Bridge is forwarded.
Error handling
When a forwarded request to a Bridge fails, the ledger interprets and returns Bridge errors with meaningful structure whenever possible. The handling depends on the error type, status code, and response structure.
Error handling summary table
| Case | Bridge Response | Bridge Status Code | Ledger Response Status Code | Ledger Error Reason | Notes |
|---|---|---|---|---|---|
| Valid bridge error | Well-formed error with data (containing reason, detail, optional custom), meta, hash, and valid proofs | Any (e.g., 400, 404, 422) | Same as bridge | Same as bridge data.reason | Bridge error is forwarded to client with ledger signature added |
| Missing fields | Missing data, meta, or hash | Any | 502 | forward.invalid-response | Response structure is incomplete |
| Invalid data structure | data missing required reason or detail fields | Any | 502 | forward.invalid-response | Data doesn't conform to ledger error schema |
| Invalid data types | reason or detail not strings, or custom not a plain object | Any | 502 | forward.invalid-response | Data fields have wrong types |
| Extra fields in data | data contains fields other than reason, detail, custom | Any | 502 | forward.invalid-response | Data has unevaluated properties |
| Invalid hash or proofs | Hash doesn't match data or proofs fail verification | Any | 502 | forward.invalid-response | Cryptographic validation failed |
| Ledger at fault | Any response structure | 401 or 403 | 500 | forward.unexpected-error | Ledger credentials or config issue |
| Connection error | Network, timeout, or code error | N/A | 500 | forward.unexpected-error | Unexpected system error |
Detailed error cases with examples
1. Valid bridge error (forwarded to client)
When the Bridge returns a well-formed error response, the ledger validates the structure and cryptographic integrity, then forwards the error to the client with the Bridge's original status code.
Bridge response (status 404):
{
"data": {
"reason": "record.not-found",
"detail": "Anchor with handle 'missing-anchor' not found",
"custom": {
"searchedHandle": "missing-anchor",
"bridgeContext": "alias-directory"
}
},
"hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
"meta": {
"moment": "2025-11-04T10:30:00.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:00.000Z"
},
"method": "ed25519-v2",
"result": "abc123...",
"public": "bridge-public-key",
"digest": "def456..."
}]
}
}Ledger response to client (status 404):
{
"data": {
"reason": "record.not-found",
"detail": "Anchor with handle 'missing-anchor' not found",
"custom": {
"searchedHandle": "missing-anchor",
"bridgeContext": "alias-directory"
}
},
"hash": "7f3e8a2b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [
{
"custom": {
"moment": "2025-11-04T10:30:00.000Z"
},
"method": "ed25519-v2",
"result": "abc123...",
"public": "bridge-public-key",
"digest": "def456..."
},
{
"custom": {
"moment": "2025-11-04T10:30:01.000Z",
"causedBy": {
"detail": "Error derived from anchor forwarding response"
}
},
"method": "ed25519-v2",
"result": "xyz789...",
"public": "ledger-public-key",
"digest": "ghi012..."
}
]
}
}Key points:
- Status code is preserved (404)
- Bridge error
reasonanddetailare forwarded as-is - Bridge
customdata is preserved indata.custom - Ledger appends its own proof to the
meta.proofsarray - Ledger adds
causedBycontext in its proof'scustomfield - Hash remains the same (hash of the original
data)
2. Invalid bridge response - Missing required fields
Bridge response (status 400):
{
"error": {
"code": "INVALID_ANCHOR",
"message": "Anchor data is invalid"
}
}Ledger response to client (status 502):
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}Key points:
- Status code changes to 502 (Bad Gateway)
- Ledger generates its own error with reason
forward.invalid-response - Bridge response doesn't contain required
data,meta,hashstructure
3. Invalid bridge response - Wrong data structure
Bridge response (status 400):
{
"data": {
"errorCode": "ANCHOR_EXISTS",
"message": "Anchor already exists"
},
"hash": "...",
"meta": {
"proofs": [...]
}
}Ledger response to client (status 502):
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}Key points:
- Status code changes to 502
- Bridge
dataobject must containreasonanddetailfields (noterrorCodeandmessage) - Only
reason,detail, andcustomfields are allowed indata
4. Invalid bridge response - Invalid hash or proofs
Bridge response (status 422):
{
"data": {
"reason": "record.invalid",
"detail": "Anchor validation failed"
},
"hash": "incorrect-hash-value",
"meta": {
"proofs": [{
"method": "ed25519-v2",
"result": "invalid-signature",
"public": "bridge-public-key",
"digest": "..."
}]
}
}Ledger response to client (status 502):
{
"data": {
"reason": "forward.invalid-response",
"detail": "Invalid response from bridge test-bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}Key points:
- Status code changes to 502
- Ledger validates that
hashmatches the hash ofdata - Ledger validates that proofs are valid for the given
hash - If validation fails, returns
forward.invalid-response
5. Ledger credentials error (401/403)
Bridge response (status 401):
{
"data": {
"reason": "auth.unauthorized",
"detail": "Invalid JWT signature"
},
"hash": "...",
"meta": {
"proofs": [...]
}
}Ledger response to client (status 500):
{
"data": {
"reason": "forward.unexpected-error",
"detail": "Unexpected error while forwarding request to bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}Key points:
- Bridge returns 401 or 403, indicating ledger's credentials are invalid
- Status code changes to 500 (Internal Server Error)
- Ledger treats this as its own fault, not a bridge business error
6. Connection error (network, timeout, etc.)
Example: Network connection timeout, DNS resolution failure, or code exception.
Ledger response to client (status 500):
{
"data": {
"reason": "forward.unexpected-error",
"detail": "Unexpected error while forwarding request to bridge"
},
"hash": "...",
"meta": {
"moment": "2025-11-04T10:30:01.000Z",
"proofs": [{
"custom": {
"moment": "2025-11-04T10:30:01.000Z"
},
"method": "ed25519-v2",
"result": "...",
"public": "ledger-public-key",
"digest": "..."
}]
}
}Key points:
- Any error that's not from the bridge (HTTP response errors)
- Status code is 500
- Indicates system-level error, not a business error
Additional notes
- No local mutation on Bridge failure: In all write operations, if the Bridge returns any kind of error, the operation is not persisted or mutated locally, regardless of strategy.
- Bridge error schema requirements: For errors to be properly forwarded, Bridges must return error responses using the ledger error schema with
data.reason,data.detail, and optionaldata.customfields. - Proof validation: All Bridge responses (both success and error) must include valid cryptographic proofs that the ledger can verify.
See the Anchor API Spec Reference for additional details on error response schemas.
Access checks and validation
- Writes (create/update/sign/drop): The ledger validates authorization before any forwarding occurs. It also performs local pre‑checks to ensure the DB operation would succeed (insert/update/sign/drop) when using the validate strategy.
- Reads (get/findAll): The ledger validates authorization on returned data. With
fallback, the ledger will return local data when available; otherwise it fetches from the Bridge and then validates proofs and access before returning. - Schema and proofs: Returned anchors are validated for proofs integrity. In validate strategy, local state is also verified before commit.
Proofs added by the ledger when forwarding
When forwarding write operations, the ledger appends a proof with custom.status = "forwarded" to the outgoing meta before calling the Bridge. The Bridge response must still contain valid proofs; the ledger will verify them before returning to the client (or before persisting, in validate mode).
Flows
Create
Update
Sign
Drop
Get by id/handle
Get many (findAll)
Security notes
- The client Authorization header is not forwarded as
authorization; it is copied tox-forwarded-authorizationfor the Bridge to inspect if needed. - The ledger signs forwarded requests by adding its own Bearer JWT in
authorization. Bridges can validate the signature with the ledger public key and may also require OAuth on top if desired. - Standard Bridge authentication can be configured separately (e.g., OAuth 2.0). If a Bridge uses OAuth, the ledger will still send its own JWT while preserving the client token in
x-forwarded-authorization. - All communication between ledger and Bridge must use TLS.
Operational notes
- Errors from the Bridge are surfaced to clients with ledger signatures and without persisting local mutations.
- In proxy strategy, records are not stored locally. Reads will come from the Bridge and writes will only affect the Bridge.
- In validate strategy, local operations are performed only after the Bridge confirms success.