Medusa Ledger · Storage SPEC
← Landing Concept API manual GitHub

Storage SPEC

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.

1. Operational DB vs ledger

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?

BUSINESS UNIT │ ┌─────────────┴─────────────┐ │ │ Operational database MEDUSA LEDGER │ │ Mongo / Postgres append-only │ │ current state evidence

Operational DB

Collections such as users, orders, payments, settlements. A row can be updated. Example:

{
  "paymentId": "P-100",
  "amount": 50000,
  "status": "PAID"
}

Medusa

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.

2. Where the chain lives

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:

Docker └── Medusa └── ledger/blocks

No operational Mongo is required to try the SPEC.

3. Node appends, Spring verifies

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.

COMPANY │ ┌─────────┴─────────┐ │ │ Node service Spring service (Payments) (Accounting) │ │ └─────────┬─────────┘ │ MEDUSA │ Ledger

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" }
“Node emits, Spring verifies” does not mean Node → Mongo → Spring → Mongo. It means both clients know append() and verify() against the same chain.

4. Payment flow (state, then evidence)

When a customer pays, the application still writes operational state first, then appends cryptographic evidence. Two records exist on purpose:

PAYMENT │ ▼ Operational DB status = PAID │ ▼ MEDUSA │ ▼ BLOCK #42 SHA-256(…)
// 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.”

5. The ledger is not a full DB copy

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.

6. payloadHash without sensitive fields

A stricter option stores only a hash of the operational document. Medusa never needs the card number or customer name:

In the ERP / Mongo

{
  "_id": "PAY-100",
  "customer": "…",
  "card": "…",
  "amount": 50000,
  "currency": "COP",
  "status": "PAID"
}

In Medusa

{
  "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.

SPEC v0.1: 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+.

7. Hash-linked chain vs public anchor

The OSS ledger is a hash-linked chain. That is not the same as a public blockchain network.

YOUR APP ├── Operational DB └── Medusa Ledger ├── Block ├── Block └── Block ← hash-linked (OSS) Medusa Ledger │ periodically (optional, Cloud) ▼ External Anchor │ ▼ Public blockchain / timestamp service

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.

LayerQuestion it answers
Operational DBCurrent business state
Medusa hash chainRecorded facts, locally verifiable
Public anchor (optional)Third-party timestamp of a tip hash

8. One chain per business unit

Do not centralize every tenant into one OSS file. Each unit keeps its operational database and its own ledger:

MEDUSA SPEC │ ┌─────────────┼─────────────┐ │ │ │ Unit A Unit B Unit C │ │ │ Ledger Ledger Ledger │ │ │ Mongo Postgres Mongo

A platform can later query or verify those ledgers. The OSS promise stays: one chain per unit, data on that unit’s disk.

9. Three storage modes

ModeWhoWhere the chain lives
Embeddednpm install @medusa-ledger/core./ledger/blocks/*.json (FileStore)
SidecarDocker / Kubernetes / any languagecontainer volume + HTTP API
CloudP2L n-1-n / Medusa Cloudmanaged store per tenant

Embedded (Node)

Node application └── Medusa SDK └── ./ledger
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());

Sidecar (Docker / K8s)

Any application │ ▼ localhost:8080 │ ▼ Medusa container │ ▼ Ledger volume
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 (managed)

Medusa Cloud │ ┌──────────┼──────────┐ │ │ │ Tenant A Tenant B Tenant C │ │ │ Ledger Ledger Ledger

Cloud is the commercial product: backups, observability, payment anchors, multi-unit administration. The OSS layout stays local:

my-app/
├── database/     ← operational
└── medusa/
    └── ledger/   ← evidence

10. append() / verify() protocol

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.

MEDUSA LEDGER ▲ │ ┌────────┴────────┐ │ │ Node service Spring service │ │ append() verify()
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.

Install Medusa with Docker or NPM, keep the operational database beside medusa/ledger/, and verify from any stack that speaks the protocol — without a central Medusa database.