The sampler is standing in a vineyard near Nemea.
It's 07:42, the fog hasn't lifted,
and the phone shows "no signal."
She works the core auger,
takes a sample from three points along the row,
snaps a photo, captures the GPS coordinates,
and taps "Save" inside the /collect route.
The sample has been collected — but the LIMS server doesn't know about it yet.
It will find out at 09:15, when the truck crests the ridge and reconnects to 4G.
That gap — anywhere from a few minutes to several hours, occasionally days —
is the trust window every offline-first system has to confront.
An ESYD assessor will ask: "When exactly did the sampling happen?"
— and the answer cannot be "whenever our server received it."
Every Sample row carries two separate timestamps in the database:
collectedAt (datetime(6) on the samples table) — the moment of observation,stamped from the sampler's device clock
when they tapped "Save" in the field.
This is the field that ISO 17025 §7.3.3(b) explicitly requires
("date AND time of sampling").
createdAt (@CreateDateColumn inherited from Common) —the moment the record reached the LIMS server
and the mutation succeeded. Stamped by the server's clock.
One timestamp is not enough.
If we kept only createdAt, we would lose the real observation moment
— the day's weather, the field conditions,
the simultaneity with other events become unrecoverable.
If we kept only collectedAt, we lose the audit trail of when the record actually entered the LIMS,
and an assessor could not tell
whether a record landed late or was edited after the fact.
ISO/IEC 17025:2017 §7.11.3 requires "protection and integrity" of the LIMS
— including a defensible record
of when each event actually occurred.
The (collectedAt, createdAt) pair satisfies three points of the clause at once:
— an assessor can see exactly how long the record took to reach the LIMS,
the third stamp receivedDate (the COLLECTED → RECEIVED transition
via markSampleReceivedAtLab) closes the custody chain.
In the sample-detail dialog, a "Sample Timestamp" block
above the status-history timeline shows both stamps side-by-side,
plus a "Sync delay" label
when the gap exceeds 5 minutes
(otherwise suppressed so walk-in samples stay clean).
The sync-status chip in the navbar shows in real time
how many records are still waiting to reach the server.
Any audit query that correlates samples with "what was happening that day"
— meteorological data, personnel shifts, consumable lots,
instrument calibration reports —
must join on collectedAt, not createdAt.
Otherwise a sample collected on Tuesday but synced on Thursday
will appear falsely correlated with Thursday's events.
This subtlety lives in the traceability queries — not the screens —
which is why it deserves to be written down explicitly here.