Explanations

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

Anchor forwarding can be configured in two ways: ledger configuration or processing policies.

Option 1: Processing policies (Preferable)

Create a processing policy with schema: 'processing' to define forwarding rules. This approach offers more flexibility including conditional forwarding with filters. This can be used to configure multiple alias directories depending on context e.g. request headers.

await sdk.policy.init().data({
  handle: 'anchor-forwarding',
  schema: 'processing',
  record: 'anchor',
  filter: { 'ctx.req.headers': 'rail-1' },  // Optional: forward only matching anchors
  values: [{
    action: 'read',
    schema: 'aspect',
    invoke: { bridge: 'MyMailBridgeHandle' },
    config: { strategy: 'fallback' | 'proxy' },
    filter: { 'ctx.req.headers': 'rail-2' } // Value filter overrides root filter
  }, {
    action: 'read',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'fallback' | 'proxy' }
  }, {
    action: 'query',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'fallback' | 'proxy' }
  }, {
    action: 'create',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'validate' | 'proxy' },
    filter: { 'new.data.schema': 'qr-code' } // resolves to this config if created anchor is has schema qr-code
  }, {
    action: 'update',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'validate' | 'proxy' }
  }, {
    action: 'sign',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'validate' | 'proxy' }
  }, {
    action: 'drop',
    schema: 'aspect',
    invoke: { bridge: 'MyBridgeHandle' },
    config: { strategy: 'validate' | 'proxy' }
  }]
}).hash().sign([{ keyPair }]).send()

For complete details on processing policies, see Processing Policies.

Option 2: Ledger configuration

Set the Bridge handle directly in ledger config. Update with CLI 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"
}

Deprecation notice: Configuring anchor forwarding via ledger configuration will be deprecated in a future release. We recommend using processing policies instead, as they provide more flexibility and better integration with the policy system. Please, migrate your forwarding config to processing policies.

Forwarding strategies

  • proxy: Forward the request without persisting locally (proxy‑only).
  • fallback: Return local data when present; otherwise call the Bridge. Only for read/query actions.
  • validate: Validate locally first, forward to Bridge, then persist if Bridge succeeds. Only for write actions.
  • none: Do not forward for this action (ledger config only).

If no strategy is specified, sensible defaults are used: 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-authorization and is not sent as authorization to 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

CaseBridge ResponseBridge Status CodeLedger Response Status CodeLedger Error ReasonNotes
Valid bridge errorWell-formed error with data (containing reason, detail, optional custom), meta, hash, and valid proofsAny (e.g., 400, 404, 422)Same as bridgeSame as bridge data.reasonBridge error is forwarded to client with ledger signature added
Missing fieldsMissing data, meta, or hashAny502forward.invalid-responseResponse structure is incomplete
Invalid data structuredata missing required reason or detail fieldsAny502forward.invalid-responseData doesn't conform to ledger error schema
Invalid data typesreason or detail not strings, or custom not a plain objectAny502forward.invalid-responseData fields have wrong types
Extra fields in datadata contains fields other than reason, detail, customAny502forward.invalid-responseData has unevaluated properties
Invalid hash or proofsHash doesn't match data or proofs fail verificationAny502forward.invalid-responseCryptographic validation failed
Ledger at faultAny response structure401 or 403500forward.unexpected-errorLedger credentials or config issue
Connection errorNetwork, timeout, or code errorN/A500forward.unexpected-errorUnexpected 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 reason and detail are forwarded as-is
  • Bridge custom data is preserved in data.custom
  • Ledger appends its own proof to the meta.proofs array
  • Ledger adds causedBy context in its proof's custom field
  • 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, hash structure
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 data object must contain reason and detail fields (not errorCode and message)
  • Only reason, detail, and custom fields are allowed in data
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 hash matches the hash of data
  • 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 optional data.custom fields.
  • 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 to x-forwarded-authorization for 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.