Several contracts existed and were tested in isolation while nothing actually used them. Reported as complete previously; this corrects that. - storage was injected and never referenced. Adds persist/hydrate, hydration before querying relays, and last-known-good state with a stale flag when a relay is unreachable, which is what makes the characterized cache-fallback behavior real rather than reduction-level only - signature verification existed as a tested helper the load path never called. Now applied through an injected verifier before state becomes authoritative, with rejection counts and an enabled/disabled readout in diagnostics - trusted mute lists had no subscription flow, so the store never populated in a real deployment. Subscribes by author, chunked - community source references decoded but were never resolved. Curator lists are now fetched and merged with a source marker, still capability-checked - root-published policy was announced and discarded. Now applied, accepted only from the root, and a malformed document leaves the working policy intact - adds import/export and capability queries Two design corrections found while wiring: - the cache key hashed the entire roster, so it rotated whenever a moderator was added and the cache was discarded. Now derived from root identity alone. - the root administrator is a constructor option: it is deployment config, and hydration could not otherwise find its own cache, since the key derives from the very fingerprint the cache would have supplied. 634 tests; lint, typecheck, and the declaration build clean.
nostr-governance
A headless, application-agnostic governance engine for Nostr: moderation, access control, and trust-graph policy, with no opinion about what your application is.
The engine carries no thresholds of its own. Applications supply a policy definition; the engine supplies precedence, aggregation, composition, and explanation. That separation is what lets one engine serve a video app and a marketplace without either leaking into the other.
Why it looks like this
Four independent decision dimensions. A governance decision is not a
hidden boolean. It is four answers:
| Dimension | Values |
|---|---|
ranking |
normal → downrank (with weight) |
visibility |
allow → warn → restrict → hide → deny |
interaction |
allow → require-explicit-action → deny |
transaction |
allow → require-review → deny |
A marketplace listing can be visible to its seller, downranked in discovery, inspectable behind a warning on its detail page, and blocked at checkout — all from one evaluation. Collapsing that into one boolean is what forces applications to reimplement policy.
Composition is ladder-maximum per dimension, which makes it commutative and associative: the order decisions arrive in cannot change the result.
Capabilities resolve at merge time, not ingest time. Revoking a moderator's role drops their contributions from effective state immediately, without replaying or rewriting anything.
Trust seeds are a fallback, not an addition. Once a viewer has real contacts, configured seeds stop contributing — following nobody must not be equivalent to following the operator's seed list.
Packages
| Package | Purpose |
|---|---|
@nostr-governance/core |
Pure evaluation. No I/O, no browser globals, no dependencies. |
@nostr-governance/nostr |
Codecs: canonical v1, NIP-56, NIP-51, legacy lists, bech32. |
@nostr-governance/runtime |
Stores, transport orchestration, commands. |
@nostr-governance/testing |
Conformance harness. |
@nostr-governance/bitvid-compat |
Reference application profile for the characterization corpus. |
Quick start
import { createPolicyDefinition, evaluateTarget } from "@nostr-governance/core";
import { createGovernanceRuntime, createMemoryTransport } from "@nostr-governance/runtime";
const policy = createPolicyDefinition({
id: "my-app",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
reports: {
spam: { downrank: 1, restrict: 3, hide: 8 },
malware: { hide: 1, interactionDeny: 1 },
},
mutes: { default: { downrank: 1, hide: 20 } },
muteWindowSeconds: 60 * 24 * 60 * 60,
},
},
});
const runtime = createGovernanceRuntime({
applicationId: "my-app",
namespace: "myapp",
transport: myRelayAdapter,
policy,
now: () => Math.floor(Date.now() / 1000),
});
runtime.setViewer(viewerPubkey);
runtime.trust.setContacts(followList);
await runtime.loadAdministrativeState();
const decision = runtime.evaluate({ type: "user", pubkey: authorPubkey }, { profile: "feed" });
if (decision.visibility.effect === "hide") return null;
Extending to a new application
Write an adapter saying what your object is in governance terms, and a policy saying what the evidence means to you. Nothing else.
import { createApplicationAdapter, evaluateObject } from "@nostr-governance/core";
const productAdapter = createApplicationAdapter({
applicationId: "my-shop",
toTargets: (product) => [
{ type: "user", pubkey: product.sellerPubkey },
{ type: "event", id: product.id, author: product.sellerPubkey },
{ type: "address", kind: "30078", pubkey: product.sellerPubkey, identifier: product.sku },
],
getPrimaryTargetKey: (product) => `address:30078:${product.sellerPubkey}:${product.sku}`,
});
const decision = evaluateObject(product, productAdapter, (target) =>
runtime.evaluate(target, { profile: "checkout" }),
);
A worked example lives in examples/commerce — a
marketplace built entirely on the public API, demonstrating seller allowlists,
address denial that survives republication, malware-driven checkout blocking,
capability-scoped moderator roles, community curator ingestion, and
seller-facing explanations. See docs/integration-guide.md.
Authority model
Root-authorized contributors: the root publishes the role roster, each actor publishes contributions under their own key, and a contribution is accepted only if the signing actor holds the matching capability.
manage-roles manage-policy manage-community-sources
contribute-user-allow contribute-user-deny contribute-event-deny
contribute-address-deny contribute-trust-seed review-evidence
Roles are convenience bundles over these; applications define their own
(listing_moderator, seller_moderator, …). No moderator ever needs the root
key. Protected actors cannot be denied by any contributor list, however many
curators list them.
Development
npm install
npm run lint # reference guard + typecheck
npm test # unit + conformance
npm run build:types # per-package .d.ts
BitVid and BitRoad are read-only reference repositories, pinned by commit in
docs/reference-map.md. Behavior is copied via fixtures
and characterization tests, never by importing their source;
npm run check:references fails the build if anything does.
Regenerate the characterization corpus with:
node scripts/build-characterization-corpus.mjs
License
GPL-3.0-or-later. See docs/license-and-provenance.md.