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?
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
- 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.
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.
| Connection | Status | Protocol | Caller | Receiver |
|---|---|---|---|---|
| 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?
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
- 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.
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?
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
- 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.
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?
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
- 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.
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
}