04 · Interfaces and trust

Every connection has an owner.

A credential is a key to one door. Collectors, operators, queues, and models do not share a pass. The inventory is the map. The sections under it are the contracts, shortened only by linking the schema files.

Cryptography simulated, not verified

Figure

Which credential is allowed to cross which boundary?

Which credential is allowed to cross which boundary? A valid credential is not a global pass. Collectors use a bound OAuth client. Operators use an OIDC session. Queue messages carry no secrets. Model output is untrusted. Collectors present OAuth2 client credentials to intake. The token binds tenant, worker, stable instance and sources. Operators sign in with OIDC. Mutations require the session and CSRF protection. Queue bindings authorize the resolver relay and the product consumer. Payloads hold references, not keys. The extraction worker holds the provider key. The model never receives that key or a tool. Public reads use TLS and a release allowlist. They do not use a consumer secret. OAuth JWT OIDC + CSRF refs only API key TLS read SERVICE Collector Per-instance OAuth client SERVICE Intake API Checks scope, then source PERSON Operator browser OIDC session and CSRF SERVICE Admin API Tenant role grants QUEUE · ASYNC Queues Bindings, no secret inside MODEL ACTOR Model provider Output is untrusted SERVICE Extraction worker Holds the provider key PERSON Public reader No consumer secret SERVICE Product API Release allowlist

Takeaway. A valid credential is not a global pass. Collectors use a bound OAuth client. Operators use an OIDC session. Queue messages carry no secrets. Model output is untrusted.

Text equivalent

  1. Collectors present OAuth2 client credentials to intake. The token binds tenant, worker, stable instance and sources.
  2. Operators sign in with OIDC. Mutations require the session and CSRF protection.
  3. Queue bindings authorize the resolver relay and the product consumer. Payloads hold references, not keys.
  4. The extraction worker holds the provider key. The model never receives that key or a tool.
  5. Public reads use TLS and a release allowlist. They do not use a consumer secret.

Status. Proposed authentication. Cryptography, the identity provider and TLS are simulated in the workbench, not verified here.

Connection inventory

Nineteen connections are recorded in the workbench. EST is established in the design. PROP is proposed. The protocol column is the proposed carrier, not a deployed endpoint.

ConnectionStatusProtocolCallerReceiver
Source fetch EST HTTPS Collector instance (ci_*) on Workers + Cron, Browser Run, a local process or a regional host External source: merchant website, marketplace API or feed (src_*)
Collector registration / bootstrap PROP OAuth2 + HTTPS/JSON An operator provisions a per-instance machine client Resolver intake Worker: POST /v1/workers/register
Artifact upload PROP HTTPS/JSON + PUT Collector instance Resolver intake: POST /v1/artifacts/uploads, then the bytes go to the returned target (a presigned R2 URL, or PUT /v1/artifacts/uploads/{upload_id} streamed through the resolver)
Capture submission EST HTTPS/JSON Collector instance Resolver intake: POST /v1/captures
Receipt lookup EST HTTPS/JSON Collector instance (its own receipts); operator dashboard (any receipt in the tenant) Resolver: GET /v1/receipts/{id}
Heartbeat and run report EST HTTPS/JSON Collector instance Resolver: POST /v1/workers/{id}/heartbeat
Operator login and session PROP OIDC + cookie Operator browser (operator dashboard static assets on the admin origin) Admin origin (resolver administrative API) and the OIDC identity provider
Review queue and case read EST HTTPS/JSON Operator dashboard Resolver admin API: GET /v1/review, GET /v1/review/{case_id}
Review decision EST HTTPS/JSON Operator dashboard (human reviewer) Resolver admin API: POST /v1/decisions
Admin operations (workers, decisions, improvements, dry run) EST HTTPS/JSON Operator dashboard Resolver admin API: GET /v1/workers, GET /v1/decisions/{id}, GET /v1/improvements, POST /v1/resolve (dry run)
Improvement replay PROP HTTPS/JSON → queue Operator dashboard (improvement loop) Resolver admin API: POST /v1/improvements/{id}/replays
Resolution job PROP Queue Resolver intake (producer, after the capture commits) Resolver queue consumers: extraction → normalization → identity
LLM extraction PROP HTTPS/JSON Resolver extraction consumer, when no adapter supports the capture or the adapter failed Model provider API (OPEN which provider)
LLM review advice PROP HTTPS/JSON Resolver review consumer Model provider API (OPEN which provider)
Accepted-change event EST Queue Resolver outbox relay (reads committed outbox rows) Product service inbox consumer
Projection snapshot export (bootstrap) PROP HTTPS/JSON + S3 GET Product service (new deployment, new catalog, or rebuild after projection loss) Resolver export API: POST /v1/exports/snapshots, GET /v1/exports/snapshots/{id}
Product API queries EST HTTPS/JSON Consumer frontend (product preview) Product service API: GET /v1/items…, /v1/merchants…, /v1/sources/{id}, POST /v1/shipping/evaluations
Evidence object storage EST R2 binding / S3 Resolver intake (write), resolver consumers and replay (read), export job (write export objects) Private R2 buckets: evidence-private, exports-private (PROP name)
Service databases EST SQL (Hyperdrive) Resolver Workers and consumers; outbox relay; product Worker Managed PostgreSQL via Hyperdrive (resolver DB)

What delivery promises, and what it does not

Synchronous calls return a status to the caller. The durable ones are the receipt, the decision commit, and the inbox commit. A 202 on capture means the receipt is stored. It does not mean the listing is identified.

Queues are at least once and unordered. Every delivery is checked in the product inbox. That check has three results. A duplicate or an older revision is ignored. A missing dependency is parked, and acknowledging that parked copy does not close the application gap. A ready revision is applied, and only then acknowledged. A dead letter does not close a gap either. SPK-02 has not tested this.

Two limits disagree and stay disagreed. Budget controls propose at most three queue deliveries for a job. The interface note proposes a dead letter after five attempts. This guide does not pick a winner.

Figure

What happens when an accepted event arrives twice, late, or before its dependency?

What happens when an accepted event arrives twice, late, or before its dependency? Every delivery is checked in the inbox. A duplicate or older revision is ignored, a missing dependency is parked, and a ready revision is applied. Acknowledging a parked event does not close the gap. A decision commits facts and outbox rows under the stream cursor lock. The relay publishes the committed row. The product inbox checks every delivery. A duplicate or an older revision is ignored after that check. A missing dependency is parked and may be acknowledged. The acknowledgment does not close the application gap. A ready revision is applied in the same transaction as its inbox row, and the queue is acknowledged after that commit. commit row publish deliver duplicate missing dependency ready PERSON Decision dec_0141 is one example DATABASE Resolver transaction Facts, cursor, outbox SERVICE Outbox relay Publishes after commit QUEUE · ASYNC Ingestion queue At least once SERVICE Product inbox Checks every delivery STATE Ignore Duplicate or older revision STATE Park Ack does not close the gap DATABASE Apply Ready, then acknowledge

Takeaway. Every delivery is checked in the inbox. A duplicate or older revision is ignored, a missing dependency is parked, and a ready revision is applied. Acknowledging a parked event does not close the gap.

