<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://www.completenoobs.com/noobs/index.php?action=history&amp;feed=atom&amp;title=Ngate_basic</id>
	<title>Ngate basic - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://www.completenoobs.com/noobs/index.php?action=history&amp;feed=atom&amp;title=Ngate_basic"/>
	<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ngate_basic&amp;action=history"/>
	<updated>2026-05-26T03:09:58Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.1</generator>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ngate_basic&amp;diff=759&amp;oldid=prev</id>
		<title>AwesomO: Created page with &quot;{{:LICENCE_HEADER_MIT}}   = nGate — Deploy to a Fresh Ubuntu 24.04 Server =  From CompleteNoobs  This guide walks an operator through deploying nGate on top of an existing stage-1 nostr-rs-relay running on a Vultr Ubuntu 24.04 box. After this you&#039;ll have:  * The relay still running as before. * Caddy still terminating TLS as before. * &#039;&#039;&#039;A new sidecar container&#039;&#039;&#039; (&lt;code&gt;ngate-sync&lt;/code&gt;) running alongside,   scanning Hive every...&quot;</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ngate_basic&amp;diff=759&amp;oldid=prev"/>
		<updated>2026-05-26T00:38:07Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;{{:LICENCE_HEADER_MIT}}   = nGate — Deploy to a Fresh Ubuntu 24.04 Server =  From CompleteNoobs  This guide walks an operator through deploying nGate on top of an existing &lt;a href=&quot;/noobs/index.php?title=Nostr_Relay_With_a_3-Key_Whitelist&amp;amp;action=edit&amp;amp;redlink=1&quot; class=&quot;new&quot; title=&quot;Nostr Relay With a 3-Key Whitelist (page does not exist)&quot;&gt;stage-1 nostr-rs-relay&lt;/a&gt; running on a Vultr Ubuntu 24.04 box. After this you&amp;#039;ll have:  * The relay still running as before. * Caddy still terminating TLS as before. * &amp;#039;&amp;#039;&amp;#039;A new sidecar container&amp;#039;&amp;#039;&amp;#039; (&amp;lt;code&amp;gt;ngate-sync&amp;lt;/code&amp;gt;) running alongside,   scanning Hive every...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;{{:LICENCE_HEADER_MIT}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= nGate — Deploy to a Fresh Ubuntu 24.04 Server =&lt;br /&gt;
&lt;br /&gt;
From CompleteNoobs&lt;br /&gt;
&lt;br /&gt;
This guide walks an operator through deploying nGate on top of an existing&lt;br /&gt;
[[Nostr_Relay_With_a_3-Key_Whitelist|stage-1 nostr-rs-relay]] running on a&lt;br /&gt;
Vultr Ubuntu 24.04 box. After this you&amp;#039;ll have:&lt;br /&gt;
&lt;br /&gt;
* The relay still running as before.&lt;br /&gt;
* Caddy still terminating TLS as before.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;A new sidecar container&amp;#039;&amp;#039;&amp;#039; (&amp;lt;code&amp;gt;ngate-sync&amp;lt;/code&amp;gt;) running alongside,&lt;br /&gt;
  scanning Hive every 6 hours, updating the relay&amp;#039;s &amp;lt;code&amp;gt;pubkey_whitelist&amp;lt;/code&amp;gt;&lt;br /&gt;
  automatically when v4call-server operators announce themselves.&lt;br /&gt;
* &amp;#039;&amp;#039;&amp;#039;Cryptographic enforcement&amp;#039;&amp;#039;&amp;#039;: only posts that prove ownership of both&lt;br /&gt;
  the Hive account AND the Nostr key (via mutual attestation) get whitelisted.&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
&lt;br /&gt;
You&amp;#039;ve already:&lt;br /&gt;
&lt;br /&gt;
* Followed [[Nostr_Relay_With_a_3-Key_Whitelist|stage 1]] and have a working&lt;br /&gt;
  &amp;lt;code&amp;gt;wss://nostr.YOUR-DOMAIN/&amp;lt;/code&amp;gt; relay.&lt;br /&gt;
* Followed [[Nostr_Hands-On|stage 2]] enough to be comfortable with the&lt;br /&gt;
  Nostr protocol fundamentals.&lt;br /&gt;
* Have at least one v4call-server announce post on Hive (signed via&lt;br /&gt;
  &amp;lt;code&amp;gt;server-sign.html&amp;lt;/code&amp;gt;, announced via &amp;lt;code&amp;gt;server-announce.html&amp;lt;/code&amp;gt;)&lt;br /&gt;
  to use as test input.&lt;br /&gt;
