The Davi NFC Agent is a small program that runs on the machine with the card reader. It talks to the reader over PC/SC, accepts scans from phones and WebNFC browsers, and publishes everything it sees on a local WebSocket — so your application reads one stream and does not care which of those produced a tap.
It is a separate program with its own releases, not part of the Davi API. It
runs on your hardware, listens on localhost, and works on its own.
When you need one
A browser cannot talk to a USB card reader. Anything that reads or writes a physical card from a desktop needs something native in between, and that is what the agent is:
- A door or a kiosk reading cards to admit people to a session
- A counter looking somebody up from the card they tap
- Encoding — writing a Davi URL onto a new card before handing it over
You do not need it to use Davi. Every API in these docs works from a phone, a server or a browser with no reader anywhere. You need it when a physical card is part of the flow.
From a tap to Davi
A scanned tag's uid is the Davi card identifier. That is the whole join
between the two products: the agent gives you a uid, and Davi's identifier
tagged union takes it as { "type": "card", "value": uid }.
So a door is one WebSocket and one HTTP call:
const AGENT = "ws://localhost:9470/ws";
const DAVI = "https://api.davi.social/api/v1";
const SESSION_SLUG = "week-1";
const TOKEN = process.env.DAVI_ORG_TOKEN; // an org-scoped token
const ws = new WebSocket(AGENT);
ws.addEventListener("message", async (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== "tagData" || msg.payload.err) return;
const res = await fetch(`${DAVI}/sessions/${SESSION_SLUG}/admit`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: { type: "card", value: msg.payload.uid },
idempotency_key: `${SESSION_SLUG}:${msg.payload.uid}:${msg.payload.scannedAt}`,
proof_metadata: { method: "nfc_tap" },
}),
});
const admit = await res.json();
switch (admit.decision) {
case "admitted":
return show("Welcome in");
case "needs_confirm":
return show("No ticket — register first");
case "blocked":
return show(reasonText[admit.blocked_reason] ?? "Not admitted");
}
});
The agent needs no Davi credentials and never sees your token: it hands you a
uid, and the call to Davi is yours to make. Which means the door's credential
only has to be able to admit — see
Org Activities for the scopes, and
Membership Lifecycle if the card carries a tier.
One client at a time. The first connection claims the session and later ones are refused until it drops, so a second tab is not a second reader.
Where to go next
| Installation | Install it, and what your OS needs |
| Client API | The WebSocket your application speaks — scans, writes, capabilities |
| Device API | Feeding the agent from a phone, a browser or your own hardware |
| Errors and TLS | Error codes, and how devices trust the certificate |
| Custom Builds | Embedding the agent as a Go library |
The agent is open source under the MIT licence — dotside-studios/davi-nfc-agent.