Text equivalent

  1. A decision commits facts and outbox rows under the stream cursor lock.
  2. The relay publishes the committed row. The product inbox checks every delivery.
  3. A duplicate or an older revision is ignored after that check.
  4. A missing dependency is parked and may be acknowledged. The acknowledgment does not close the application gap.
  5. A ready revision is applied in the same transaction as its inbox row, and the queue is acknowledged after that commit.

Status. Proposed delivery rules (ADR-009 and the queue-continuity text). SPK-02 has not been run.

Small payloads

The walkthrough shows the Lumen upload, receipt, and decision. The accepted-change body below is generated from example 0 of the schema, including sequence and schema_version. Its published_at is an event timestamp. History epoch history-demo-1 is the fixture’s label for that stream, not a production epoch. evt_0142 remains listing_mapping.corrected. It is not the release.

Complete payload. Example 0 from product.accepted-change.v1.json. The identifiers are fictional fixtures, not a production trace.

{
  "event_id": "evt_0142",
  "tenant_id": "ten_primary",
  "catalog_id": "cat_primary",
  "sequence": 9,
  "type": "listing_mapping.corrected",
  "schema_version": "accepted-change/1",
  "aggregate": {
    "type": "listing_mapping",
    "id": "lst_lum_p5_1t_hs",
    "revision": 2
  },
  "dependencies": [
    {
      "type": "catalog_item",
      "id": "itm_ssd_p5_1t_hs",
      "revision": 1
    }
  ],
  "decision_id": "dec_0141",
  "evidence": [
    "cap_lum_0310"
  ],
  "observed_at": "2026-10-10T03:31:55Z",
  "accepted_at": "2026-10-11T09:12:20Z",
  "published_at": "2026-10-11T09:12:22Z",
  "payload": {
    "listing": "lst_lum_p5_1t_hs",
    "from": "itm_ssd_p5_1t",
    "item": "itm_ssd_p5_1t_hs"
  },
  "history_epoch": "history-demo-1"
}

Complete schemas: product.accepted-change.v1.json, resolution.job.v1.json, resolution.replay.v1.json, and openapi.yaml.

if_source_fetch · collector-source

Source fetch

EST Synchronous request

Caller · EST
Collector instance (ci_*) on Workers + Cron, Browser Run, a local process or a regional host
Receiver · EST
External source: merchant website, marketplace API or feed (src_*)
Protocol · EST
HTTPS to the source, from the collector's own egress. The resolver never fetches sources.
Authentication · PROP
Whatever the source requires (public pages, a partner API key held by the collector deployment). Source credentials never go to the resolver.
Authorization · PROP
The collector fetches only the targets in its registration (targets[]), and these must match the source grants in its token.
Idempotency · EST
GET-only. Re-fetching gives a new capture with a new captured_at, and identical bytes dedupe by sha256 at upload.
Retries · PROP
Retry 429/5xx with backoff and Retry-After. Report repeated failures in the heartbeat run report (if_heartbeat). Never retry 4xx except 408 and 429.
Delivery · EST
Request/response. execution_region and egress are recorded on the capture and are never treated as a market.
Tenant · PROP
The tenant is implicit from the collector's registration. The source sees no tenant information.
Timeouts · PROP
Connect 10 s, total 30 s per page (browser render 60 s).

Complete schema: OpenAPI, where this connection has an HTTP route. Otherwise the authoritative text is the field above and ADR-009 for queues.

Errors

  • 429 RATE_LIMITED — retry. Honor Retry-After
  • 503 UPSTREAM_503 — retry. Reported as run.error in the heartbeat
  • 404 NOT_FOUND — do not retry. Recorded as a run outcome; listing may be retracted after review

Example

Explanatory excerpt, not a complete payload.

{
  "method": "GET",
  "url": "https://lumenparts.example.com/catalog/ssd/strata-p5-1tb-hs",
  "headers": {
    "User-Agent": "PRW-collector/0.2.0 (+contact placeholder)"
  }
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 200,
  "media_type": "text/html",
  "bytes": 233904
}

Enrollment, one step at a time

Registration is how a collector instance is allowed to submit. An operator provisions the client first. The instance then asks the token service for a client-credentials token and calls register with that bearer token. The call records a lease. It does not start a machine, and it cannot pick a different tenant. Re-registering the same instance returns the existing lease. Cryptography for this path is simulated in the workbench.

Figure

How does a collector instance become allowed to submit?

How does a collector instance become allowed to submit? An operator provisions the client. The instance gets a client-credentials token, then registers. Registration records a lease. It does not start a machine or change the tenant. An operator provisions a per-instance client before any token is issued. The collector requests an OAuth2 client-credentials token from the token service. The token carries the stable instance, the worker, the tenant, and the source grants. POST /v1/workers/register upserts the instance and returns a lease. A later heartbeat renews that lease. Registration itself never starts a machine. provision token bearer upsert renew PERSON Operator provisions Client, worker, sources SERVICE Collector asks Client credentials EXTERNAL Token service Choice of provider is open SERVICE Register Bearer token, one instance DATABASE Lease recorded Same body returns the lease SERVICE Heartbeat later Renews. Does not grant more.

Takeaway. An operator provisions the client. The instance gets a client-credentials token, then registers. Registration records a lease. It does not start a machine or change the tenant.

Text equivalent

  1. An operator provisions a per-instance client before any token is issued.
  2. The collector requests an OAuth2 client-credentials token from the token service.
  3. The token carries the stable instance, the worker, the tenant, and the source grants.
  4. POST /v1/workers/register upserts the instance and returns a lease.
  5. A later heartbeat renews that lease. Registration itself never starts a machine.

Status. Proposed enrollment (collector registration). Cryptography, the identity provider, and TLS are simulated in the workbench, not verified here.

if_collector_bootstrap · collector-resolver

Collector registration / bootstrap

PROP Synchronous request

Caller · PROP
An operator provisions a per-instance machine client. The collector instance then registers itself.
Receiver · EST
Resolver intake Worker: POST /v1/workers/register
Protocol · PROP
(1) The operator creates a client in the admin UI or IdP and gets client_id and secret (or private_key_jwt). (2) The collector gets a token from the IdP token endpoint (OAuth2 client-credentials). (3) HTTPS/JSON POST /v1/workers/register with that bearer token.
Authentication · PROP
Collector OAuth2 client credentials with workers:register. Operator provisioning establishes worker, stable instance, tenant and source grants before token issuance. This request uses no operator cookie or CSRF headers; cryptography is simulated.
Authorization · PROP
worker_id must match the provisioned worker; instance_id and addressed instance must equal token sub (the stable instance, not client_id). targets must be a nonempty subset of provisioned source grants. Registration records capabilities and a lease; it does not create privileges or start a machine.
Idempotency · PROP
Registration is an upsert keyed by instance_id. Re-registering with the same body returns 200 with the existing lease.
Retries · PROP
Exponential backoff with full jitter (base 1 s, cap 5 min, max 8 attempts). 401 CREDENTIAL_REVOKED is not retried. Stop and alert the operator.
Delivery · EST
Request/response. Registration only records the instance. It never starts machines.
Tenant · PROP
Tenant and source grants come from the provisioned authenticated binding. Registration cannot select another tenant or expand grants. A source outside grants is denied with SOURCE_SCOPE_DENIED.
Timeouts · PROP
Token request 10 s; register 10 s.

Contract pointers

  • POST /v1/workers/register
  • securitySchemes.collectorOAuth2

