Operational DB
Collections such as users, orders, payments, settlements. A row can be updated. Example:
{
"paymentId": "P-100",
"amount": 50000,
"status": "PAID"
}
Operational data and cryptographic evidence are two different stores. Medusa does not replace MongoDB, PostgreSQL, or the ERP. It records facts that can be verified later — on disk the developer owns.
Your data stays yours. Your ledger stays yours. Medusa provides the protocol and verification.
A business unit already has a database. That store answers what is the current state. Medusa answers a different question: which facts were recorded, and can they be shown unaltered?
Collections such as users, orders, payments, settlements. A row can be updated. Example:
{
"paymentId": "P-100",
"amount": 50000,
"status": "PAID"
}
An event plus hashes. History is not rewritten in place:
PAYMENT_RECEIVED amount: 50000 timestamp: … hash: ABC… previousHash: XYZ…
MongoDB (or any ERP store) is state. Medusa is evidence. That split is the product, not an implementation detail.
The ledger does not have to sit in the same MongoDB as the application. The Core must not require that database. OSS storage is independent:
medusa/ │ ├── ledger/ │ ├── blocks/ │ │ ├── 000001.json │ │ ├── 000002.json │ │ └── … │ └── metadata.json
FileStore is the v0.1 default. SQLite, PostgreSQL, or a later distributed backend can hold the same protocol. A Docker demo can boot with only:
No operational Mongo is required to try the SPEC.
Two services can share one ledger without sharing one database. Node does not write Spring’s Mongo, and Spring does not open Node’s collections. Both speak the Medusa protocol.
Node records PAYMENT_RECEIVED. Spring later asks whether that event exists. Medusa answers: event present, hash valid, chain valid. Spring never enters the payments database.
// Node — append
await ledger.append({
eventId: "PAY-100",
eventType: "PAYMENT_RECEIVED",
payload: { amount: 50000, currency: "COP" }
});
// Spring — verify (HTTP sidecar in v0.1)
GET http://localhost:8080/api/verify
→ { "valid": true, "blocks": 3, "message": "Ledger integrity verified" }
When a customer pays, the application still writes operational state first, then appends cryptographic evidence. Two records exist on purpose:
// 1) State — your app / ERP
db.payments.updateOne(
{ _id: "P-100" },
{ $set: { status: "PAID", amount: 50000 } }
);
// 2) Evidence — Medusa
await ledger.append({
eventId: "PAY-100",
eventType: "PAYMENT_RECEIVED",
aggregateId: "ORDER-900",
payload: { amount: 50000, currency: "COP" }
});
Operational store: “payment P-100 is PAID.” Ledger: “event PAYMENT_RECEIVED occurred, and this is the hash proof.”
Do not dump the entire operational document into Medusa. Register only what later proof requires:
{
"eventId": "PAY-100",
"eventType": "PAYMENT_RECEIVED",
"aggregateId": "ORDER-900",
"timestamp": "2026-08-14T12:00:00.000Z",
"amount": 50000,
"currency": "COP"
}
That is a fact, not a replica of the ERP schema. Users, cards, addresses, and internal flags stay in the business database.
A stricter option stores only a hash of the operational document. Medusa never needs the card number or customer name:
{
"_id": "PAY-100",
"customer": "…",
"card": "…",
"amount": 50000,
"currency": "COP",
"status": "PAID"
}
{
"eventId": "PAY-100",
"eventType": "PAYMENT_RECEIVED",
"aggregateId": "PAY-100",
"payloadHash": "a91f8…",
"timestamp": "…"
}
const crypto = require("crypto");
const canonical = JSON.stringify(doc, Object.keys(doc).sort());
const payloadHash = crypto.createHash("sha256").update(canonical).digest("hex");
await ledger.append({
eventId: "PAY-100",
eventType: "PAYMENT_RECEIVED",
payloadHash
});
Later verification: the document shown today hashes to the same digest that was sealed when the event was appended — without Medusa holding PII.
payloadHash = SHA256(canonicalJSON(payload)). Object keys sorted lexicographically. Node Core is the reference hash implementation; Spring and .NET talk HTTP to the sidecar until native hashes land in v1.1+.The OSS ledger is a hash-linked chain. That is not the same as a public blockchain network.
Local proof: SHA-256 + previousHash. External proof: an optional later anchor. v0.1 documents the hook; it does not require a public chain to be useful.
| Layer | Question it answers |
|---|---|
| Operational DB | Current business state |
| Medusa hash chain | Recorded facts, locally verifiable |
| Public anchor (optional) | Third-party timestamp of a tip hash |
Do not centralize every tenant into one OSS file. Each unit keeps its operational database and its own ledger:
A platform can later query or verify those ledgers. The OSS promise stays: one chain per unit, data on that unit’s disk.
| Mode | Who | Where the chain lives |
|---|---|---|
| Embedded | npm install @medusa-ledger/core | ./ledger/blocks/*.json (FileStore) |
| Sidecar | Docker / Kubernetes / any language | container volume + HTTP API |
| Cloud | P2L n-1-n / Medusa Cloud | managed store per tenant |
const { MedusaChain, FileStore } = require("@medusa-ledger/core");
const chain = new MedusaChain(new FileStore("./ledger"), { autoSeal: true });
await chain.appendEvent({
eventId: "order-001",
eventType: "ORDER_CREATED",
payload: { amount: 50000, currency: "COP" }
});
console.log(await chain.verifyChain());
docker compose up --build
curl -X POST http://localhost:8080/api/events \
-H "Content-Type: application/json" \
-d '{"eventId":"order-001","eventType":"ORDER_CREATED","payload":{"amount":50000}}'
Cloud is the commercial product: backups, observability, payment anchors, multi-unit administration. The OSS layout stays local:
my-app/
├── database/ ← operational
└── medusa/
└── ledger/ ← evidence
Every stack implements the same two verbs. Each service may keep its own database and its own job (payments vs accounting). The shared contract is the ledger protocol, not a shared Mongo.
POST /api/events
{
"eventId": "order-001",
"eventType": "ORDER_CREATED",
"payload": { "customerId": "customer-42", "amount": 50000, "currency": "COP" }
}
GET /api/verify
{ "valid": true, "blocks": 3, "message": "Ledger integrity verified" }
# Tamper a stored amount →
{ "valid": false, "block": 2, "reason": "Hash mismatch" }
Idempotence: the same eventId must not create a second block. Canonical event → canonical JSON → SHA-256 → block → chain. See SPEC.md for hash rules. Node is the reference implementation in v0.1; Spring Boot and .NET clients call the Docker sidecar over HTTP.
medusa/ledger/, and verify from any stack that speaks the protocol — without a central Medusa database.