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.
Binaries are built using the Makefile. The build always targets a specific git tag. The script checks out that tag before compiling, so the binary is guaranteed to reflect a clean, tagged commit.
make defender # builds the server binary
make defender-cli # builds the CLI binary
make all # builds bothBy default, the latest tag is resolved automatically via git describe. You can also target a specific tag explicitly:
make defender TAG=v1.2.3Built binaries are placed in the build/ directory and named after the tag, e.g. build/defender-v1.2.3. The git tag is also embedded into the binary at compile time and exposed via the GET /v1/health endpoint.
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" }