Errors

  • 401 INVALID_TOKEN — do not retry. Unknown, expired or incorrectly bound token
  • 401 CREDENTIAL_REVOKED — do not retry. Revoked or superseded client
  • 403 MISSING_SCOPE — do not retry. workers:register absent
  • 403 INSTANCE_SCOPE_DENIED — do not retry. Different stable instance
  • 403 ATTRIBUTION_DENIED — do not retry. Different worker
  • 403 SOURCE_SCOPE_DENIED — do not retry. Target outside provisioned grants
  • 403 PERMISSION_DENIED — do not retry. Operator session is not collector registration
  • 404 NOT_FOUND — do not retry. Unknown server-side instance
  • 503 UNAVAILABLE — retry.

Example

Explanatory excerpt, not a complete payload.

{
  "worker_id": "cw_halden_us_browser",
  "instance_id": "ci_halden_fra_01",
  "version": "0.4.2",
  "runtime": "local-process",
  "runtime_kind": "long-running",
  "execution_region": {
    "country": "DE",
    "label": "Frankfurt regional host",
    "declared_by": "operator"
  },
  "egress": {
    "country": "DE",
    "verified": false
  },
  "targets": [
    "src_halden_us"
  ],
  "capabilities": [
    "browser.render",
    "html",
    "screenshot",
    "policy-pages"
  ]
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 201,
  "body": {
    "instance_id": "ci_halden_fra_01",
    "lease_seconds": 90,
    "heartbeat_url": "/v1/workers/ci_halden_fra_01/heartbeat",
    "granted": {
      "tenant_id": "ten_primary",
      "sources": [
        "src_halden_us"
      ]
    }
  }
}

if_artifact_upload · collector-resolver

Artifact upload

PROP Synchronous request, then object storage

Caller · PROP
Collector instance
Receiver · PROP
Resolver intake: POST /v1/artifacts/uploads, then the bytes go to the returned target (a presigned R2 URL, or PUT /v1/artifacts/uploads/{upload_id} streamed through the resolver)
Protocol · PROP
Two-step. Declare source_id, sha256, bytes and media_type over HTTPS/JSON. The resolver derives worker_id and instance_id from the authenticated collector, verifies source_id against its grants, and returns an immutable upload target. PUT bytes using the presigned capability or authenticated streamed endpoint; the latter resolves ownership from stored upload metadata. Verify the digest before marking stored; an existing tenant digest returns already_stored.
Authentication · PROP
Per Trust & tenancy recommendation: an OAuth2 client-credentials JWT (scope artifacts:write). The presigned target URL is a short-lived capability (PROP 300 s TTL), valid for one key and one content length.
Authorization · PROP
Require artifacts:write and a granted source_id. Derive tenant, worker and stable instance from authenticated context, never require client attribution fields. Conflicting supplied worker/instance fields are denied. A streamed PUT uses the stored upload owner and source; a rotated credential for the same stable instance retains access.
Idempotency · PROP
Natural key (tenant_id, sha256). A repeated declare returns the same ref. If the object exists it returns already_stored, and a repeated PUT of the same bytes is harmless.
Retries · PROP
Exponential backoff with full jitter (base 1 s, cap 5 min, max 8 attempts). Retry the PUT with the same upload_id until expires_at, then declare again. A digest mismatch (422 DIGEST_MISMATCH) is not retryable without re-reading the source bytes.
Delivery · PROP
Request/response. An artifact can be referenced by a capture only after it reaches status stored. A capture citing an unstored ref gets 422 ARTIFACT_NOT_STORED.
Tenant · PROP
The source_id selector must name a source in the authenticated tenant and provisioned grants. Object keys are derived from authenticated tenant + digest. Source outside grants: 403 SOURCE_SCOPE_DENIED; another tenant: 403 TENANT_SCOPE_DENIED; missing or conflicting attribution: 403 ATTRIBUTION_DENIED. Identical bytes in different tenants remain different objects.
Timeouts · PROP
Declare 10 s. PUT 120 s. The presigned URL expires after 300 s, so a slow upload re-declares.

Contract pointers

  • POST /v1/artifacts/uploads
  • PUT /v1/artifacts/uploads/{upload_id}

Errors

  • 200 already_stored — do not retry. Same tenant+sha256 already stored; skip PUT
  • 401 CREDENTIAL_REVOKED — do not retry.
  • 413 ARTIFACT_TOO_LARGE — do not retry. Above PROP limit
  • 422 DIGEST_MISMATCH — do not retry. Uploaded bytes do not hash to declared sha256
  • 503 STORAGE_UNAVAILABLE — retry.
  • 403 TENANT_SCOPE_DENIED — do not retry. Source belongs to another tenant
  • 403 SOURCE_SCOPE_DENIED — do not retry. Source is not granted
  • 403 MISSING_SCOPE — do not retry. artifacts:write absent
  • 403 ATTRIBUTION_DENIED — do not retry. Missing source selector or conflicting attribution
  • 403 INSTANCE_SCOPE_DENIED — do not retry. Another instance owns the streamed upload

Example

Explanatory excerpt, not a complete payload.

{
  "sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
  "bytes": 233904,
  "media_type": "text/html",
  "capture_hint": "cap_lum_0310",
  "mode": "presigned",
  "source_id": "src_lumen_web"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 201,
  "body": {
    "upload_id": "upl_0310_html",
    "status": "pending",
    "ref": "r2://evidence-private/ten_primary/a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
    "sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
    "target": {
      "method": "PUT",
      "url": "https://evidence-upload.example.com/presigned/upl_0310_html?sig=<placeholder>",
      "headers": {
        "Content-Type": "text/html",
        "Content-Length": "233904"
      }
    },
    "expires_at": "2026-10-10T03:36:55Z"
  }
}

if_capture_submit · collector-resolver

Capture submission

EST Synchronous request

Caller · EST
Collector instance
Receiver · EST
Resolver intake: POST /v1/captures
Protocol · EST
HTTPS/JSON. Returns a durable receipt, never a resolution result.
Authentication · PROP
Proposed: OAuth2 client credentials. Bound tenant, stable collector instance, worker and assigned sources; required resource:action scope checked on every request. Cryptography simulated in this workbench.
Authorization · PROP
Scope captures:write. source.source_id must be in the token's source grants. worker_id and instance_id must match the token binding.
Idempotency · EST
Idempotency-Key (worker_id:native_record_id:captured_at). Same key and same body → 200 with the existing receipt. Same key and a different body → 409 IDEMPOTENCY_CONFLICT. Dedupe window PROP 7 days.
Retries · EST
503 and network errors: resubmit with the same Idempotency-Key, honoring Retry-After. 409, 422, 401 and 403 are not retryable.
Delivery · EST
Request/response. On 202, intake commits the capture and enqueues resolution.job.v1 (refs only).
Tenant · PROP
Tenant and catalog come from the authenticated identity (token claims or operator membership). Any client-supplied tenant_id, catalog_id or source_id is only checked against those grants; it never grants access. Mismatch → 403 TENANT_SCOPE_DENIED.
Timeouts · PROP
Client total 15 s. Server commit budget 5 s.

Contract pointers

  • POST /v1/captures

