Operating it
Discovery: who serves a domain
Answering "who serves domain X" rather than only "what key does this known
subscriber use". The console's Who serves section, and the ?domain=
filter behind it.
Status: implemented (issue #14).
Trust plane, then discovery plane
Until this landed, DeDi could answer one question: given a subscriber_id you
already have, what key does it sign with. That is a trust question, and it
is what signature validation needs.
It could not answer a discovery question. FindBecknSubscriber takes an id
you already know and returns one row (LIMIT 1); it exists for validation. The
query endpoint filtered on name, state and time, and never reached into the
payload — even though every participant record already carried
payload->>'domain'.
So the url we published in every record was read by nothing. It was in the
log, and it was decorative.
A filter, not a new endpoint
The obvious design is GET /dedi/domain/{domain}. We did not do that, and the
reason is in design.md §120 rather than in this implementation: attribute
filtering over payload fields — role, domain, city/coverage, status — is
specified there as "a namespaced extension backed by the JSONB GIN index",
required for gateway discovery. A separate endpoint would have invented a second
shape for a question the specification already places on the query path.
GET /dedi/query/{namespace}/{registry}?domain=retail
It is strictly additive. Without the parameter the endpoint behaves exactly as
before and still never reads the payload; only the presence of ?domain= opts a
caller into the extension. A spec-conformant client cannot be surprised by it.
With ?domain= the route accepts only domain and internal. Anything else,
a page_size or a state filter included, is a 400: this branch answers the
domain question alone, so a filter or page sent with it would otherwise be
ignored rather than applied. An empty domain is a 400 too.
The answer is confined to the namespaces in DEDI_WILDCARD_NAMESPACES (on a
node with no publisher keys and no list, every namespace), live records only,
with status SUBSCRIBED or absent, newest first, at most 100.
What comes back
A deliberately narrow projection — not the record:
{
"subscriber_id": "bpp.example.com",
"record_id": "beckn-testnet/subscribers.beckn.one/76EU7ofw…",
"url": "https://bpp.example.com/beckn",
"type": "BPP",
"digest": "632243e7…",
"state": "live",
"lookup_url": "/dedi/lookup/beckn-testnet/subscribers.beckn.one/76EU7ofw…"
}
Who this is, where to reach them, and what to check them with. The full record
is deliberately absent, and lookup_url is deliberately present: this list is
a starting point, not an answer to be trusted. This node assembled it, and
only the record and its inclusion proof say what was actually published. A
response that inlined everything would invite a caller to act on an unverified
list, which is the habit the whole project exists to break.
Two constraints that are not incidental
It is confined to the eligible namespaces. A discovery caller has no
subscriber_id it already believed in — that is the whole point — so without
the DEDI_WILDCARD_NAMESPACES allowlist, anyone able to publish on this node
could insert themselves as a destination for any domain. Trust lookups are
anchored by the id you brought with you; discovery has no such anchor and needs
the allowlist instead.
The registry in the path scopes the answer. The store searches every eligible namespace, which is what discovery means, but a caller asked about one registry and should not be handed another's participants.
Both shapes of domain resolve
A participant may declare one domain as a string or several as an array, and the
spec permits both. Migration 0008_payload_domain_index.sql adds two indexes
for that reason:
CREATE INDEX idx_le_payload_domain ON log_entries ((payload ->> 'domain'))
WHERE entry_type = 'record';
CREATE INDEX idx_le_payload_gin ON log_entries USING GIN (payload jsonb_path_ops)
WHERE entry_type = 'record';
Supporting only one form would have made a seed using the other silently invisible — present, correct, and returned by nothing.
Revocation gains its second meaning
This is the part worth caring about. Before discovery, revoking a participant meant they stopped being verifiable. Now it also means they stop being returned as a destination.
Those are different guarantees, and the second is the one an operator thinks they are getting when they revoke someone. A test worth running after any revocation:
curl -s "$DEDI/dedi/query/$NS/$REG?domain=$DOMAIN" | jq '.data.participants[].subscriber_id'
What this does not do
It does not make ONIX route through it. ONIX's router reads the destination straight out of the message it was handed:
// pkg/plugin/implementation/router/router.go:258
bppURI := getContextString(uriBody.Context, "bpp_uri", "bppUri", "receiverUri")
and the dediregistry plugin implements only RegistryLookup, hard-requiring
both subscriber_id and key_id. There is no discovery method for it to call.
DeDi can now answer the question; nothing in a stock Beckn deployment asks it.
Closing that needs a Beckn gateway plus upstream plugin changes. If the broadcast story needs demonstrating before then, a small standalone gateway that queries dedid and fans out is the cheap route.
A caveat about what is in the registry
Discovery returns whatever was published, which makes bad records visible in a
way trust lookups do not. On the demo network it immediately surfaced two:
participants registered with railway.internal URLs that no external router can
resolve, and one subscriber present twice under two record names, returned twice
and revocable only halfway.
Neither was a bug in this surface. Both were seeding bugs that had been invisible for as long as the only question anyone could ask was "what key does this known id use".