Skip to content
Understanding the platform

Core concepts

Five ideas explain most of the platform's behavior. Read these and the API reference stops holding surprises.

The hybrid write path

The platform splits writes by kind, not by client. Capture goes straight to Firestore — that's what makes offline work, because the database's own persistence layer queues and drains writes without anyone hand-rolling a sync engine. Everything else — analysis results, catalog mutations, user administration — goes through the REST API, which owns validation, cross-document transactions and authorization.

Security rules enforce the split rather than documenting it. A client that tries to write image_analysis or state on an image is rejected outright, including your own client.

The image lifecycle

An image moves through five states. Analysis is embedded on the image document rather than stored in a separate collection, so a client reading an image gets its analysis in the same snapshot.

started

created locally, not yet uploaded

uploaded

bytes in storage, analysis not yet canonical

finished

canonical analysis present

warning

present but degraded

error

failed terminally

Reconciliation

Both server inference and on-device inference land on the same PUT …/analysis. When a result arrives for an image that already has one, the server decides which is canonical, in this order:

  1. 1

    source: human always wins

    An operator corrected it; no model output overrides that. A later human submission replaces an earlier one.

  2. 2

    Higher model_version wins

    Which is why model_version must sort correctly — use zero-padded semver or an ISO date, never v1.10 next to v1.9.

  3. 3

    On equal version, server beats device

    The server model is the full-size one; the on-device model is a quantised approximation of it.

  4. 4

    On a full tie, later analyzed_at wins

The loser is kept with superseded: true — for audit, and because a device result the server later disagreed with is a useful training signal. The canonical analysis on an image is never superseded.

The response's accepted field reports the outcome. A device that submits a result and gets accepted: false should adopt the returned canonical analysis rather than retrying.

Corrections

Human corrections are sent as a delta, not as a re-serialised analysis. Two reasons: the individual edits are the training signal — this box was missed, this SKU was confused for that one — and a delta makes concurrent edits detectable.

base_analyzed_at carries the timestamp of the analysis the operator was looking at. If the canonical analysis has moved on, the server responds 409 instead of applying edits to a version nobody reviewed. Applying corrections marks the analysis source: human, pinning it against any later machine submission.

# Corrections are sent as a delta, not a re-serialised analysis:# the individual edits are the training signal.curl -X POST \  "https://api.theshelfpro.com/v1/tenants/acme/tasks/t_8f21c4\/images/img_44c9e0/analysis/corrections" \  -H "Authorization: Bearer $SHELFPRO_TOKEN" \  -H "Content-Type: application/json" \  -d '{    "corrected_by": "OPqA3nT7xVdKp2wZ",    "base_analyzed_at": "2026-08-16T09:14:24.118Z",    "corrections": [      {        "op": "relabel",        "object_index": 4,        "object": { "class_id": 40219, "class_name": "Gold 1.5L" }      },      { "op": "remove", "object_index": 9 }    ]  }'

Detector vs classifier

Two distinct results per object, and conflating them causes a recurring class of bug.

Detector

Is there a thing here, and what kind?

detection_id
detection_class
detection_score

Always present.

Classifier

Which SKU is it?

class_id
class_name
class_score

Absent for out_of_stock, and absent when the classifier declines.

An out_of_stock object is a gap on the shelf: it has a box and no product. Share-of-shelf denominators must include gaps; SKU counts must not.

Coordinates

Boxes are stored twice — normalized (0–1 doubles) and pixel (integers) — and only one of them is authoritative.

Normalized is the contract. It survives resizing, thumbnailing and re-encoding. The pixel fields are a convenience mirror computed against the analysis's width/height; any consumer that changes the image dimensions must recompute them rather than trust them.

Origin is top-left, (x1, y1) is the top-left corner and (x2, y2) the bottom-right — matching both the model output and Android's RectF.

Tracing

A 32-character hex trace_id is minted on the device at the shutter and never reissued — retries, offline syncs and corrections all keep the original. It rides on the image document, the storage object's metadata, the traceparent header and every log record along the way.

Filter the platform to one trace id and you get the complete life of a single shelf photo, in order, across every module. When an operator says “this one looks wrong,” that's the thread you pull.