Errors

  • 200 idempotent_replay — do not retry. Same key and body
  • 401 CREDENTIAL_REVOKED — do not retry. Token valid in form but client revoked
  • 403 TENANT_SCOPE_DENIED — do not retry. source_id not granted
  • 409 IDEMPOTENCY_CONFLICT — do not retry.
  • 422 VALIDATION_FAILED — do not retry. Nothing persisted
  • 422 ARTIFACT_NOT_STORED — do not retry. Ref not uploaded/verified
  • 503 STORAGE_UNAVAILABLE — retry. Retry-After

Example

The workbench points at another fixture instead of repeating a body: @capture. That pointer is not a payload.

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 202,
  "body": {
    "receipt_id": "rcpt_0310",
    "capture_id": "cap_lum_0310",
    "status": "stored",
    "received_at": "2026-10-10T03:32:01Z",
    "links": {
      "receipt": "/v1/receipts/rcpt_0310"
    }
  }
}

if_receipt_get · collector-resolver

Receipt lookup

EST Synchronous request

Caller · EST
Collector instance (its own receipts); operator dashboard (any receipt in the tenant)
Receiver · EST
Resolver: GET /v1/receipts/{id}
Protocol · EST
HTTPS/JSON
Authentication · PROP
Collector: OAuth2 client-credentials JWT (scope receipts:read). Operator: OIDC session (if_operator_session).
Authorization · PROP
Collectors read receipts owned by their stable instance_id, not the rotating OAuth client_id. Credential rotation preserves legitimate receipt access. Authenticated unknown receipt: 404 NOT_FOUND; another tenant: 403 TENANT_SCOPE_DENIED; same tenant/source but another instance: 403 INSTANCE_SCOPE_DENIED. Operators use their tenant-scoped viewer grant.
Idempotency · EST
Safe GET
Retries · PROP
Exponential backoff with full jitter (base 1 s, cap 5 min, max 8 attempts). Poll interval ≥ 30 s; prefer heartbeat-time batch checks.
Delivery · EST
Request/response. A receipt status of accepted never means applied.
Tenant · PROP
Tenant and catalog come from the authenticated identity (token claims or operator membership). Any client-supplied tenant_id, catalog_id or source_id is only checked against those grants; it never grants access. Mismatch → 403 TENANT_SCOPE_DENIED.
Timeouts · PROP
5 s

Contract pointers

  • GET /v1/receipts/{id}

Errors

  • 401 CREDENTIAL_REVOKED — do not retry.
  • 404 NOT_FOUND — do not retry. Unknown receipt
  • 403 TENANT_SCOPE_DENIED — do not retry. Different tenant
  • 403 INSTANCE_SCOPE_DENIED — do not retry. Another stable collector instance
  • 403 MISSING_SCOPE — do not retry. receipts:read absent

Example

Explanatory excerpt, not a complete payload.

{
  "method": "GET",
  "path": "/v1/receipts/rcpt_0310"
}

Recorded response

The workbench points at another fixture instead of repeating a body: @receipt:rcpt_0310. That pointer is not a payload.

if_heartbeat · collector-resolver

Heartbeat and run report

EST Synchronous request

Caller · EST
Collector instance. Long-running instances send liveness; scheduled instances send a report per invocation.
Receiver · EST
Resolver: POST /v1/workers/{id}/heartbeat
Protocol · EST
HTTPS/JSON
Authentication · PROP
Per Trust & tenancy recommendation: an OAuth2 client-credentials JWT (scope workers:heartbeat).
Authorization · PROP
The path {id} must equal the token's bound instance_id.
Idempotency · PROP
Keyed by (instance_id, at). A duplicate is recorded once.
Retries · PROP
Best effort: retry once after 5 s, then skip to the next interval. A missed heartbeat is visible as lease expiry, not data loss.
Delivery · EST
Request/response. Lease 90 s (DEC-05, OPEN thresholds).
Tenant · PROP
Tenant and catalog come from the authenticated identity (token claims or operator membership). Any client-supplied tenant_id, catalog_id or source_id is only checked against those grants; it never grants access. Mismatch → 403 TENANT_SCOPE_DENIED.
Timeouts · PROP
5 s

Contract pointers

  • POST /v1/workers/{id}/heartbeat

Errors

  • 401 CREDENTIAL_REVOKED — do not retry. Collector must stop
  • 403 TENANT_SCOPE_DENIED — do not retry. Path id ≠ token instance
  • 404 UNKNOWN_INSTANCE — do not retry. Register first

Example

Explanatory excerpt, not a complete payload.

{
  "instance_id": "ci_bazaarline_cf",
  "at": "2026-10-11T08:00:04Z",
  "run": {
    "started_at": "2026-10-11T08:00:00Z",
    "outcome": "failed",
    "error": {
      "code": "UPSTREAM_503",
      "retryable": true
    },
    "captures_submitted": 0
  }
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 200,
  "body": {
    "instance_id": "ci_bazaarline_cf",
    "consecutive_failures": 25,
    "last_success": "2026-10-09T06:00:41Z",
    "lease_expires_at": null
  }
}

if_operator_session · operator-resolver

Operator login and session

PROP Synchronous request

Caller · PROP
Operator browser (operator dashboard static assets on the admin origin)
Receiver · PROP
Admin origin (resolver administrative API) and the OIDC identity provider
Protocol · PROP
OIDC authorization-code flow with PKCE, then an HttpOnly, Secure, SameSite=Strict session cookie on the admin origin. HTTPS only.
Authentication · PROP
Per Trust & tenancy recommendation: an operator OIDC session. A Cloudflare Access application in front of the admin origin is optional defense in depth. See trust.html for P3's final recommendation.
Authorization · PROP
Roles per tenant: viewer, reviewer, operator_admin (P3 owns the role list). State-changing requests also need a CSRF token.
Idempotency · PROP
Not applicable to login. Decisions use expected_revision (if_review_decision).
Retries · PROP
On 401 SESSION_EXPIRED the UI re-runs login, then retries GET requests. POSTs are not replayed automatically.
Delivery · PROP
Browser redirect flow
Tenant · PROP
The session holds the operator's tenant memberships. The active tenant is chosen in the UI and must be one of them. Requests for other tenants get 403 TENANT_SCOPE_DENIED.
Timeouts · PROP
Session idle 30 min, absolute 12 h (P3 to confirm).

Contract pointers

  • securitySchemes.operatorOidc

Errors

  • 401 SESSION_EXPIRED — do not retry. Re-login
  • 403 TENANT_SCOPE_DENIED — do not retry. No membership in requested tenant
  • 403 FORBIDDEN — do not retry. Role insufficient

Example

Explanatory excerpt, not a complete payload.

{
  "method": "GET",
  "path": "/auth/login?return_to=/review",
  "note": "redirect to IdP authorize endpoint (placeholder)"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 302,
  "headers": {
    "Location": "https://idp.example.com/authorize?client_id=prw-admin&code_challenge=<placeholder>"
  }
}

if_review_read · operator-resolver

Review queue and case read

EST Synchronous request

Caller · EST
Operator dashboard
Receiver · EST
Resolver admin API: GET /v1/review, GET /v1/review/{case_id}
Protocol · EST
HTTPS/JSON
Authentication · PROP
Operator OIDC session (if_operator_session)
Authorization · PROP
Role viewer or higher. Evidence artifact links are short-lived signed reads.
Idempotency · EST
Safe GET. The case revision is returned for the optimistic check in decisions.
Retries · PROP
GET is safe to retry with backoff.
Delivery · EST
Request/response. Cases are durable rows, not queue messages.
Tenant · PROP
Only cases in the active tenant are returned. Shared-catalog (cat_primary) cases are visible to operators with membership in the catalog's owning tenant (P3 to confirm).
Timeouts · PROP
10 s

