No bundler required. Build the widget once, self-host it, and configure everything in markup. Then replace the preset with thresholds that fit your own community.
Clone the repository and build. The output is a single self-contained file with no runtime dependencies.
git clone https://github.com/PR0M3TH3AN/Nostr-Governance
cd Nostr-Governance
npm install
npm run build:widget # → packages/widget/dist/bitgate.js
<bitgate-provider> builds the runtime, loads
administrative state from your relays, and shares both with every
element inside it.
<script type="module" src="/vendor/bitgate/bitgate.js"></script>
<bitgate-provider
relays="wss://relay-one.example,wss://relay-two.example"
root="<your-root-pubkey>"
policy="social">
<!-- everything governed goes in here -->
</bitgate-provider>
root is your deployment's administrator pubkey — the key
that publishes the moderator roster. It is configuration, not something
discovered at runtime.
Elements read their target from attributes, so a server-rendered page needs no JavaScript at all.
<bitgate-veil profile="feed" target-user="<author-pubkey>">
<article>…your post markup…</article>
</bitgate-veil>
<bitgate-veil profile="feed" target-event="<event-id>" target-author="<pubkey>">…</bitgate-veil>
<bitgate-veil profile="browse" target-address="30078:<pubkey>:sku-001">…</bitgate-veil>
The veil applies the decision: blur on restrict, a
disclosure with a "show anyway" control on hide, a warning
line on warn. Assigning .target in JavaScript
still works and wins over the attribute.
BitGate needs to know who is looking, both to build the trust graph and to check what they may do. BitLogin's provider fits without an adapter.
const provider = document.querySelector("bitgate-provider");
await provider.ready;
await provider.useSigner(window.nostr);
// BitGate reads the follow graph from your app — it has no opinion
// about how you fetch kind:3.
provider.runtime.trust.setContacts(followList);
ready, don't listen for it. The
bitgate:ready event fires during element upgrade, which for a
page that defines elements after parsing is before a listener could be
attached. The promise never has that race.
Presets exist so the first run works, not because the engine has an opinion. Every number in them is a starting point.
import { createPolicyDefinition } from "@bitgate/core";
const policy = createPolicyDefinition({
id: "my-app",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
reports: {
malware: { hide: 1, interactionDeny: 1 },
spam: { downrank: 2, restrict: 4, hide: 8 },
default: { downrank: 2, warn: 4 },
},
mutes: { default: { downrank: 1, hide: 12 } },
muteWindowSeconds: 60 * 24 * 60 * 60,
},
},
});
provider.runtime.policies.setLocalPolicy(policy);
Embed only what you need; each is independent.
| Element | What it does |
|---|---|
bitgate-provider | Builds the runtime from attributes and shares it |
bitgate-veil | Applies a decision to whatever it wraps |
bitgate-report | Report dialog — needs no capability, anyone may report |
bitgate-status | Effect, reasons, and evidence for one target |
bitgate-capabilities | What the signed-in account may actually do |
bitgate-action | A capability-gated button that explains refusals |
bitgate-admin-panel | Effective state, attribution, and controls |
Set with the policy attribute.
| Name | Profiles | For |
|---|---|---|
social | feed, profile, playback | Timelines and posts. Discovery declines to hard-hide. |
commerce | browse, detail, checkout, seller-dashboard | Marketplaces. Checkout decides transactions only. |
admin-only | default | Operator deny lists, with no trust-graph signals at all. |
0 means disabled, not
"fires on everything" — so zeroing a value is a safe way to switch it off.
If your renderer compares report counts to thresholds, the point has been lost. Consume the decision.
Evaluate transaction-critical actions against their own profile at the moment of action. State moves.
setViewer clears viewer-scoped state by design. Set contacts, blocks, and overrides after it.
Public surfaces get counts; appeal surfaces get identities. exposeEvidence is off by default for a reason.
The full guide, including the adapter contract and every profile option, lives in docs/integration-guide.md.