Start here

Quickstart

From nothing to a signed, provable record in about five minutes. You need Docker, curl and jq. No Go toolchain and no source checkout: everything runs from the published image flywheelai/dedi-node.

Tags are latest, main, and sha-<commit> for an exact build. Pin a sha- tag for anything you care about; latest moves.

1. Start Postgres and a node

docker network create dedi-net
docker run -d --name dedi-pg --network dedi-net \
  -e POSTGRES_USER=dedi -e POSTGRES_PASSWORD=dedi -e POSTGRES_DB=dedi \
  postgres:16-alpine
until docker exec dedi-pg pg_isready -h 127.0.0.1 -U dedi -q; do sleep 1; done

docker run -d --name dedi-node --network dedi-net -p 8080:8080 \
  -e DATABASE_URL='postgres://dedi:dedi@dedi-pg:5432/dedi?sslmode=disable' \
  -e DEDI_ORIGIN=localhost/log \
  flywheelai/dedi-node:latest
until curl -sf localhost:8080/healthz >/dev/null; do sleep 1; done

DATABASE_URL and DEDI_DB_URL both work; DEDI_DB_URL wins if both are set. There is no key to generate: on first start the node mints its Ed25519 identity key and keeps it in its own database, so it survives restarts. It prints the public half on every boot:

docker logs dedi-node 2>&1 | grep -A1 'verifier key'

That verifier key is what anyone checking your checkpoints, or witnessing your node, needs. DEDI_ORIGIN names your log; it is the first line of every checkpoint, so give a real node its real domain.

2. Look at it

curl -s localhost:8080/healthz | jq .
curl -s localhost:8080/dedi/log/checkpoint
curl -s localhost:8080/dedi/lookup/demo | jq .

The checkpoint is a signed note: origin, tree size, Merkle root, signature. The lookup is a 404 in the standard's error envelope, because nothing has been published. A fresh node is read-only, and that is the intended starting state: there are no write routes until you configure a publisher key, so /admin answers 404, not 401.

Open http://localhost:8080/ in a browser for the node's own pages.

3. Open the write plane

Mint a publisher key for one namespace. The private half stays in a file you keep; the node is only ever given the public half.

KEYS=$(docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/w" -w /w \
  flywheelai/dedi-node:latest pubkeygen -kid op-1 -namespace demo \
  | sed -n 's/^DEDI_PUBLISHER_KEYS=//p')
echo "$KEYS"

It writes publisher.key and prints a line DEDI_PUBLISHER_KEYS=op-1:demo:<base64>, which the command above keeps in $KEYS. Restart the node with it and an admin password:

export DEDI_ADMIN_PASSWORD=change-me
docker rm -f dedi-node
docker run -d --name dedi-node --network dedi-net -p 8080:8080 \
  -e DATABASE_URL='postgres://dedi:dedi@dedi-pg:5432/dedi?sslmode=disable' \
  -e DEDI_ORIGIN=localhost/log \
  -e DEDI_PUBLISHER_KEYS="$KEYS" \
  -e DEDI_ADMIN_PASSWORD="$DEDI_ADMIN_PASSWORD" \
  flywheelai/dedi-node:latest
until curl -sf localhost:8080/healthz >/dev/null; do sleep 1; done

The identity key is in the database, so the node comes back as the same node.

Two gates now guard every write, and they answer different questions. The admin password (HTTP Basic, user admin unless DEDI_ADMIN_USER says otherwise) decides who may reach /admin at all. The publisher signature decides who is writing and whether that key may write to this namespace, and it is what the log records as created_by: publisher:op-1. Neither replaces the other.

Set DEDI_WILDCARD_NAMESPACES only if this node serves the Beckn ONIX registry lookup; see configuration. Without it the node starts with that lookup switched off.

4. Make a signed write

dedid sign prints the headers that authenticate one request. It signs the method, path, body, timestamp and precondition, so it runs once per request. This shell function signs a request with the image and sends it:

send() {  # send METHOD PATH BODYFILE [-create | -if-match TAG]
  docker run --rm -v "$PWD:/w" -w /w flywheelai/dedi-node:latest \
    sign -key publisher.key -kid op-1 -method "$1" -path "$2" -body "$3" "${@:4}" > headers.txt
  curl -s -u "admin:$DEDI_ADMIN_PASSWORD" -X "$1" "localhost:8080$2" \
    -H @headers.txt -H 'Content-Type: application/json' --data-binary @"$3"
}

Create a namespace, a registry in it, and a record in that:

echo '{"payload":{"description":"my first namespace"}}' > ns.json
echo '{"payload":{"description":"people we know"}}'     > reg.json
echo '{"payload":{"name":"Alice","url":"https://alice.example"}}' > alice.json

send PUT  /admin/namespaces/demo                 ns.json  -create | jq -c .data
send PUT  /admin/namespaces/demo/registries/people reg.json -create | jq -c .data
send POST /admin/namespaces/demo/registries/people/records/alice/publish alice.json -create | jq -c .data

-create means "this must not exist yet" (If-None-Match: *). Every write that replaces something must say which version it replaces, so a replayed or racing write fails with 412 instead of silently overwriting. To change Alice, pass the current version_tag:

tag=$(curl -s localhost:8080/dedi/lookup/demo/people/alice | jq -r .data.version_tag)
echo '{"payload":{"name":"Alice","url":"https://alice.example/v2"}}' > alice.json
send POST /admin/namespaces/demo/registries/people/records/alice/publish alice.json -if-match "$tag" | jq -c .data

5. Read it back, with proof

curl -s 'localhost:8080/dedi/lookup/demo/people/alice?proof=inclusion' | jq .
curl -s  localhost:8080/dedi/versions/demo/people/alice | jq .data.versions

The proof block carries the leaf, its index, the audit path, and the signed checkpoint it verifies against. http://localhost:8080/verify checks it in your browser with no help from the node. Every earlier version stays resolvable with ?version_id=<id> from the versions list.

Clean up

docker rm -f dedi-node dedi-pg && docker network rm dedi-net
rm -f publisher.key headers.txt ns.json reg.json alice.json

pubkeygen refuses to overwrite an existing publisher.key, so remove it before running the quickstart again.

Next

← All documentation