Contract pointers

  • GET /v1/review
  • GET /v1/review/{case_id}

Errors

  • 401 SESSION_EXPIRED — do not retry.
  • 403 TENANT_SCOPE_DENIED — do not retry.
  • 404 NOT_FOUND — do not retry. Unknown case

Example

Explanatory excerpt, not a complete payload.

{
  "method": "GET",
  "path": "/v1/review?status=awaiting_human&kind=identity_mapping"
}

Recorded response

The workbench points at another fixture instead of repeating a body: @review. That pointer is not a payload.

if_review_decision · operator-resolver

Review decision

EST Synchronous request, then a database transaction

Caller · EST
Operator dashboard (human reviewer)
Receiver · EST
Resolver admin API: POST /v1/decisions
Protocol · EST
HTTPS/JSON
Authentication · PROP
Operator OIDC session plus a CSRF token
Authorization · PROP
Role reviewer. Identity-creating decisions need reviewer and the case kind's permission. An LLM actor can never call this.
Idempotency · PROP
expected_revision (optimistic concurrency) plus an Idempotency-Key header (PROP, 24 h window). Facts and outbox rows commit in one transaction.
Retries · PROP
Retry only on network errors or 503, with the same Idempotency-Key. 409 STALE_CASE_REVISION means reload and re-check, never retry blindly.
Delivery · EST
Request/response. A 201 means accepted by the resolver. The product service has not applied it yet.
Tenant · PROP
The case must belong to the active tenant. The decision is recorded with the operator principal and tenant_id.
Timeouts · PROP
10 s

Contract pointers

  • POST /v1/decisions

Errors

  • 409 STALE_CASE_REVISION — do not retry. Reload
  • 403 FORBIDDEN — do not retry. Role
  • 403 TENANT_SCOPE_DENIED — do not retry.
  • 503 UNAVAILABLE — retry. Same Idempotency-Key

Example

Explanatory excerpt, not a complete payload.

{
  "case_id": "rc_0007",
  "expected_revision": 2,
  "action": "correct",
  "corrections": {
    "core:mpn": "CVN-SP5-1T0-HS",
    "storage:heatsink": true
  },
  "create_item": {
    "id": "itm_ssd_p5_1t_hs",
    "family": "fam_strata_p5"
  },
  "map_to": "itm_ssd_p5_1t_hs",
  "rationale": "Spec grid and title agree on integrated heatsink; JSON-LD sku is stale."
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 201,
  "body": {
    "decision_id": "dec_0141",
    "case_id": "rc_0007",
    "case_revision": 3,
    "outbox": [
      "evt_0141",
      "evt_0142"
    ]
  }
}

if_admin_read · operator-resolver

Admin operations (workers, decisions, improvements, dry run)

EST Synchronous request

Caller · EST
Operator dashboard
Receiver · EST
Resolver admin API: GET /v1/workers, GET /v1/decisions/{id}, GET /v1/improvements, POST /v1/resolve (dry run)
Protocol · EST
HTTPS/JSON
Authentication · PROP
Operator OIDC session
Authorization · PROP
Role viewer for GET requests. Role operator_admin for POST /v1/resolve (it reads evidence but writes nothing).
Idempotency · EST
GET is safe. A dry run has no side effects (writes: 0).
Retries · PROP
Safe to retry with backoff. A dry run writes nothing.
Delivery · EST
Request/response
Tenant · PROP
Results are filtered to the active tenant
Timeouts · PROP
10 s; dry run 30 s

Contract pointers

  • GET /v1/workers
  • GET /v1/decisions/{id}
  • GET /v1/improvements
  • POST /v1/resolve

Errors

  • 401 SESSION_EXPIRED — do not retry.
  • 403 FORBIDDEN — do not retry.

Example

Explanatory excerpt, not a complete payload.

{
  "method": "POST",
  "path": "/v1/resolve",
  "body": {
    "capture_id": "cap_bzl_0906",
    "record": "OFR-55120-NF",
    "adapter": "auto",
    "dry_run": true
  }
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 200,
  "body": {
    "dry_run": true,
    "adapter": "pa_bazaarline_offers@1.4.1",
    "writes": 0
  }
}

Replay, from the request to one message per capture

An operator asks for a replay. Dry run is the default. Apply mode needs a reviewer-approved improvement. The admin API writes one queue message per capture. Each message pins the adapter and the profile and keeps the original observed time. The consumer reads the stored bytes. It does not fetch the shop again. The improvement story itself is in Operations. This figure is only the hand-off.

Figure

What is handed off when an operator asks for a replay?

What is handed off when an operator asks for a replay? Dry run is the default. The API writes one queue message per capture. Each message pins the parser and keeps the original observed time. Apply mode waits for a reviewer. The operator calls the admin API. Dry run is the default. Apply mode is refused unless a reviewer has approved the improvement. The API fans out one resolution.replay.v1 message per capture. The consumer reads the pinned artifact and does not fetch the shop again. observed_at stays on the original capture. New claims still need the accept path. request fan out read compare dry run PERSON Operator request Dry run unless approved SERVICE Admin API Checks pins and tenant QUEUE · ASYNC One message each replay id and capture id OBJECT STORE Pinned artifact Stored bytes, not a new fetch STATE Compare claims observed time unchanged GATE Apply waits Reviewer approval required

Takeaway. Dry run is the default. The API writes one queue message per capture. Each message pins the parser and keeps the original observed time. Apply mode waits for a reviewer.

Text equivalent

  1. The operator calls the admin API. Dry run is the default.
  2. Apply mode is refused unless a reviewer has approved the improvement.
  3. The API fans out one resolution.replay.v1 message per capture.
  4. The consumer reads the pinned artifact and does not fetch the shop again.
  5. observed_at stays on the original capture. New claims still need the accept path.

Status. Proposed replay job. The Lumen improvement imp_0003 is a fictional draft. This is the message sequence, not a completed replay.

if_replay_job · operator-resolver

Improvement replay

PROP Synchronous request, then an asynchronous job

Caller · PROP
Operator dashboard (improvement loop)
Receiver · PROP
Resolver admin API: POST /v1/improvements/{id}/replays. The API fans out one resolution.replay.v1 message per capture to the internal queue.
Protocol · PROP
HTTPS/JSON in, then a queue message (Cloudflare Queues, PROP) to the replay consumer
Authentication · PROP
HTTP: operator OIDC session. Queue: platform producer/consumer bindings. There is no bearer token inside messages.
Authorization · PROP
Role operator_admin. mode=apply needs a reviewer-approved improvement. Dry run is the default.
Idempotency · PROP
job_id = replay_id:capture_id. The consumer inbox dedupes. Replays preserve the original observed_at and pin the adapter and profile versions.
Retries · PROP
At-least-once. No ordering guarantee is assumed. Consumers dedupe by message key in a durable inbox table and ack only after their DB transaction commits. After 5 failed deliveries (PROP) the message moves to the dead-letter queue. Retryable: storage or DB unavailable, LLM 429/5xx. Not retryable: adapter version missing, or capture not in tenant (sent straight to the DLQ).
Delivery · PROP
At-least-once. No ordering guarantee is assumed. Consumers dedupe by message key in a durable inbox table and ack only after their DB transaction commits. After 5 failed deliveries (PROP) the message moves to the dead-letter queue.
Tenant · PROP
Captures must belong to the active tenant. tenant_id and catalog_id are stamped by the API from the session, and the consumer re-checks them against the capture rows.
Timeouts · PROP
API 10 s. Consumer per message 60 s (verify against provider limit for queue consumer wall time).