&lt;br /&gt;
If you don&amp;#039;t have a v4call-server post yet, you can still deploy nGate and&lt;br /&gt;
test it (the scan will find existing operators&amp;#039; posts), you just won&amp;#039;t be in&lt;br /&gt;
the whitelist until you publish your own.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
&lt;br /&gt;
* [[#Step_1:_SSH_to_the_relay_box|1 Step 1: SSH to the relay box]]&lt;br /&gt;
* [[#Step_2:_Install_runtime_dependencies|2 Step 2: Install runtime dependencies]]&lt;br /&gt;
* [[#Step_3:_Clone_nGate|3 Step 3: Clone nGate]]&lt;br /&gt;
* [[#Step_4:_Install_npm_dependencies|4 Step 4: Install npm dependencies]]&lt;br /&gt;
* [[#Step_5:_Bootstrap_config.toml|5 Step 5: Bootstrap config.toml]]&lt;br /&gt;
* [[#Step_6:_Create_seed.toml|6 Step 6: Create seed.toml (CRITICAL — don&amp;#039;t skip)]]&lt;br /&gt;
* [[#Step_7:_Configure_ngate.yaml|7 Step 7: Configure ngate.yaml]]&lt;br /&gt;
* [[#Step_8:_Dry-run_the_chain|8 Step 8: Dry-run the chain]]&lt;br /&gt;
* [[#Step_9:_Merge_sidecar_into_docker-compose.yml|9 Step 9: Merge sidecar into docker-compose.yml]]&lt;br /&gt;
* [[#Step_10:_Build_and_start_the_sidecar|10 Step 10: Build and start the sidecar]]&lt;br /&gt;
* [[#Step_11:_Watch_a_cycle|11 Step 11: Watch a cycle]]&lt;br /&gt;
* [[#Step_12:_Routine_monitoring|12 Step 12: Routine monitoring]]&lt;br /&gt;
* [[#Updating_nGate|13 Updating nGate]]&lt;br /&gt;
* [[#Known_gotchas|14 Known gotchas]]&lt;br /&gt;
* [[#Removing_nGate|15 Removing nGate (back to manual whitelist)]]&lt;br /&gt;
&lt;br /&gt;
== Step 1: SSH to the relay box ==&lt;br /&gt;
&lt;br /&gt;
 ssh root@nostr.YOUR-DOMAIN&lt;br /&gt;
&lt;br /&gt;
If you set up a non-root user during stage 1, use that and prefix everything&lt;br /&gt;
with &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; as needed.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Install runtime dependencies ==&lt;br /&gt;
&lt;br /&gt;
nGate needs a few things beyond what stage 1 installed. Inside the sidecar&lt;br /&gt;
container, all deps are auto-installed (Alpine image bakes Node + jq + curl&lt;br /&gt;
+ docker-cli + yq). Outside the container — for dry-runs and manual sync&lt;br /&gt;
commands — install on the host:&lt;br /&gt;
&lt;br /&gt;
 apt update&lt;br /&gt;
 apt install -y nodejs npm jq curl&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&lt;br /&gt;
 for c in bash curl jq sed awk node; do command -v $c &amp;gt;/dev/null &amp;amp;&amp;amp; echo &amp;quot;✓ $c&amp;quot; || echo &amp;quot;✗ $c MISSING&amp;quot;; done&lt;br /&gt;
&lt;br /&gt;
All six should print ✓.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Clone nGate ==&lt;br /&gt;
&lt;br /&gt;
The relay lives at &amp;lt;code&amp;gt;/opt/nostr-relay/&amp;lt;/code&amp;gt;. Clone nGate next to it so&lt;br /&gt;
the bind-mount paths in &amp;lt;code&amp;gt;docker-compose.example.yml&amp;lt;/code&amp;gt; work:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 git clone https://github.com/CompleteNoobs/nGate src&lt;br /&gt;
&lt;br /&gt;
That gives you:&lt;br /&gt;
&lt;br /&gt;
 /opt/nostr-relay/&lt;br /&gt;
 ├── config.toml             ← from stage 1&lt;br /&gt;
 ├── Caddyfile               ← from stage 1&lt;br /&gt;
 ├── docker-compose.yml      ← from stage 1&lt;br /&gt;
 ├── data/                   ← from stage 1 (relay sqlite DB)&lt;br /&gt;
 ├── caddy_data/             ← from stage 1&lt;br /&gt;
 └── src/                    ← NEW (nGate repo)&lt;br /&gt;
     ├── scripts/&lt;br /&gt;
     ├── ngate.yaml.example&lt;br /&gt;
     ├── docker-compose.example.yml&lt;br /&gt;
     └── ...&lt;br /&gt;
&lt;br /&gt;
For convenience, symlink the scripts dir + copy the YAML template up to&lt;br /&gt;
&amp;lt;code&amp;gt;/opt/nostr-relay/&amp;lt;/code&amp;gt; (so bind-mount paths are simple):&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 ln -s src/scripts scripts&lt;br /&gt;
 cp src/ngate.yaml.example ngate.yaml&lt;br /&gt;
&lt;br /&gt;
Now &amp;lt;code&amp;gt;scripts/ngate-*.sh&amp;lt;/code&amp;gt; work from &amp;lt;code&amp;gt;/opt/nostr-relay/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;💡 Tip&amp;#039;&amp;#039;&amp;#039;: you can also clone nGate anywhere else and adjust the&lt;br /&gt;
&amp;lt;code&amp;gt;NGATE_*&amp;lt;/code&amp;gt; env-vars in &amp;lt;code&amp;gt;ngate.yaml&amp;lt;/code&amp;gt; to point at the&lt;br /&gt;
right paths. The clone-into-&amp;lt;code&amp;gt;src/&amp;lt;/code&amp;gt;-and-symlink approach is just&lt;br /&gt;
the path of least resistance.&lt;br /&gt;
&lt;br /&gt;
== Step 4: Install npm dependencies ==&lt;br /&gt;
&lt;br /&gt;
The Hive ECDSA + Nostr schnorr helpers are Node scripts that need&lt;br /&gt;
&amp;lt;code&amp;gt;@hiveio/dhive&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;nostr-tools&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay/src/scripts&lt;br /&gt;
 npm install&lt;br /&gt;
&lt;br /&gt;
This creates &amp;lt;code&amp;gt;node_modules/&amp;lt;/code&amp;gt; in the scripts folder (~35MB).&lt;br /&gt;
Gitignored — won&amp;#039;t bloat the repo. One-time cost; subsequent&lt;br /&gt;
&amp;lt;code&amp;gt;git pull&amp;lt;/code&amp;gt; + &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; is incremental.&lt;br /&gt;
&lt;br /&gt;
Verify both deps are present:&lt;br /&gt;
&lt;br /&gt;
 ls node_modules/@hiveio/dhive/package.json&lt;br /&gt;
 ls node_modules/nostr-tools/package.json&lt;br /&gt;
&lt;br /&gt;
Both should exist.&lt;br /&gt;
&lt;br /&gt;
== Step 5: Bootstrap config.toml ==&lt;br /&gt;
&lt;br /&gt;
Your stage-1 &amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt; has an &amp;lt;code&amp;gt;[authorization]&amp;lt;/code&amp;gt;&lt;br /&gt;
section with hand-pasted hex pubkeys. nGate manages a specific block of&lt;br /&gt;
this file between BEGIN/END marker comments. Run the bootstrap to wrap your&lt;br /&gt;
existing section:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 ./scripts/ngate-apply.sh --bootstrap&lt;br /&gt;
&lt;br /&gt;
Output should look like:&lt;br /&gt;
&lt;br /&gt;
 ngate-apply: ✓ Bootstrapped — wrapped [authorization] in BEGIN/END markers.&lt;br /&gt;
 ngate-apply:   ngate-apply now manages everything between those markers.&lt;br /&gt;
 ngate-apply:   Operator-edited keys go in: /opt/nostr-relay/seed.toml&lt;br /&gt;
&lt;br /&gt;
Inspect &amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
 cat /opt/nostr-relay/config.toml&lt;br /&gt;
&lt;br /&gt;
You should see your existing pubkey_whitelist entries, now wrapped:&lt;br /&gt;
&lt;br /&gt;
 # === BEGIN NGATE-MANAGED — DO NOT EDIT BY HAND ===&lt;br /&gt;
 [authorization]&lt;br /&gt;
 pubkey_whitelist = [&lt;br /&gt;
   &amp;quot;your_existing_hex_1&amp;quot;,&lt;br /&gt;
   &amp;quot;your_existing_hex_2&amp;quot;,&lt;br /&gt;
   &amp;quot;your_existing_hex_3&amp;quot;,&lt;br /&gt;
 ]&lt;br /&gt;
 # === END NGATE-MANAGED ===&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;⚠ From this point forward, don&amp;#039;t hand-edit between those markers.&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
nGate will overwrite that region on every sync. Manual additions go in&lt;br /&gt;
&amp;lt;code&amp;gt;seed.toml&amp;lt;/code&amp;gt; (next step).&lt;br /&gt;
&lt;br /&gt;
== Step 6: Create seed.toml (CRITICAL — don&amp;#039;t skip) ==&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;Why this matters&amp;#039;&amp;#039;&amp;#039;: nGate&amp;#039;s first &amp;lt;code&amp;gt;--apply&amp;lt;/code&amp;gt; with&lt;br /&gt;
&amp;lt;code&amp;gt;--allow-removals&amp;lt;/code&amp;gt; can delete entries that aren&amp;#039;t currently&lt;br /&gt;
backed by a valid v4call-server post on Hive — including yours, if your&lt;br /&gt;
operator key isn&amp;#039;t yet announced. Without a seed entry, you can lock&lt;br /&gt;
yourself out.&lt;br /&gt;
&lt;br /&gt;
Make a &amp;lt;code&amp;gt;seed.toml&amp;lt;/code&amp;gt; at &amp;lt;code&amp;gt;/opt/nostr-relay/seed.toml&amp;lt;/code&amp;gt;&lt;br /&gt;
listing every hex pubkey that should ALWAYS be allowed, regardless of&lt;br /&gt;
discovery state. Most importantly: your own operator key.&lt;br /&gt;
&lt;br /&gt;
 nano /opt/nostr-relay/seed.toml&lt;br /&gt;
&lt;br /&gt;
Paste:&lt;br /&gt;
&lt;br /&gt;
 # nGate seed list — operator-managed, never auto-removed.&lt;br /&gt;
 # One hex pubkey per line. Comments allowed.&lt;br /&gt;
 #&lt;br /&gt;
 # Critical: include your own operator key here so nGate can&amp;#039;t accidentally&lt;br /&gt;
 # lock you out if your v4call-server post temporarily fails verification.&lt;br /&gt;
&lt;br /&gt;
 your_operator_hex_pubkey_here   # @noblemage&amp;#039;s nostr key&lt;br /&gt;
&lt;br /&gt;
If you&amp;#039;ve been running stage 1 with 3 manual keys, you might want all 3 as&lt;br /&gt;
seeds initially. Worst case is one extra entry in the whitelist; not a&lt;br /&gt;
correctness problem.&lt;br /&gt;
&lt;br /&gt;
== Step 7: Configure ngate.yaml ==&lt;br /&gt;
&lt;br /&gt;
Open the YAML config and edit for your relay:&lt;br /&gt;
&lt;br /&gt;
 nano /opt/nostr-relay/ngate.yaml&lt;br /&gt;
&lt;br /&gt;
The defaults are sensible. Key fields to customise:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;instance_name&amp;lt;/code&amp;gt; — appears in logs. e.g. &amp;lt;code&amp;gt;nostr-hive-book&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &amp;lt;code&amp;gt;scan_interval_seconds&amp;lt;/code&amp;gt; — default 21600 (6 hours). If you&lt;br /&gt;
  want faster updates and don&amp;#039;t mind the Hive RC, drop to 3600 (1 hour).&lt;br /&gt;
* &amp;lt;code&amp;gt;gate.account&amp;lt;/code&amp;gt; — &amp;lt;code&amp;gt;escrow&amp;lt;/code&amp;gt; (default; gates the&lt;br /&gt;
  operational account that holds caller funds) or &amp;lt;code&amp;gt;hive_account&amp;lt;/code&amp;gt;&lt;br /&gt;
  (gates the announce-signing account).&lt;br /&gt;
* &amp;lt;code&amp;gt;gate.min_hp&amp;lt;/code&amp;gt; — minimum HP staked. &amp;lt;code&amp;gt;3&amp;lt;/code&amp;gt; is a fine&lt;br /&gt;
  test threshold. Production servers might want 30+.&lt;br /&gt;
* &amp;lt;code&amp;gt;gate.min_token_*&amp;lt;/code&amp;gt; — leave blank to disable; set to a&lt;br /&gt;
  Hive-Engine token symbol + amount if you want token-gating.&lt;br /&gt;
* &amp;lt;code&amp;gt;max_consecutive_failures&amp;lt;/code&amp;gt; — 3 = ~18 hours of absence before&lt;br /&gt;
  remove. Keep default unless you have a reason.&lt;br /&gt;
* &amp;lt;code&amp;gt;restart_command&amp;lt;/code&amp;gt; — default &amp;lt;code&amp;gt;docker restart nostr-relay&amp;lt;/code&amp;gt;&lt;br /&gt;
  matches the container_name in your docker-compose.yml. Don&amp;#039;t change&lt;br /&gt;
  unless you renamed the relay container.&lt;br /&gt;
* &amp;lt;code&amp;gt;paths.*&amp;lt;/code&amp;gt; — only change if you put nGate somewhere other than&lt;br /&gt;
  &amp;lt;code&amp;gt;/opt/nostr-relay/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Step 8: Dry-run the chain ==&lt;br /&gt;
&lt;br /&gt;
Before turning the sidecar loose, run the full pipeline manually in&lt;br /&gt;
&amp;lt;code&amp;gt;--dry-run&amp;lt;/code&amp;gt; mode (no writes, no restarts):&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 ./scripts/ngate-scan.sh 2&amp;gt;/dev/null \&lt;br /&gt;
   | ./scripts/ngate-verify.sh 2&amp;gt;&amp;amp;1 \&lt;br /&gt;
   | NGATE_MIN_HP=3 ./scripts/ngate-gate.sh 2&amp;gt;&amp;amp;1 \&lt;br /&gt;
   | ./scripts/ngate-apply.sh&lt;br /&gt;
&lt;br /&gt;
Expected stderr output sequence:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;ngate-scan:&amp;lt;/code&amp;gt; ok, N posts&lt;br /&gt;
* &amp;lt;code&amp;gt;ngate-verify:&amp;lt;/code&amp;gt; OK / REJECT / skip for each candidate&lt;br /&gt;
* &amp;lt;code&amp;gt;ngate-gate:&amp;lt;/code&amp;gt; OK / REJECT for each verified candidate&lt;br /&gt;
* &amp;lt;code&amp;gt;ngate-apply:&amp;lt;/code&amp;gt; seed: X loaded, current: Y in whitelist, stdin:&lt;br /&gt;
  Z passed, summary: add=… keep=… tolerate=… remove=… → DRY RUN, no&lt;br /&gt;
  files changed.&lt;br /&gt;
&lt;br /&gt;
Read the output. Check that:&lt;br /&gt;
&lt;br /&gt;
* Your own operator key is in the SEED count.&lt;br /&gt;
* Your v4call-server post (if you have one) appears as OK in verify and&lt;br /&gt;
  passes the gate (or fails for an understandable reason).&lt;br /&gt;
* The summary &amp;quot;new whitelist size&amp;quot; looks plausible.&lt;br /&gt;
* config.toml hasn&amp;#039;t been modified (&amp;lt;code&amp;gt;cat config.toml&amp;lt;/code&amp;gt; shows the&lt;br /&gt;
  same content as after bootstrap).&lt;br /&gt;
&lt;br /&gt;
If anything looks off, fix it (re-sign + re-announce, adjust ngate.yaml,&lt;br /&gt;
adjust seed.toml) before going live.&lt;br /&gt;
&lt;br /&gt;
== Step 9: Merge sidecar into docker-compose.yml ==&lt;br /&gt;
&lt;br /&gt;
Your stage-1 &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; already has the &amp;lt;code&amp;gt;nostr-relay&amp;lt;/code&amp;gt;&lt;br /&gt;
and &amp;lt;code&amp;gt;caddy&amp;lt;/code&amp;gt; services. The nGate sidecar adds a third. Open both&lt;br /&gt;
files:&lt;br /&gt;
&lt;br /&gt;
 nano /opt/nostr-relay/docker-compose.yml&lt;br /&gt;
 # In another terminal:&lt;br /&gt;
 cat /opt/nostr-relay/src/docker-compose.example.yml&lt;br /&gt;
&lt;br /&gt;
Copy the &amp;lt;code&amp;gt;ngate-sync&amp;lt;/code&amp;gt; service block from the example into your&lt;br /&gt;
real &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt;. The block looks like:&lt;br /&gt;
&lt;br /&gt;
 services:&lt;br /&gt;
   # ... your existing nostr-relay + caddy services ...&lt;br /&gt;
&lt;br /&gt;
   ngate-sync:&lt;br /&gt;
     build:&lt;br /&gt;
       context: .&lt;br /&gt;
       dockerfile: src/scripts/Dockerfile&lt;br /&gt;
     container_name: ngate-sync&lt;br /&gt;
     restart: unless-stopped&lt;br /&gt;
     volumes:&lt;br /&gt;
       - ./src/scripts:/app/scripts&lt;br /&gt;
       - ./ngate.yaml:/app/ngate.yaml:ro&lt;br /&gt;
       - ./seed.toml:/app/seed.toml:ro&lt;br /&gt;
       - ./config.toml:/app/config.toml&lt;br /&gt;
       - ./state.json:/app/state.json&lt;br /&gt;
       - /var/run/docker.sock:/var/run/docker.sock&lt;br /&gt;
     environment:&lt;br /&gt;
       - NGATE_YAML=/app/ngate.yaml&lt;br /&gt;
     depends_on:&lt;br /&gt;
       - nostr-relay&lt;br /&gt;
     networks:&lt;br /&gt;
       - relaynet&lt;br /&gt;
&lt;br /&gt;
(Note: &amp;lt;code&amp;gt;./src/scripts&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;./scripts&amp;lt;/code&amp;gt; because&lt;br /&gt;
we kept the cloned repo at &amp;lt;code&amp;gt;src/&amp;lt;/code&amp;gt; and symlinked. If you did it&lt;br /&gt;
differently, adjust the volume paths.)&lt;br /&gt;
&lt;br /&gt;
Make sure to pre-create the state.json so the bind-mount works:&lt;br /&gt;
&lt;br /&gt;
 echo &amp;#039;{&amp;quot;last_run&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;last_apply&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;restart_log&amp;quot;:[],&amp;quot;candidates&amp;quot;:{}}&amp;#039; &amp;gt; /opt/nostr-relay/state.json&lt;br /&gt;
&lt;br /&gt;
&amp;#039;&amp;#039;&amp;#039;⚠ Security note&amp;#039;&amp;#039;&amp;#039;: the sidecar mounts &amp;lt;code&amp;gt;/var/run/docker.sock&amp;lt;/code&amp;gt;&lt;br /&gt;
so it can &amp;lt;code&amp;gt;docker restart nostr-relay&amp;lt;/code&amp;gt;. This gives the sidecar&lt;br /&gt;
container effective root on the host. Acceptable on a single-purpose relay&lt;br /&gt;
box. NOT acceptable on a multi-tenant machine. If that bothers you, set&lt;br /&gt;
&amp;lt;code&amp;gt;restart_command&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;ngate.yaml&amp;lt;/code&amp;gt; to a noop (e.g.&lt;br /&gt;
&amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;) and restart the relay manually after each cycle —&lt;br /&gt;
configuration writes still work; only the restart is skipped.&lt;br /&gt;
&lt;br /&gt;
== Step 10: Build and start the sidecar ==&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 docker compose build ngate-sync&lt;br /&gt;
 docker compose up -d ngate-sync&lt;br /&gt;
&lt;br /&gt;
First build downloads node:20-alpine + yq + docker-cli (~150MB). Subsequent&lt;br /&gt;
builds are fast (cached layers). On startup the sidecar runs&lt;br /&gt;
&amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; inside the bind-mounted scripts dir if&lt;br /&gt;
&amp;lt;code&amp;gt;node_modules&amp;lt;/code&amp;gt; isn&amp;#039;t present — should be quick if you already&lt;br /&gt;
ran step 4.&lt;br /&gt;
&lt;br /&gt;
Check the container&amp;#039;s running:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps ngate-sync&lt;br /&gt;
&lt;br /&gt;
Should show &amp;lt;code&amp;gt;Up&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Step 11: Watch a cycle ==&lt;br /&gt;
&lt;br /&gt;
Tail the logs:&lt;br /&gt;
&lt;br /&gt;
 docker compose logs -f ngate-sync&lt;br /&gt;
&lt;br /&gt;
The sidecar runs ONE cycle immediately at startup, then sleeps for&lt;br /&gt;
&amp;lt;code&amp;gt;scan_interval_seconds&amp;lt;/code&amp;gt; between cycles. The first cycle should&lt;br /&gt;
show:&lt;br /&gt;
&lt;br /&gt;
 [2026-05-13T…] [nostr-hive-book] starting ngate-sync instance=… interval=21600s …&lt;br /&gt;
 [2026-05-13T…] [nostr-hive-book] ─── cycle starting ───&lt;br /&gt;
 ngate-scan: trying https://api.hive.blog …&lt;br /&gt;
 ngate-scan:   ok, N posts&lt;br /&gt;
 ngate-scan: scan complete&lt;br /&gt;
 ngate-verify: OK @cnoobs/call.completenoobs.com — Hive sig + Nostr attestation valid …&lt;br /&gt;
 ngate-verify: OK @v4call/v4call.com — Hive sig + Nostr attestation valid …&lt;br /&gt;
 ngate-verify: OK @hive-book/hive-book.com — Hive sig + Nostr attestation valid …&lt;br /&gt;
 ngate-gate: config: account=escrow mode=or hp_enabled=1 (&amp;gt;=3, delegated=false) …&lt;br /&gt;
 ngate-gate: OK @cnoobs/… — passed via hp hp=…&lt;br /&gt;
 ngate-gate: complete — total=3 passed=3 rejected=0 errors=0&lt;br /&gt;
 ngate-apply: seed: 1 pubkey(s) loaded from /app/seed.toml&lt;br /&gt;
 ngate-apply: current: 3 pubkey(s) in /app/config.toml whitelist&lt;br /&gt;
 ngate-apply: stdin: 3 candidate(s) passed gate this run&lt;br /&gt;
 ngate-apply: summary: add=… keep=… tolerate=0 remove=0 seed=1 → new whitelist size = …&lt;br /&gt;
 ngate-apply: ✓ wrote new whitelist to /app/config.toml&lt;br /&gt;
 ngate-apply: restarting relay: docker restart nostr-relay&lt;br /&gt;
 ngate-apply: ✓ restart succeeded&lt;br /&gt;
 [2026-05-13T…] [nostr-hive-book] ─── cycle complete (apply ok) ───&lt;br /&gt;
 [2026-05-13T…] [nostr-hive-book] sleeping 21600s until next cycle&lt;br /&gt;
&lt;br /&gt;
After this, your &amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt;&amp;#039;s nGate-managed block should&lt;br /&gt;
reflect the discovered+gated set.&lt;br /&gt;
&lt;br /&gt;
Verify the relay still works:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps nostr-relay   # Up&lt;br /&gt;
 # From your laptop:&lt;br /&gt;
 nak req -k 1 -a YOUR_HEX wss://nostr.YOUR-DOMAIN&lt;br /&gt;
&lt;br /&gt;
If it returns events, the restart worked cleanly.&lt;br /&gt;
&lt;br /&gt;
== Step 12: Routine monitoring ==&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Status snapshot&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:From inside the sidecar:&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose exec ngate-sync /app/scripts/ngate-status.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
:Or from the host (env-vars on the command):&lt;br /&gt;
:&lt;br /&gt;
:  NGATE_YAML=/opt/nostr-relay/ngate.yaml \&lt;br /&gt;
:  NGATE_STATE_PATH=/opt/nostr-relay/state.json \&lt;br /&gt;
:  NGATE_CONFIG_PATH=/opt/nostr-relay/config.toml \&lt;br /&gt;
:  NGATE_SEED_PATH=/opt/nostr-relay/seed.toml \&lt;br /&gt;
:  /opt/nostr-relay/scripts/ngate-status.sh&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Logs&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:* &amp;lt;code&amp;gt;docker compose logs -f ngate-sync&amp;lt;/code&amp;gt; — live stream&lt;br /&gt;
:* &amp;lt;code&amp;gt;tail -F /opt/nostr-relay/ngate-sync.log&amp;lt;/code&amp;gt; — bind-mounted host file&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Manual sync&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:If you want a fresh cycle right now (don&amp;#039;t wait 6 hours):&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose exec ngate-sync /app/scripts/ngate-sync.sh --once&amp;lt;/code&amp;gt;&lt;br /&gt;
:Or restart the sidecar (runs one cycle on startup):&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose restart ngate-sync&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Inspect state.json&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:Shows per-key first-seen, last-seen, consecutive_misses, source&lt;br /&gt;
:(discovery|seed|removed):&lt;br /&gt;
: &amp;lt;code&amp;gt;jq . /opt/nostr-relay/state.json&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Updating nGate ==&lt;br /&gt;
&lt;br /&gt;
When the repo updates:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay/src&lt;br /&gt;
 git pull&lt;br /&gt;
 cd scripts &amp;amp;&amp;amp; npm install   # in case dep versions changed&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 docker compose build ngate-sync   # only needed if Dockerfile changed&lt;br /&gt;
 docker compose restart ngate-sync&lt;br /&gt;
&lt;br /&gt;
The scripts dir is bind-mounted, so script-only changes take effect on next&lt;br /&gt;
sync without rebuild. Restart picks them up immediately.&lt;br /&gt;
&lt;br /&gt;
== Known gotchas ==&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;&amp;quot;Keychain not detected&amp;quot; in server-announce&amp;#039;&amp;#039;&amp;#039; (browser-side, not nGate)&lt;br /&gt;
:Some browsers don&amp;#039;t expose &amp;lt;code&amp;gt;window.hive_keychain&amp;lt;/code&amp;gt; even with the&lt;br /&gt;
:extension installed (iOS Safari, Brave on iOS, etc.). server-announce.html&lt;br /&gt;
:has a posting-key paste fallback that uses dhive to broadcast directly —&lt;br /&gt;
:scroll past the Keychain button to the &amp;quot;POSTING KEY PASTE&amp;quot; section. Key&lt;br /&gt;
:never leaves your browser.&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;nGate-verify rejects with &amp;quot;no nostr_attestation in well-known OR post&amp;quot;&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:Your v4call-server post is pre-attestation, OR the well-known doesn&amp;#039;t have&lt;br /&gt;
:the &amp;lt;code&amp;gt;nostr_attestation&amp;lt;/code&amp;gt; field, OR the post body doesn&amp;#039;t have a&lt;br /&gt;
:&amp;lt;code&amp;gt;NOSTR-ATTESTATION:&amp;lt;/code&amp;gt; base64 line. Re-sign in&lt;br /&gt;
:&amp;lt;code&amp;gt;server-sign.html&amp;lt;/code&amp;gt; (Option B canonical) and re-announce. The&lt;br /&gt;
:full chain produces both the well-known field AND the post-body line in&lt;br /&gt;
:one signed event.&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;nGate-verify rejects with &amp;quot;Hive signature DID NOT verify&amp;quot;&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:Most common cause: signature was computed under a DIFFERENT canonical&lt;br /&gt;
:payload shape than what nGate-verify reconstructs. Re-sign with the&lt;br /&gt;
:current server-sign.html (which produces the Option B / SHORT canonical&lt;br /&gt;
:that nGate-verify and v4call&amp;#039;s existing federation code both expect).&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;nGate-verify rejects with &amp;quot;attestation v4call_hive_account tag (X) ≠ post&amp;#039;s HIVE-ACCOUNT (Y)&amp;quot;&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:You signed the attestation with one Hive account name (e.g.&lt;br /&gt;
:&amp;lt;code&amp;gt;hive-book&amp;lt;/code&amp;gt;) but the Hive post declares a different one (e.g.&lt;br /&gt;
:&amp;lt;code&amp;gt;hive-book.com&amp;lt;/code&amp;gt;). Re-sign with consistent values across the&lt;br /&gt;
:whole flow (server-sign → server-announce → Hive post).&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Sidecar can&amp;#039;t restart relay (&amp;quot;permission denied&amp;quot; on docker.sock)&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:Less common, but if the relay box has docker access locked down: either&lt;br /&gt;
:loosen perms on &amp;lt;code&amp;gt;/var/run/docker.sock&amp;lt;/code&amp;gt; (default world-writable&lt;br /&gt;
:on most distros) OR change &amp;lt;code&amp;gt;restart_command&amp;lt;/code&amp;gt; in ngate.yaml to&lt;br /&gt;
:a noop and restart the relay manually after each sync.&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;Sidecar in a restart loop&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:Probably an invalid &amp;lt;code&amp;gt;ngate.yaml&amp;lt;/code&amp;gt; or missing&lt;br /&gt;
:&amp;lt;code&amp;gt;node_modules&amp;lt;/code&amp;gt;. Check:&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs --tail=50 ngate-sync&amp;lt;/code&amp;gt;&lt;br /&gt;
:The startup log shows YAML parse errors clearly. If&lt;br /&gt;
:&amp;lt;code&amp;gt;node_modules&amp;lt;/code&amp;gt; is missing the sidecar will install it on first&lt;br /&gt;
:start; subsequent runs are fast.&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;&amp;quot;Restart cap hit&amp;quot; in logs&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:nGate refuses to restart the relay more than&lt;br /&gt;
:&amp;lt;code&amp;gt;max_restarts_per_day&amp;lt;/code&amp;gt; times in 24h. If you see this&lt;br /&gt;
:repeatedly, something upstream is causing the whitelist to flap.&lt;br /&gt;
:Investigate state.json and check whether candidates are missing-then-back&lt;br /&gt;
:repeatedly (suggests Hive RPC flakiness or a v4call-server post&lt;br /&gt;
:disappearing temporarily).&lt;br /&gt;
&lt;br /&gt;
;&amp;#039;&amp;#039;&amp;#039;&amp;quot;Sanity bound triggered&amp;quot; in logs&amp;#039;&amp;#039;&amp;#039;&lt;br /&gt;
:nGate refused to apply because the result would have removed every&lt;br /&gt;
:non-seed entry. Triggered when upstream had errors AND&lt;br /&gt;
:&amp;lt;code&amp;gt;--allow-removals&amp;lt;/code&amp;gt; was somehow set. With the sidecar&amp;#039;s&lt;br /&gt;
:PIPESTATUS-aware logic, this shouldn&amp;#039;t happen in practice; if it does,&lt;br /&gt;
:check upstream phase exit codes.&lt;br /&gt;
&lt;br /&gt;
== Removing nGate (back to manual whitelist) ==&lt;br /&gt;
&lt;br /&gt;
If you want to disable nGate and go back to hand-editing&lt;br /&gt;
&amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/nostr-relay&lt;br /&gt;
 docker compose down ngate-sync&lt;br /&gt;
 docker compose rm -f ngate-sync&lt;br /&gt;
&lt;br /&gt;
Then manually edit &amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
 nano /opt/nostr-relay/config.toml&lt;br /&gt;
&lt;br /&gt;
Either:&lt;br /&gt;
&lt;br /&gt;
* Delete the BEGIN/END marker comments (keep the&lt;br /&gt;
  &amp;lt;code&amp;gt;[authorization]&amp;lt;/code&amp;gt; section between them as-is), OR&lt;br /&gt;
* Replace the entire managed block with a hand-curated whitelist.&lt;br /&gt;
&lt;br /&gt;
Restart the relay to pick up the new config:&lt;br /&gt;
&lt;br /&gt;
 docker compose restart nostr-relay&lt;br /&gt;
&lt;br /&gt;
The relay reads &amp;lt;code&amp;gt;config.toml&amp;lt;/code&amp;gt; on startup regardless of markers.&lt;br /&gt;
nGate&amp;#039;s markers are JUST for nGate&amp;#039;s internal &amp;quot;what to manage&amp;quot; logic; the&lt;br /&gt;
relay itself ignores them as TOML comments.&lt;br /&gt;
&lt;br /&gt;
== When you&amp;#039;re done ==&lt;br /&gt;
&lt;br /&gt;
After 24 hours of clean runs, nGate has handled at least one normal cycle&lt;br /&gt;
(scan + verify + gate + maybe-apply). At that point you can:&lt;br /&gt;
&lt;br /&gt;
* Stake some HIVE on your test operator to confirm a transition (off-gate&lt;br /&gt;
  → passing the gate after stake).&lt;br /&gt;
* Publish a v4call-server post for a new operator and watch it get picked&lt;br /&gt;
  up on the next cycle.&lt;br /&gt;
* Deploy nGate to your second relay box (nostr.v4call.com) with a&lt;br /&gt;
  staggered scan time (set &amp;lt;code&amp;gt;scan_interval_seconds&amp;lt;/code&amp;gt; the same but&lt;br /&gt;
  schedule the startup at a different time of day for faster&lt;br /&gt;
  federation-wide pickup).&lt;br /&gt;
&lt;br /&gt;
For the next phase of work (stage 3.6 refinements + stage 4 strfry&lt;br /&gt;
migration), see [[STATUS.md]] in the nGate repo.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
</feed>