Defender is a policy and security service for the Zapstore Nostr relay. It acts as a decision layer between event ingestion and persistence, combining fast admission checks with asynchronous analysis to detect spam, scams, and malicious content (WIP).
As the relay evolved, basic whitelist and blacklist checks became insufficient. Trust decisions now depend on multiple factors: external reputation signals, repository validation, and delayed malware analysis.
Embedding this logic directly into the relay would make it complex, slow, and difficult to extend. Defender exists to separate these concerns, allowing trust and safety rules to evolve independently from the core relay while supporting both immediate decisions and deferred corrective actions.
The relay owns everything it can answer locally, cheaply, and without knowing anything about the outside world.
The defender owns everything that requires external knowledge, accumulated history, or expensive computation about an author or their content.
-
Control access to the relay
- Apply immediate admission decisions based on whitelist, blacklist, and reputation signals.
- Support integration with external scoring providers like Vertex.
-
Support asynchronous threat detection
- Run slower, deeper analysis such as malware detection and behavioral signals after ingestion.
- Continuously improve detection using new data sources and heuristics.
-
Enable reactive enforcement
- Issue post-publication actions such as revocation or deletion when malicious activity is detected.
- Maintain consistency between detected threats and relay state.
- Fast path decisions must be low-latency and resilient to external failures.
- Expensive checks are handled asynchronously and should not block ingestion.
- All decisions must be logged for auditability.
- The system should evolve without requiring changes to the relay core.
Make a .env file, customize it to your needs and fill in the required variables.
You can take a look at the .env.example for the list of supported variables with their default values.
To run the server or CLI locally without building a binary, cd into the relevant command directory and use go run .:
The server listens on localhost:8080 by default.
make release
make release REF=v1.2.3The binary is dist/defender-<ref>-<arch>, or dist/defender-dev-<arch> when REF is empty. REF is embedded as the version reported by GET /v1/health. The CLI is go build -o dist/defender-cli ./cmd/cli.
The defender-cli tool manages entity policies directly against the local database.
By default the CLI uses defender.db in the current directory. Override with the DATABASE_PATH environment variable.
Returns the service status, running version, and uptime.
Response:
Evaluates a Nostr event and returns an admission decision.
Request body — a JSON-encoded Nostr event:
{
"id": "...",
"pubkey": "...",
"created_at": 1700000000,
"kind": 1,
"tags": [],
"content": "...",
"sig": "..."
}Response:
{
"decision": "accept" | "reject",
"reason": "human readable explanation"
}Evaluates a Blossom blob upload and returns an admission decision.
Request body:
{
"pubkey": "...",
"hash": "...",
"size": 1234,
"type": "image/png"
}Response:
{
"decision": "accept" | "reject",
"reason": "human readable explanation"
}Returns all policies. Accepts optional query parameters to filter results:
?platform=nostr|github|gitlab|codeberg?status=allowed|blocked
Both filters can be combined: ?platform=github&status=blocked.
Response:
[
{
"id": "...",
"platform": "nostr" | "github" | "gitlab" | "codeberg",
"status": "allowed" | "blocked",
"reason": "...",
"added_by": "...",
"created_at": 1700000000
}
]Returns the policy for a specific entity.
Response: a single policy object as above. Returns 404 if no policy exists for the entity.
Creates or updates the policy for an entity.
Request body:
{
"status": "allowed" | "blocked",
"reason": "...",
"added_by": "..."
}Response: 204 No Content on success.
Removes the policy for an entity. Returns 204 No Content regardless of whether the policy existed.
{ "status": "ok", "version": "v1.2.3", "uptime": "3h14m22s" }