Context
nostr-watch is the primary NIP-66 relay monitoring system. It currently uses @nostrability/schemata + AJV for validation, but only in the browser GUI (Web Worker) and an auditor test library. The production server-side apps that publish and consume NIP-66 events — relaymon, route66 — have zero schema validation.
This issue proposes adding a @nostrwatch/schemata-codegen package to the nostr-watch monorepo, generated by this tool, to provide typed interfaces, a kind registry, lightweight validators, and pre-processed AJV schemas.
What schemata-codegen adds vs. what exists today
| Capability |
schemata-js-ajv today |
schemata-codegen adds |
Benefit |
| TypeScript types per kind |
None — events are any/NostrEvent |
177 interfaces with literal kind discriminants |
Catch malformed events at compile time; enables IDE autocomplete on event fields |
| Tag tuple types |
None — tags are string[][] |
156 typed tuples (ETag, RTag, DTag, etc.) |
Prevents tag construction bugs (wrong position, missing field); self-documenting code |
| Kind name registry |
None |
KIND_NAMES[30166] → "Relay Discovery (NIP-66)" |
Human-readable labels in UI, logs, and error messages without maintaining a manual mapping |
| Lightweight validators |
Full AJV (heavy, needs peer dep) |
Zero-dependency tag validators (~132 kinds) |
Runs anywhere (edge, WASM, workers) without bundling AJV; faster for tag-only checks |
| Pre-processed AJV schemas |
Strips $id at runtime per call |
Already stripped at build time |
Eliminates repeated work on every validation call; cleaner AJV integration |
| Error messages |
AJV's cryptic schema paths |
Human-friendly: "kind must equal 30166" |
Actionable feedback for users/developers instead of #/allOf/1/properties/kind/const |
Current schemata coverage of nostr-watch kinds and tags
Kinds (9 used by nostr-watch)
| Kind |
Schema? |
Registry? |
Validator? |
Notes |
| 0 |
✓ |
✓ |
✗ |
No tag constraints to validate |
| 2 |
✗ |
✗ |
✗ |
Deprecated (replaced by kind 10002) |
| 3 |
✓ |
✓ |
✗ |
No tag constraints to validate |
| 1066 |
✗ |
✗ |
✗ |
nostr-watch extension, not standardized |
| 10002 |
✓ |
✓ |
✓ |
Full coverage |
| 10166 |
✓ |
✓ |
✗ |
PR #106 adds tag constraints → would generate a validator |
| 20166 |
✗ |
✗ |
✗ |
nostr-watch extension, not standardized |
| 30002 |
✓ |
✓ |
✓ |
Full coverage |
| 30166 |
✓ |
✓ |
✓ |
PR #106 enriches tag constraints → richer validator |
3 fully covered, 3 partial (schema but no validator), 3 missing (2 non-standard)
Tags (18 used by nostr-watch)
| Tag |
Schema? |
TS Type? |
NIP-66 semantic match? |
d |
✓ |
✓ DTag |
✓ |
r |
✓ |
✓ RTag |
✓ |
n |
✓ |
✓ NTag |
⚠️ Schemata = Bitcoin networks; NIP-66 = clearnet/tor/i2p |
N |
✓ |
✗ |
No TS type generated for uppercase N |
R |
✓ |
✗ |
No TS type generated for uppercase R |
c |
✓ |
✓ CTag |
⚠️ Schemata = commitment; NIP-66 = check type |
k |
✓ |
✓ KTag |
✓ |
p |
✓ |
✓ PTag |
✓ |
s |
✓ |
✓ STag |
⚠️ Schemata = status; NIP-66 = software name |
g |
✓ |
✓ GTag |
✓ |
l |
✓ |
✓ LTag |
✓ |
L |
✓ |
✓ UpperLTag |
✓ |
frequency |
✗ |
✗ |
NIP-66 specific, not in schemata |
timeout |
✗ |
✗ |
NIP-66 specific, not in schemata |
rtt-open |
✗ |
✗ |
NIP-66 specific, not in schemata |
rtt-read |
✗ |
✗ |
NIP-66 specific, not in schemata |
rtt-write |
✗ |
✗ |
NIP-66 specific, not in schemata |
client |
✓ |
✓ ClientTag |
✓ |
Semantic collisions: Tags n, c, and s have different meanings across NIPs. The generic tag types in tags.d.ts reflect their original NIP definitions, not NIP-66. This is expected — the kind-level validators (not tag-level types) enforce NIP-66-specific values per kind.
Missing tag schemas: frequency, timeout, rtt-open, rtt-read, rtt-write are NIP-66-specific tags with no standalone schemas in schemata. However, PR #106 adds these as kind-level tag constraints on kinds 10166 and 30166, so the codegen validators will enforce them.
Dependency on schemata PR #106
schemata PR #106 enriches the NIP-66 kind schemas with per-tag validation:
- kind 10166 gains constraints for:
frequency, timeout, c, g tags
- kind 30166 gains constraints for:
rtt-*, n, N, R, T, t, k, g, l tags
Before #106: codegen generates nearly empty validators for NIP-66 kinds (only d tag check on 30166).
After #106: codegen generates validators that check tag structure, value patterns, and enums — actually catching the bugs found in nostr-watch's Kind30166.ts.
This integration should wait for #106 to merge and a new schemata release to publish.
Where validation is used today in nostr-watch
| Component |
Role |
Schema validation? |
| GUI (browser) |
Dashboard |
Yes — schemata-js-ajv in Web Worker |
| Auditor (library) |
Protocol compliance testing |
Yes — @nostrability/schemata directly |
| rstate (server) |
REST + MCP API |
AJV for MCP tool output only, not Nostr schemas |
| relaymon (server) |
Publishes Kind 10166/30166/1066 |
None 🧪 add schemata-cogen lightweight validators, types 🧪 |
| trawler (server) |
Crawls relay lists (Kind 2/3/10002) |
None 🧪 add schemata-cogen lightweight validators, types 🧪 |
| route66 (library) |
Consumes/aggregates NIP-66 events |
None 🧪 add schemata-cogen lightweight validators, types🧪 |
Production publish and consume paths are entirely unvalidated.
Bugs found in nostr-watch event construction
Kind30166.ts — RTT validation checks the wrong variable:
// BUG: checks 'open' not 'read'
if( read && typeof open === 'number' && read > 0 ){
tags.push(['rtt-read', String(Math.round(read))]);
}
// BUG: checks 'open' not 'write'
if( write && typeof open === 'number' && write > 0 ){
tags.push(['rtt-write', String(Math.round(write))]);
}
Other type safety gaps:
Kind10166Data.networks accepts arbitrary strings (should be "clearnet" | "tor" | "i2p")
Kind30166 CheckData.info.supported_nips accepts scalar numbers (not just arrays)
Kind30166 silently truncates attributes to 9 and kinds to 21 without warning
- SSL date parsing can produce
NaN silently
- GeoData is fully untyped (
{[key: string]: any})
Proposed integration
New package: libraries/schemata-codegen/
A new workspace package @nostrwatch/schemata-codegen containing generated outputs:
libraries/schemata-codegen/
package.json # zero runtime dependencies, tsc build
tsconfig.json
generate.sh # regeneration script
src/
index.ts # barrel re-export
kinds.d.ts # 177 kind event interfaces
tags.d.ts # 156 tag tuple types
validators.ts # ~132 zero-dep kind validators + 3 tag validators
kind-registry.ts # KIND_REGISTRY, KIND_NAMES, KNOWN_KIND_NUMBERS
error-messages.ts # human-friendly error messages
ajv-schemas/ # 177 pre-processed JSON schemas for AJV
Full set (all 177 kinds, 156 tags) — tree-shakeable, zero runtime cost for unused.
Automation: CI drift detection
Prerequisite: Publish @nostrability/schemata-codegen to npm.
- Add
@nostrability/schemata-codegen and @nostrability/schemata as devDependencies
generate.sh runs npx schemata-codegen --schemas node_modules/@nostrability/schemata/dist ...
- CI drift check:
pnpm generate && git diff --exit-code — fails if generated files are stale
- Renovate/Dependabot bumps versions when either package publishes
Performance cost
- Runtime: Near-zero. Types erased at compile time. Validators do simple string/array checks (microseconds). Event publishing is network-bound (milliseconds).
- Build: ~1-2 seconds for tsc. Zero new runtime dependencies.
- Bundle: Tree-shakeable — unused exports stripped.
Relationship to existing packages
- Does not touch
libraries/schemata-js-ajv — coexists as complementary package
- Does not touch
libraries/schemata/ (deprecated local schema copy)
- Future:
schemata-js-ajv could import pre-processed AJV schemas instead of runtime $id stripping
Sequencing
- ✅ schemata #105 — issue filed for NIP-66 gaps
- ⏳ schemata #106 — PR adding NIP-66 tag constraints (must merge first)
- 🔲 Publish new schemata release with enriched NIP-66 schemas
- 🔲 Publish
@nostrability/schemata-codegen to npm (this repo)
- 🔲 Create
@nostrwatch/schemata-codegen package in nostr-watch (PR)
References
Context
nostr-watch is the primary NIP-66 relay monitoring system. It currently uses
@nostrability/schemata+ AJV for validation, but only in the browser GUI (Web Worker) and an auditor test library. The production server-side apps that publish and consume NIP-66 events — relaymon, route66 — have zero schema validation.This issue proposes adding a
@nostrwatch/schemata-codegenpackage to the nostr-watch monorepo, generated by this tool, to provide typed interfaces, a kind registry, lightweight validators, and pre-processed AJV schemas.What schemata-codegen adds vs. what exists today
any/NostrEventkinddiscriminantsstring[][]ETag,RTag,DTag, etc.)KIND_NAMES[30166]→"Relay Discovery (NIP-66)"$idat runtime per call"kind must equal 30166"#/allOf/1/properties/kind/constCurrent schemata coverage of nostr-watch kinds and tags
Kinds (9 used by nostr-watch)
3 fully covered, 3 partial (schema but no validator), 3 missing (2 non-standard)
Tags (18 used by nostr-watch)
drnNRckpsglLfrequencytimeoutrtt-openrtt-readrtt-writeclientSemantic collisions: Tags
n,c, andshave different meanings across NIPs. The generic tag types intags.d.tsreflect their original NIP definitions, not NIP-66. This is expected — the kind-level validators (not tag-level types) enforce NIP-66-specific values per kind.Missing tag schemas:
frequency,timeout,rtt-open,rtt-read,rtt-writeare NIP-66-specific tags with no standalone schemas in schemata. However, PR #106 adds these as kind-level tag constraints on kinds 10166 and 30166, so the codegen validators will enforce them.Dependency on schemata PR #106
schemata PR #106 enriches the NIP-66 kind schemas with per-tag validation:
frequency,timeout,c,gtagsrtt-*,n,N,R,T,t,k,g,ltagsBefore #106: codegen generates nearly empty validators for NIP-66 kinds (only
dtag check on 30166).After #106: codegen generates validators that check tag structure, value patterns, and enums — actually catching the bugs found in nostr-watch's Kind30166.ts.
This integration should wait for #106 to merge and a new schemata release to publish.
Where validation is used today in nostr-watch
schemata-js-ajvin Web Worker@nostrability/schematadirectlyProduction publish and consume paths are entirely unvalidated.
Bugs found in nostr-watch event construction
Kind30166.ts — RTT validation checks the wrong variable:
Other type safety gaps:
Kind10166Data.networksaccepts arbitrary strings (should be"clearnet" | "tor" | "i2p")Kind30166 CheckData.info.supported_nipsaccepts scalar numbers (not just arrays)Kind30166silently truncatesattributesto 9 andkindsto 21 without warningNaNsilently{[key: string]: any})Proposed integration
New package:
libraries/schemata-codegen/A new workspace package
@nostrwatch/schemata-codegencontaining generated outputs:Full set (all 177 kinds, 156 tags) — tree-shakeable, zero runtime cost for unused.
Automation: CI drift detection
Prerequisite: Publish
@nostrability/schemata-codegento npm.@nostrability/schemata-codegenand@nostrability/schemataas devDependenciesgenerate.shrunsnpx schemata-codegen --schemas node_modules/@nostrability/schemata/dist ...pnpm generate && git diff --exit-code— fails if generated files are stalePerformance cost
Relationship to existing packages
libraries/schemata-js-ajv— coexists as complementary packagelibraries/schemata/(deprecated local schema copy)schemata-js-ajvcould import pre-processed AJV schemas instead of runtime$idstrippingSequencing
@nostrability/schemata-codegento npm (this repo)@nostrwatch/schemata-codegenpackage in nostr-watch (PR)References
next)