Queue schema: resolution.replay.v1.json.

Contract pointers

  • POST /v1/improvements/{id}/replays
  • webhooks.resolutionReplay

Errors

  • 202 queued — do not retry.
  • 403 FORBIDDEN — do not retry. operator_admin required
  • 409 REPLAY_IN_PROGRESS — do not retry. Same improvement+pins already running
  • 422 UNKNOWN_ADAPTER_VERSION — do not retry.
  • DLQ resolution-replay-dlq — do not retry. After 5 attempts

Example

Complete payload. Example 0 from resolution.replay.v1.json. The identifiers are fictional fixtures, not a production trace.

{
  "schema_version": "resolution.replay/1",
  "job_id": "rpl_0003_1:cap_lum_0310",
  "replay_id": "rpl_0003_1",
  "improvement_id": "imp_0003",
  "tenant_id": "ten_primary",
  "catalog_id": "cat_primary",
  "capture_id": "cap_lum_0310",
  "artifacts": [
    {
      "artifact_id": "art_0310_html",
      "ref": "r2://evidence-private/ten_primary/a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
      "sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
      "media_type": "text/html",
      "bytes": 233904
    }
  ],
  "pins": {
    "adapter": "pa_lumen_specgrid@1.0.0",
    "profile": "np_storage@4",
    "identity_generation": 1,
    "definition_versions": {
      "storage:heatsink": 1
    }
  },
  "mode": "dry_run",
  "preserve_observed_at": true,
  "requested_by": "prn_operator_reviewer",
  "enqueued_at": "2026-10-11T10:00:00Z",
  "history_epoch": "history-demo-1"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "status": 202,
  "body": {
    "replay_id": "rpl_0003_1",
    "captures": 4,
    "enqueued": 4,
    "preserves_observed_at": true
  }
}

if_resolution_job · resolver-internal

Resolution job

PROP Asynchronous job

Caller · PROP
Resolver intake (producer, after the capture commits)
Receiver · PROP
Resolver queue consumers: extraction → normalization → identity. Each stage enqueues the next stage message.
Protocol · PROP
Queue (Cloudflare Queues, PROP), JSON message body
Authentication · PROP
Platform queue bindings. Only the resolver Worker can produce, and only the resolver consumer can consume. Messages carry no credentials.
Authorization · PROP
The consumer re-reads the capture row and checks that tenant_id and catalog_id in the message match it. A mismatch goes to the DLQ and raises a security alert.
Idempotency · PROP
job_id = capture_id:stage:attempt_group. The consumer writes job_id to its inbox table in the same transaction as its results. A duplicate is acked without side effects.
Retries · PROP
Retryable: R2 or DB unavailable, Hyperdrive timeouts, LLM 429/5xx (backoff with delay). Not retryable: schema invalid, artifact sha256 mismatch, tenant mismatch → DLQ immediately.
Delivery · PROP
At-least-once. No ordering guarantee is assumed. Consumers dedupe by message key in a durable inbox table and ack only after their DB transaction commits. After 5 failed deliveries (PROP) the message moves to the dead-letter queue.
Tenant · PROP
tenant_id and catalog_id are copied from the committed capture row, never from collector input.
Timeouts · PROP
Per message 60 s. Verify against provider limit for consumer wall-clock and CPU.

Queue schema: resolution.job.v1.json.

Contract pointers

  • webhooks.resolutionJob

Errors

  • retry STORAGE_UNAVAILABLE — retry. Delayed retry
  • DLQ SCHEMA_INVALID — do not retry.
  • DLQ TENANT_MISMATCH — do not retry. Security alert
  • DLQ DIGEST_MISMATCH — do not retry. Artifact bytes ≠ sha256

Example

Complete payload. Example 0 from resolution.job.v1.json. The identifiers are fictional fixtures, not a production trace.

{
  "schema_version": "resolution.job/1",
  "job_id": "cap_lum_0310:extract:1",
  "stage": "extract",
  "tenant_id": "ten_primary",
  "catalog_id": "cat_primary",
  "capture_id": "cap_lum_0310",
  "receipt_id": "rcpt_0310",
  "source_id": "src_lumen_web",
  "artifacts": [
    {
      "artifact_id": "art_0310_html",
      "ref": "r2://evidence-private/ten_primary/a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
      "sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
      "media_type": "text/html",
      "bytes": 233904
    }
  ],
  "pins": null,
  "enqueued_at": "2026-10-10T03:32:01Z",
  "attempt_group": 1,
  "trace_id": "trc_0310",
  "history_epoch": "history-demo-1"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "ack": "after consumer DB commit",
  "outcome": [
    "stage_completed",
    "review_case_opened",
    "ignored_duplicate"
  ]
}

if_llm_extract · resolver-providers

LLM extraction

PROP Synchronous request to an external provider

Caller · PROP
Resolver extraction consumer, when no adapter supports the capture or the adapter failed
Receiver · PROP
Model provider API (OPEN which provider)
Protocol · PROP
HTTPS/JSON to the provider, with structured-output prompt extract-listing@5
Authentication · PROP
A provider API key held as a resolver secret (Workers secret / secret store). It is never sent to collectors or the browser. P3 owns rotation.
Authorization · PROP
Outputs are proposals only. They can open or annotate review cases and can never accept facts. Inputs are untrusted (page text can carry prompt injection), so the model gets no tools and no credentials.
Idempotency · PROP
Keyed by (capture_id, artifact sha256, prompt version, model id). A cached result is reused on queue redelivery.
Retries · PROP
Retry 429/5xx with backoff, up to 3 times. A schema-invalid output is retried once with a repair prompt, then recorded as an extraction failure.
Delivery · PROP
Request/response inside a queue consumer
Tenant · PROP
One request per capture. No cross-tenant context in a prompt. The provider must not retain data or train on it (OPEN, contract term).
Timeouts · PROP
60 s per call

Complete schema: OpenAPI, where this connection has an HTTP route. Otherwise the authoritative text is the field above and ADR-009 for queues.

Errors

  • 429 RATE_LIMITED — retry.
  • 5xx PROVIDER_ERROR — retry.
  • invalid OUTPUT_SCHEMA_INVALID — do not retry. Recorded; human review

Example

Explanatory excerpt, not a complete payload.

{
  "model": "<provider model id>",
  "prompt_version": "extract-listing@5",
  "input": {
    "capture_id": "cap_lum_0310",
    "artifact_sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
    "text_excerpt": "<untrusted page text>"
  },
  "response_format": "json_schema:claims/1"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "claims": [
    {
      "attribute": "core:mpn",
      "value": "CVN-SP5-1T0-HS",
      "evidence_locator": "#specs tr:nth-child(3)"
    }
  ],
  "diagnostics": [
    "JSON-LD sku conflicts with spec grid"
  ]
}

