This document explains the technical architecture and design decisions of the Nostr schemata project.
- Standards-based: Uses JSON Schema Draft-07 for maximum compatibility
- Modular: Schemas can reference and compose each other
- Language-agnostic: Source schemas in YAML, output to multiple languages
- Type-safe: Generated code includes proper type definitions
- Extensible: Easy to add new NIPs, kinds, and message types
graph LR
A[YAML Sources] --> B[JSON Conversion]
B --> C[Reference Rewriting]
C --> D[Dereferencing]
D --> E[Bundle Generation]
D --> F[Language Code Gen]
- Tool:
yaml-convert - Process: All
.yamlfiles are converted to.json - Location: Results stored in
dist/maintaining source structure
- Script:
scripts/rewriteRefs.js - Purpose: Convert relative references to absolute paths
- Example:
@/note.yaml→https://schemata.nostr.watch/note
- Script:
scripts/deref.js - Tool:
@apidevtools/json-schema-ref-parser - Purpose: Resolve all
$refpointers, creating self-contained schemas - Result: Each schema file contains all referenced definitions inline
- Script:
build.js - Tool:
esbuild - Output:
dist/bundle/schemas.js- ES module with named exportsdist/bundle/schemas.d.ts- TypeScript declarationsdist/bundle/schemas.bundle.js- Minified bundle
- Tool:
quicktype - Languages: Rust, Python, Go, Java, Swift, Kotlin, TypeScript
- Output: Type-safe validation code for each language
Provides semantic shortcuts to actual schemas:
@/note.yaml → nips/nip-01/note/schema.yaml
@/tag/e.yaml → nips/nip-01/tag/e/schema.yaml
Benefits:
- Shorter, memorable references
- Version independence
- Easier refactoring
nips/
├── nip-01/ # Core protocol
│ ├── note/ # Base event structure
│ ├── kind-*/ # Specific event kinds
│ ├── messages/ # Protocol messages
│ └── tag/ # Tag definitions
├── nip-XX/ # Other NIPs
└── nipless/ # Experimental kinds
mips/
└── mip-00/ # MLS protocol (Marmot Improvement Proposals)
├── kind-*/ # MIP event kinds
└── tag/ # MIP tag definitions
Uses JSON Schema's allOf for composition:
# kind-1 inherits from base note
allOf:
- $ref: "@/note.yaml"
- type: object
properties:
kind:
const: 1The build.js script generates exports following these rules:
-
Base name processing:
schema.json→ removed to avoid redundancyschema.content.json→Content
-
Tag schemas:
tag/a/schema.json→aTagSchematag/_A/schema.json→ATagSchema
-
Kind schemas:
kind-1/schema.json→kind1Schema
-
Message schemas:
client-req/schema.json→clientReqSchema
NIPs take precedence over aliases to avoid naming conflicts.
- Type validation:
type: string|number|object|array - Format validation:
patternfor regex matching - Constraints:
minimum,maximum,minItems,maxItems - Conditional logic:
if/then/elsefor complex rules - Composition:
allOf,oneOf,anyOf - Constants:
constfor fixed values
Each property includes:
errorMessage: Human-readable validation errordescription: Documentation for developers
- Dereferencing: Eliminates runtime reference resolution
- Bundling: Single file import for all schemas
- Minification: Reduced bundle size for web usage
- Tree-shaking: Named exports allow importing only needed schemas
- Create directory:
nips/nip-XX/ - Add schemas following conventions
- Run build process
- Schemas automatically included in bundle
- Extend
package.jsonscripts for quicktype - Add language-specific configuration
- Generated code placed in
dist/packages/[language]/
Extend schemas with:
- Custom formats
- Additional constraints
- Business logic via
if/then/else
- Input validation: All schemas validate untrusted input
- Size limits: Arrays and strings have maximum sizes
- Pattern matching: Strict regex for identifiers
- Type safety: Generated code prevents type confusion
- Schema versioning: Track breaking changes
- Migration tools: Update events between schema versions
- Runtime validation: WebAssembly for consistent validation
- Schema registry: Decentralized schema discovery