16 Tests Passed. Production Still Failed Closed Incorrectly: What We Learned Building Evidence Governance in MyZubster
We have been building a small real-world pilot inside MyZubster around a surprisingly simple event:
one person gives kefir culture to another person by hand.
There is no payment.
There is no automatic blockchain transaction.
There is no assumption that participation is research consent.
And yet this small interaction forced us to confront a much larger engineering question:
How should a system decide which evidence may persist, which evidence may become a cryptographic commitment, and which evidence must never become public?
This week we moved that question from documentation into executable policy.
And then we discovered something more valuable than another green test suite:
our policy passed every test and still behaved incorrectly in the real runtime.
Here is what happened.
The pilot
The current MyZubster kefir flow models a physical handover between a donor and a recipient.
The lifecycle is deliberately explicit:
ACCEPTED
↓
HANDED_OVER
↓
RECEIVED
↓
RECORDED
The donor confirms the physical handover.
The recipient independently confirms receipt.
Only after those confirmations can the interaction become a recorded event.
The flow is free by design:
paymentRequired = false
Recording the handover also does not automatically mean recording something on a blockchain.
That distinction became increasingly important as we started designing a broader research and evidence-governance layer.
From "data" to evidence classes
We introduced an evidence classification model.
The initial classes are:
PUBLIC
RESTRICTED
PARTICIPANT_ONLY
EPHEMERAL
They represent different disclosure and persistence expectations.
For example, a participant-only handover may legitimately exist inside the system while still being inappropriate for public anchoring.
Ephemeral information should not silently become permanent evidence at all.
This led to an important architectural separation:
EVENT
↓
EVIDENCE CLASSIFICATION
↓
POLICY
↓
COMMITMENT ELIGIBILITY
↓
PUBLICATION / ANCHORING POLICY
A cryptographic hash does not magically make sensitive information safe.
And the ability to create a commitment does not imply permission to publish it.
Privacy should be a policy, not a comment
We implemented an evidence policy service and started enforcing it in the kefir handover path.
Among the rules:
missing classification → deny
PUBLIC → commitment may be prepared
PUBLIC + explicit permission → public anchoring may be allowed
RESTRICTED → private commitment possible, public anchoring denied
PARTICIPANT_ONLY → private commitment possible, public anchoring denied
EPHEMERAL → persistent commitment denied
The critical default is simple:
Missing classification must fail closed.
We also made new kefir handovers default to participant-only, non-research evidence.
Conceptually:
evidenceClass: PARTICIPANT_ONLY
purpose: kefir_hand_delivery_evidence
researchEligible: false
publicAnchoringAllowed: false
This is important because operational participation is not the same thing as research participation.
Using a marketplace does not equal research consent.
Giving or receiving kefir does not equal research consent.
Contributing code does not equal research consent.
Research participation requires a separate explicit process.
The tests were green
We added Node tests covering the policy.
The suite included cases such as:
✔ missing classification fails closed
✔ PUBLIC evidence can prepare a commitment
✔ PUBLIC evidence requires explicit permission for public anchoring
✔ RESTRICTED evidence may have a private commitment but cannot be publicly anchored
✔ PARTICIPANT_ONLY evidence may have a private commitment but cannot be publicly anchored
✔ EPHEMERAL evidence cannot become persistent evidence or commitment
The existing handover tests also passed:
✔ free kefir handover is mounted in the pilot API
✔ handover states advance only through explicit confirmations
✔ donor and recipient confirmations are separated
✔ free handover never implies payment or blockchain evidence
✔ kefir handover defaults to participant-only non-research evidence
✔ commitment preparation is governed by evidence classification
Final result:
tests 16
pass 16
fail 0
Everything looked correct.
It wasn't.
The legacy-data problem
Before considering the feature complete, we inspected existing handovers directly in MongoDB.
There were two legacy records without the new evidence classification.
One of them had already reached:
state = RECORDED
and had an existing blockchain commitment from an earlier version of the system.
This was the perfect test case.
According to the new policy, asking the system to prepare another commitment for this legacy record should fail:
classification = MISSING
↓
fail closed
↓
EVIDENCE_CLASS_REQUIRED
So we tested it against the running API.
Instead we got:
HTTP=200
The commitment preparation succeeded.
That was the first important failure.
MongoDB said MISSING. Mongoose said PARTICIPANT_ONLY.
We compared three ways of reading exactly the same document.
Raw MongoDB:
MISSING
Hydrated Mongoose document:
{
"evidenceClass": "PARTICIPANT_ONLY",
"purpose": "kefir_hand_delivery_evidence",
"disclosureAudience": [
"donor",
"recipient"
],
"researchEligible": false,
"publicAnchoringAllowed": false
}
Mongoose with .lean():
MISSING
That exposed the bug.
The legacy MongoDB document did not contain an evidence classification.
But the Mongoose schema had defaults for new handovers.
When the legacy record became a hydrated Mongoose document, those defaults made the record appear classified even though the classification had never been persisted.
Our policy wasn't failing.
We were giving the policy the wrong representation of reality.
The route effectively saw:
MongoDB:
classification = MISSING
↓ Mongoose hydration
Application:
classification = PARTICIPANT_ONLY
↓
canPrepareCommitment()
↓
ALLOWED
This distinction matters enormously in systems dealing with consent, privacy, authorization, provenance or evidence.
A schema default is not historical evidence.
A value synthesized by an ODM is not necessarily a value a participant actually supplied, approved or previously persisted.
Fixing the trust boundary
We changed the commitment gate so that the security decision uses the classification actually persisted in MongoDB.
Conceptually:
const rawEvidence =
await MarketplaceHandover.collection.findOne(
{ _id: handover._id },
{ projection: { evidenceClassification: 1 } }
);
const commitmentPolicy = canPrepareCommitment(
rawEvidence?.evidenceClassification
);
The hydrated document can still be useful for normal application behavior.
But for this specific policy decision, the important question is:
Was this classification actually stored?
If the answer is no, the system fails closed.
We also strengthened the regression test to ensure the route does not silently return to:
canPrepareCommitment(
handover.evidenceClassification
);
for this decision.
Again, the suite passed:
16/16
We restarted the service.
We tested the legacy handover again.
And it still returned:
HTTP=200
That led to the second bug.
We were restarting the wrong process
Our environment contained a Sepolia frontend/backend setup managed partly through PM2.
We had been restarting the apparent Sepolia process after changing the backend.
But then we inspected the process actually listening on port 5003.
It wasn't the PM2 process.
The live gateway was:
/usr/bin/node /root/myzubster/scripts/start-gateway-systemd.js
and its cgroup belonged to:
myzubster.service
So we had:
source code patched ✓
tests against source ✓
PM2 restarted ✓
actual gateway restarted ✗
The real API was being served by systemd.
This explained why our code and tests said one thing while the runtime continued doing another.
We restarted the actual service:
myzubster.service
The new process came online, connected to MongoDB and listened again on:
127.0.0.1:5003
Then we repeated the same runtime test.
The result we wanted
This time:
{
"success": false,
"code": "EVIDENCE_CLASS_REQUIRED",
"message": "La classificazione dell’evidenza non consente la preparazione del commitment."
}
And:
HTTP=409
Now the complete chain behaved as intended:
LEGACY MONGODB DOCUMENT
classification = MISSING
↓
read persisted classification
↓
canPrepareCommitment(undefined)
↓
EVIDENCE_CLASS_REQUIRED
↓
HTTP 409
That was our actual proof.
Not merely:
unit tests = green
but:
policy tests
+
legacy database state
+
real authentication
+
real participant authorization
+
real service process
+
real API request
=
runtime fail-closed proof
Evidence governance is becoming infrastructure
This work is part of a larger separation we are introducing in MyZubster.
We now distinguish between operational participation, contribution, research candidacy and research participation.
In simplified form:
PILOT PARTICIPANT
≠
CONTRIBUTOR
≠
RESEARCH CANDIDATE
≠
RESEARCH PARTICIPANT
Research consent must be explicit.
Candidate inclusion is not research consent.
Marketplace activity is not research consent.
A GitHub contribution is not research consent.
We have also started documenting a separate private-verifiability research direction around the question:
What is the minimum information required to verify an event without unnecessarily exposing participants, relationships, locations or activities?
That work currently remains a research/design track.
We explicitly document things that are not implemented, including:
Monero settlement integration: NOT IMPLEMENTED
Zero-knowledge prototype: NOT IMPLEMENTED
Private reputation system: NOT IMPLEMENTED
Formal university validation: NOT CLAIMED
That boundary is intentional.
Documentation should describe the system we actually have, not the system we hope people assume we have.
What we learned
The biggest lesson was not about MongoDB.
It was about evidence.
In an ordinary CRUD application, a schema default may simply be convenient.
In an evidence system, the difference between:
"value exists"
and:
"the application supplied a default because the value was missing"
can be a trust-boundary issue.
The same applies to consent.
The same applies to provenance.
The same applies to privacy classifications.
And the deployment lesson was equally useful:
A test suite proves properties of the code it executed. It does not prove that the process receiving real requests is executing that code.
We needed both.
What is still missing
There is plenty left to build.
The current work establishes policy and governance foundations, not a completed research infrastructure.
We still need stronger integration tests around persisted legacy records, explicit migration strategies for old evidence, clearer lifecycle management for evidence classifications, and further separation between private commitments and genuinely public evidence.
The private-verifiability work is even earlier.
Zero-knowledge proofs are not implemented.
Monero integration is not implemented.
Private reputation is not implemented.
Formal university validation is not claimed.
And our current research-participant registry remains deliberately empty until an appropriate explicit research-consent process exists.
Why the kefir pilot matters
A hand-delivered jar of kefir may sound like an unusually small thing to build infrastructure around.
That is precisely why it is useful.
The interaction is understandable:
one person offers something
another accepts
the donor hands it over
the recipient confirms receipt
the system records what happened
From there, difficult questions appear naturally.
Who is allowed to confirm each step?
What counts as evidence?
Who may see it?
Should it persist?
Can it be hashed?
Can that hash be made public?
Does participation imply research consent?
What happens to records created before those rules existed?
Those questions apply far beyond kefir.
The principle we're converging on
The direction can be summarized in one sentence:
Verifiable does not have to mean public.
And we can make it slightly stronger:
verification ≠ publication
participation ≠ research consent
schema default ≠ persisted evidence
commitment ≠ permission to anchor publicly
green tests ≠ verified runtime behavior
The next phase is to keep turning those distinctions into enforceable system properties rather than leaving them as documentation.
Because if privacy, consent and provenance matter, they should not depend on everyone remembering what a comment in the code once said.
Top comments (0)