if_llm_review · resolver-providers

LLM review advice

PROP Synchronous request to an external provider

Caller · PROP
Resolver review consumer
Receiver · PROP
Model provider API (OPEN which provider)
Protocol · PROP
HTTPS/JSON, prompt review-case@3
Authentication · PROP
Provider API key (resolver secret), as for if_llm_extract
Authorization · EST
Advisory only. LLM-dependent new identities or mapping changes need a human decision (DEC-02).
Idempotency · PROP
Keyed by (case_id, case revision, prompt version)
Retries · PROP
As for if_llm_extract
Delivery · PROP
Request/response inside a queue consumer
Tenant · PROP
Only the case's own evidence is sent
Timeouts · PROP
60 s

Complete schema: OpenAPI, where this connection has an HTTP route. Otherwise the authoritative text is the field above and ADR-009 for queues.

Errors

  • 429 RATE_LIMITED — retry.
  • invalid OUTPUT_SCHEMA_INVALID — do not retry. Case stays awaiting_human

Example

Explanatory excerpt, not a complete payload.

{
  "prompt_version": "review-case@3",
  "case_id": "rc_0007",
  "revision": 2,
  "evidence": [
    "<untrusted excerpts>"
  ]
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "recommendation": {
    "action": "correct",
    "rationale": "<model text>",
    "cited_evidence": [
      "cap_lum_0310"
    ]
  },
  "advisory": true
}

if_accepted_change · resolver-product

Accepted-change event

EST Asynchronous event

