The package documented itself as safe to import in Node, with registration a no-op absent a DOM. That was false: a class declaration evaluates its extends clause at module load, so `extends globalThis.HTMLElement` threw before canRegisterElements() could run. Caught by loading the deployed bundle rather than the source. Falls back to a plain base class when there is no DOM, so a server-rendering host can import the helpers and the documented promise is now true.
BitGate
Moderation and access control for Nostr apps. Decide what to show, what to allow, and what to sell — without writing the policy code yourself.
Pairs with BitLogin: BitLogin answers "who are you." BitGate answers "what may you see, and what may you do."
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 |
|---|---|
@bitgate/core |
Pure evaluation. No I/O, no browser globals, no dependencies. |
@bitgate/nostr |
Codecs: canonical v1, NIP-56, NIP-51, legacy lists, bech32. |
@bitgate/runtime |
Stores, transport orchestration, commands. |
@bitgate/verify |
Optional NIP-01 signature verification, so the core stays dependency-free. |
@bitgate/testing |
Conformance harness. |
@bitgate/widget |
Drop-in custom elements: viewer controls and a moderator console. |
@bitgate/bitvid-compat |
Reference application profile for the characterization corpus. |
Quick start — a static page
No npm install, no bundler. Build the widget once, self-host it, and configure everything in markup:
npm install && npm run build:widget # produces packages/widget/dist/bitgate.js
<script type="module" src="/vendor/bitgate/bitgate.js"></script>
<bitgate-provider relays="wss://relay.example" root="<root-pubkey>" policy="social">
<bitgate-veil profile="feed" target-user="<author-pubkey>">
<img src="thumbnail.jpg" alt="" />
</bitgate-veil>
</bitgate-provider>
That is the whole integration. The provider builds a runtime, loads administrative state, and descendants find it themselves — no wiring code.
Sign in with BitLogin and BitGate knows who the viewer is:
const provider = document.querySelector("bitgate-provider");
await provider.ready;
await provider.useSigner(window.nostr); // BitLogin's NIP-07 provider
Policy presets: social, commerce, admin-only. Every number in them is a
starting point — see the integration guide to
write your own.
Quick start — JavaScript
import { createPolicyDefinition, evaluateTarget } from "@bitgate/core";
import { createBitGate, createMemoryTransport } from "@bitgate/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 = createBitGate({
applicationId: "my-app",
namespace: "myapp",
root: ROOT_PUBKEY,
transport: createRelayTransport(["wss://relay.example"]),
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 "@bitgate/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.
Plug-and-play UI
@bitgate/widget ships custom elements that render decisions and issue
commands — and compute no policy of their own:
<bitgate-veil profile="feed"><img src="thumbnail.jpg" alt="" /></bitgate-veil>
import { defineBitGateElements } from "@bitgate/widget";
defineBitGateElements();
const veil = document.querySelector("bitgate-veil");
veil.runtime = runtime;
veil.target = { type: "event", id, author };
Viewer elements (bitgate-veil, bitgate-report, bitgate-status)
and moderator elements (bitgate-capabilities, bitgate-action,
bitgate-admin-panel) are separate, so a site embeds only what it needs.
See packages/widget/README.md and the runnable
examples/commerce/demo.html.
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 # .d.ts for every package + the widget bundle
The widget bundle is not committed. Build it from source and self-host the result — a moderation widget is exactly the kind of artifact where reading the source matters.
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.