Caller · EST
Resolver outbox relay (reads committed outbox rows)
Receiver · EST
Product service inbox consumer
Protocol · PROP
Queue product.accepted-change.v1 (Cloudflare Queues, PROP), JSON body
Authentication · PROP
Platform queue bindings: resolver relay = producer, product Worker = consumer. No credentials in messages.
Authorization · PROP
The product service accepts events only for (tenant_id, catalog_id) pairs it is configured to project. Others go to the DLQ and raise an alert.
Idempotency · PROP
Dedupe on tenant/catalog/history_epoch/event_id. Committed sequence is a transactional per-stream cursor. Check epoch before inbox; old-history events are terminally ignored, never relabeled. Revision and dependencies decide application.
Retries · PROP
The relay retries publishing until the queue acks, then marks the row published_at. The consumer retries on DB unavailable. A parked event (missing dependency) is stored in the inbox and acked.
Delivery · PROP
Accepted-change committed positions are contiguous within (tenant_id, catalog_id, history_epoch). Writers allocate them with state and outbox under the transactional cursor lock; rollback consumes no position. Queue arrivals may be unordered, duplicated or temporarily missing, so consumers park dependencies and retrieve retained events to close gaps. Durable progress advances only through every processed position, including safely ignored stale revisions. A DLQ entry or an ACK of a parked event does not close a gap. Internal resolution/replay jobs use their own dedupe IDs and do not share this committed cursor. Commit projection changes, inbox outcomes, dependency parking and the greatest contiguous processed position together in the live product transaction. Persist/report progress monotonically to resolver retention control for the same tenant, catalog and history epoch only after that commit; an interrupted report conservatively retains extra events. The pruner may use only acknowledged durable progress. Shadow catch-up has a separate cursor and lease; it never advances the live cursor. Cutover atomically swaps serving generation and durable live cursor before releasing the rebuild lease.
Tenant · PROP
tenant_id and catalog_id are stamped from the outbox row (the resolver's own data). The product service never trusts payload fields for scope.
Timeouts · PROP
Consumer per batch 30 s

Queue schema: product.accepted-change.v1.json.

Contract pointers

  • webhooks.acceptedChange

Errors

  • ignored ignored_duplicate — do not retry. ack
  • ignored ignored_stale_revision — do not retry. ack
  • parked awaiting dependency — do not retry. stored then ack
  • retry DB_UNAVAILABLE — retry.
  • DLQ SCOPE_NOT_PROJECTED — do not retry. Unknown tenant/catalog

Example

The workbench points at another fixture instead of repeating a body: @event:evt_0142. That pointer is not a payload.

Recorded response

Explanatory excerpt, not a complete payload.

{
  "outcome": "applied",
  "projection": "listing_mapping:lst_lum_p5_1t_hs → rev 2"
}

if_projection_bootstrap · resolver-product

Projection snapshot export (bootstrap)

PROP Synchronous request, then object storage

Caller · PROP
Product service (new deployment, new catalog, or rebuild after projection loss)
Receiver · PROP
Resolver export API: POST /v1/exports/snapshots, GET /v1/exports/snapshots/{id}
Protocol · PROP
Request pinned repeatable-read snapshot over HTTP; renew lease; load immutable objects, read ordered retained tail, catch up to H, atomically cut over projection pointer and live cursor. Queue delivery alone is not a replay source.
Authentication · PROP
Per Trust & tenancy recommendation: an OAuth2 client-credentials JWT for the product service principal (scope exports:read).
Authorization · PROP
The product principal may export only the tenant and catalog pairs it is granted. Signed object URLs are scoped to one export.
Idempotency · PROP
Idempotency-Key on POST. The same key returns the same export_id. Loading is idempotent: the product upserts by aggregate revision.
Retries · PROP
Exponential backoff with full jitter (base 1 s, cap 5 min, max 8 attempts). An expired signed URL is fixed by re-GETting the manifest.
Delivery · PROP
Serialize writers: Lock the cursor row for tenant_id + catalog_id + history_epoch BEFORE changing exported state. Update the transactional counter, accepted rows and outbox in one transaction. Rollback consumes no position. A delayed writer blocks later commits in this stream. Pin retention: Under the retention-control lock shared by the pruner, create a renewable rebuild lease pinned after current pruned-through cursor P. Commit before opening the snapshot. Read one snapshot: REPEATABLE READ READ ONLY: read history_epoch, committed stream cursor W, and all accepted state including private/public releases and aggregate revisions in one snapshot. Never use max(outbox.sequence) or a PostgreSQL sequence. Publish export: Advance lease pin from P to W under retention coordination. Write checksummed export objects. Manifest becomes ready only after export completion and a valid lease check; abort on epoch change. Load shadow: Load the manifest into an isolated projection generation, with aggregate revisions and W as its starting contiguous applied position. Renew the lease throughout export/load/catch-up. Retrieve retained events: GET /v1/exports/events?after=W&lease_id=…&history_epoch=… returns ordered authorized stream events. The pruner cannot remove events after an active pin. A stale cursor or expired lease returns 410; restart from a new export. Catch up and cut over: Choose committed target H. Apply every position through H, retaining dedupe, revisions and dependency parking. Require no unresolved dependencies at/below H. Pause the local applier, atomically swap the serving generation and durable consumer cursor, resume after H, then release the lease. Advance live retention progress: Commit projection changes, inbox outcomes, dependency parking and the greatest contiguous processed position together in the live product transaction. Persist/report progress monotonically to resolver retention control for the same tenant, catalog and history epoch only after that commit; an interrupted report conservatively retains extra events. The pruner may use only acknowledged durable progress. Shadow catch-up has a separate cursor and lease; it never advances the live cursor. Cutover atomically swaps serving generation and durable live cursor before releasing the rebuild lease. Fence restored history: Pause writers, relay, consumers and public serving; issue a NEW epoch in a trusted control plane outside the restored database. Fence old workers; abandon old events without relabeling; initialize a new cursor and rebuild from restored state. Audit and terminally ignore old-epoch deliveries.
Tenant · PROP
Tenant and catalog come from the authenticated identity (token claims or operator membership). Any client-supplied tenant_id, catalog_id or source_id is only checked against those grants; it never grants access. Mismatch → 403 TENANT_SCOPE_DENIED.
Timeouts · PROP
POST 10 s. Poll until ready (export runs async). Object GET 120 s each.

Contract pointers

  • POST /v1/exports/snapshots
  • GET /v1/exports/snapshots/{id}

Errors

  • 202 pending — do not retry. Poll GET
  • 401 CREDENTIAL_REVOKED — do not retry.
  • 403 TENANT_SCOPE_DENIED — do not retry.
  • 410 EXPORT_EXPIRED — do not retry. Request a new export
  • 503 UNAVAILABLE — retry.

Example

Explanatory excerpt, not a complete payload.

{
  "tenant_id": "ten_primary",
  "catalog_id": "cat_primary",
  "aggregates": [
    "catalog_item",
    "listing_mapping",
    "merchant",
    "shipping_policy",
    "inventory_observation"
  ]
}

Recorded response

The workbench points at another fixture instead of repeating a body: @manifest. That pointer is not a payload.

if_product_query · consumer-product

Product API queries

EST Synchronous request

Caller · EST
Consumer frontend (product preview)
Receiver · EST
Product service API: GET /v1/items…, /v1/merchants…, /v1/sources/{id}, POST /v1/shipping/evaluations
Protocol · EST
HTTPS/JSON
Authentication · PROP
Public read with no user identity. Rate limiting is at the edge. An optional publishable API key per frontend is OPEN (P3).
Authorization · PROP
Use shared public serializers backed by an authorized, applied release snapshot. Item identity, published_revision and publication_revision are consistent across collection/detail/listings/inventory. Internal acceptance or projection application does not publish a record. Preserve allowed original URLs and observation times; never emit private evidence, capture/review/decision references or internal tags.
Idempotency · EST
Safe. Shipping evaluation is computed per request and stores nothing.
Retries · PROP
GET and shipping evaluation are side-effect-free. Retry 5xx once.
Delivery · EST
Release authorization snapshots public fields and accepted listing mappings. Application waits for item and mapping dependencies, then changes the release pointer and affected public listing assignments in one product transaction. Each listing has one public item owner; a later internal remap remains private until an applied release. published_revision is the released item revision; publication_revision identifies the release. No public serializer reads live internal mappings.
Tenant · PROP
Deployment binds ten_primary and tenant-private cat_primary. Shared catalogs plus overlays are a future alternative requiring a new decision.
Timeouts · PROP
5 s; shipping evaluation 5 s

Contract pointers

  • GET /v1/items
  • GET /v1/merchants
  • POST /v1/shipping/evaluations

Errors

  • 404 NOT_FOUND — do not retry.
  • 422 BASKET_SPANS_MERCHANTS — do not retry.
  • 422 INVALID_DESTINATION — do not retry.
  • 429 RATE_LIMITED — retry.

Example

Explanatory excerpt, not a complete payload.

{
  "method": "GET",
  "path": "/v1/items?class=cls_storage_ssd&attr[storage:capacity]=1 TB"
}

Recorded response

The workbench points at another fixture instead of repeating a body: @items. That pointer is not a payload.

if_object_storage · storage-db

Evidence object storage

EST Object storage

Caller · EST
Resolver intake (write), resolver consumers and replay (read), export job (write export objects)
Receiver · EST
Private R2 buckets: evidence-private, exports-private (PROP name)
Protocol · PROP
R2 conditional put using etagDoesNotMatch:*; filesystem temp envelope + fsync + exclusive atomic link. Port get returns the entire object as Uint8Array; streaming/ranged reads need a separate future port.
Authentication · PROP
Workers binding (no key material in code). Presigned URLs are signed by the resolver with an R2 API token held as a secret (P3 owns scope and rotation).
Authorization · PROP
The buckets are private. There is no public bucket access and no public r2.dev access. Only the resolver can write evidence; artifacts are immutable once stored.
Idempotency · PROP
Atomic create-if-absent commits bytes and immutable metadata together. Retry succeeds only when digest, size and media type agree. Metadata mismatch is 409.
Retries · PROP
Retry 5xx/network with backoff. A put is idempotent by content-addressed key.
Delivery · EST
Strongly consistent read-after-write is assumed. Verify against provider documentation.
Tenant · PROP
The tenant prefix is derived server-side. Reads check that the object's tenant prefix equals the caller's tenant.
Timeouts · PROP
Binding op 30 s

Complete schema: OpenAPI, where this connection has an HTTP route. Otherwise the authoritative text is the field above and ADR-009 for queues.

Errors

  • 409 IMMUTABLE_METADATA_CONFLICT — do not retry. Do not overwrite or reinterpret existing bytes
  • 503 STORAGE_UNAVAILABLE — retry. Retry same digest and metadata

Example

Explanatory excerpt, not a complete payload.

{
  "op": "putIfAbsent",
  "key": "evidence-private/ten_primary/a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
  "sha256": "a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192",
  "bytes": 233904,
  "media_type": "text/html"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "etag": "<etag>",
  "stored": true
}

if_db · storage-db

Service databases

EST Database transaction

Caller · EST
Resolver Workers and consumers; outbox relay; product Worker
Receiver · EST
Managed PostgreSQL via Hyperdrive (resolver DB). The product projection store is a separate database or schema (OPEN).
Protocol · EST
SQL (PostgreSQL wire protocol through Hyperdrive)
Authentication · PROP
Per-service database roles: resolver_app, resolver_relay, product_app. Credentials live in the Hyperdrive config, never in code.
Authorization · PROP
Least privilege. The relay can read the outbox and update published_at only. product_app has no access to the resolver schema. Row-level security by tenant_id (P3 recommends).
Idempotency · EST
Facts and outbox are written in one transaction. Consumer inbox keys are unique.
Retries · PROP
Retry serialization failures and connection resets up to 3 times. Never retry after an ambiguous commit without an idempotency check.
Delivery · EST
Transactional
Tenant · PROP
Every tenant-scoped row has tenant_id. The session sets app.tenant_id from the authenticated context for RLS. Synthetic tenants ten_synth_a and ten_synth_b exist only to demonstrate isolation.
Timeouts · PROP
Statement timeout 5 s (API), 30 s (consumers)

Complete schema: OpenAPI, where this connection has an HTTP route. Otherwise the authoritative text is the field above and ADR-009 for queues.

Errors

  • 40001 serialization_failure — retry.
  • 57014 statement_timeout — retry. Once
  • 42501 insufficient_privilege — do not retry. Role misconfig

Example

Explanatory excerpt, not a complete payload.

{
  "sql": "BEGIN; SELECT position FROM stream_cursor WHERE tenant_id=$1 AND catalog_id=$2 AND history_epoch=$3 FOR UPDATE; /* facts + outbox positions + cursor updated together */ COMMIT;",
  "role": "resolver_app"
}

Recorded response

Explanatory excerpt, not a complete payload.

{
  "rows": 1
}

Data and vocabulary