- Go 65.3%
- Shell 34.1%
- Dockerfile 0.6%
|
All checks were successful
Build and Push Docker Image / build (push) Successful in 6m12s
|
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| allowlist.go | ||
| bolt_buffer.go | ||
| buffer.go | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| embedding.go | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| management.go | ||
| README.md | ||
| reindex.go | ||
| test_e2e.sh | ||
AMB Relay
A Nostr relay for AMB (Learning Resource Metadata) events (kind 30142). Built on the khatru relay framework with Typesense as the full-text search backend.
Quick Start
- Copy
.env.exampleto.envand fill in your values - Run
docker compose up
The relay listens on :3334, Typesense on :8108.
Environment Variables
Relay Metadata
NAME: Your relay's display namePUBKEY: Your Nostr public key (hex)DESCRIPTION: A description of your relayICON: URL to your relay's icon image
Typesense Configuration
| Variable | Description | Local dev | Docker Compose |
|---|---|---|---|
TS_APIKEY |
Typesense API key | xyz |
xyz (change for production) |
TS_HOST |
Typesense URL | http://localhost:8108 |
leave empty (auto-set to http://typesense:8108) |
TS_COLLECTION |
Collection name | amb_events |
amb_events |
Networking
| Variable | Description | Default |
|---|---|---|
SERVICE_URL |
Public-facing WebSocket URL (e.g. wss://your-relay.example.com). Set when running behind a reverse proxy so NIP-98 auth u tag validation uses the correct URL instead of the auto-detected ws:// |
auto-detected |
Storage & Management
| Variable | Description | Default |
|---|---|---|
DB_PATH |
Path to BoltDB file for raw event persistence | ./data/relay.db |
ADMIN_PUBKEYS |
Comma-separated hex pubkeys for NIP-86 management API access (in addition to PUBKEY) |
empty |
Semantic Search (Optional)
| Variable | Description | Default |
|---|---|---|
EMBED_ENDPOINT |
URL of embedding service (e.g., https://embed.edufeed.org/embed) |
empty (disabled) |
EMBED_TOKEN |
Bearer token for embedding service | empty |
SEMANTIC_SEARCH_ENABLED |
Auto-enable semantic search on startup | false |
When configured with SEMANTIC_SEARCH_ENABLED=true, the relay performs hybrid search (keyword + vector similarity) automatically. Can also be enabled/disabled at runtime via NIP-86.
Model: Uses MiniLM-L12-v2 (384 dimensions) for embeddings. See the eventstore README for technical details.
Deployment
Docker Compose runs both Typesense and the relay:
docker compose up -d --build
The Docker build downloads all dependencies from git.edufeed.org — no additional repos or local files needed.
Development
Setup
After cloning, enable the pre-push hook that runs E2E tests before every push:
git config core.hooksPath .githooks
To skip the hook when needed: git push --no-verify
Simple: everything in Docker
docker compose up
With local eventstore changes
The eventstore (typesense30142) lives in the nostrlib fork. To develop both simultaneously:
-
Clone both repos side-by-side:
edufeed/ ├── go.work # workspace file ├── amb-relay/ └── nostrlib/ -
Create
edufeed/go.work:go 1.25 use ( ./amb-relay ./nostrlib ) -
Run Typesense in Docker, relay locally:
docker compose up -d typesense go run .
The go.work file tells Go to use local nostrlib instead of the version from git.edufeed.org. Changes to the eventstore are immediately available on restart.
Updating the nostrlib dependency
After pushing changes to nostrlib on git.edufeed.org:
# Get the new pseudo-version
GOWORK=off go list -m git.edufeed.org/edufeed/nostrlib@latest
# Update go.mod replace directive with the new version
# Then run:
GOWORK=off go mod tidy
Testing
nak CLI
# Full-text search
nak req --search "mathematik" -k 30142 ws://localhost:3334
# Field-specific search
nak req --search "publisher.name:e-teaching.org" -k 30142 ws://localhost:3334
# Time range filter
nak req --since 1700000000 --until 1800000000 -k 30142 ws://localhost:3334
# Filter by tagged pubkey
nak req -p <pubkey> -k 30142 ws://localhost:3334
# Delete an event (NIP-09) — by addressable event reference
nak event -k 5 -t "a=30142:<author-pubkey>:<d-tag>" --sec <your-secret> ws://localhost:3334
# Count events (NIP-45)
nak count -k 30142 ws://localhost:3334
# Count with filters
nak count -k 30142 --search "physics" ws://localhost:3334
# Semantic search (if enabled)
# Finds "quantum mechanics" even when searching for related terms
nak req --search "Heisenberg uncertainty principle" -k 30142 ws://localhost:3334
Note: nak does not support colon-delimited tag names (#about:id, #learningResourceType:id). For these filters, use a Go client with nostr.TagMap. See the eventstore README for full query documentation.
Direct Typesense debugging
curl -H "X-TYPESENSE-API-KEY: $TS_APIKEY" \
"$TS_HOST/collections/$TS_COLLECTION/documents/search?q=*&per_page=1"
Event Validation
The relay accepts kind 30142 events and kind 5 deletion events (NIP-09).
Kind 30142 events require:
- A
dtag (resource identifier) - A
nametag (resource title)
Deletion events (kind 5) allow authors to delete their own events by referencing them with a tags (addressable) or e tags (by ID). Only the original author can delete an event.
Events from banned pubkeys are rejected.
NIP-86 Management API
The relay supports NIP-86 for remote management via HTTP. Send a POST request to the relay URL with Content-Type: application/nostr+json+rpc and a NIP-98 Authorization header.
Only the relay operator (PUBKEY) and additional admins (ADMIN_PUBKEYS) are authorized.
Supported methods
| Method | Description |
|---|---|
supportedmethods |
List available methods |
banpubkey |
Ban a pubkey from publishing |
listbannedpubkeys |
List all banned pubkeys |
allowpubkey |
Remove a pubkey ban |
banevent |
Ban an event by ID |
listbannedevents |
List all banned event IDs |
allowevent |
Remove an event ban |
changerelayname |
Update relay name (in memory) |
changerelaydescription |
Update relay description |
changerelayicon |
Update relay icon URL |
stats |
Get relay statistics |
Ban lists are persisted in BoltDB and survive restarts.
Typesense management methods
These custom methods control the Typesense search index schema, reindexing, and collection settings.
| Method | Params | Description |
|---|---|---|
getcollectionschema |
none | Returns the current Typesense collection schema (custom or default) |
updatecollectionschema |
[{fields: [...], default_sorting_field: "...", enable_nested_fields: bool}] |
Stores a new schema in BoltDB. Does not apply until reindex is called |
resetcollectionschema |
none | Removes the custom schema, reverting to the hardcoded default |
reindex |
none | Drops the Typesense collection, recreates it with the stored schema, and re-indexes all events from BoltDB. Runs asynchronously |
getreindexstatus |
none | Returns {running, total, indexed, errors, error} |
Schema changes are deferred — updatecollectionschema only stores the schema, and reindex applies it. During reindex the relay cannot serve search results (drop + rebuild approach).
Semantic search methods
| Method | Params | Description |
|---|---|---|
getsemanticsearchconfig |
none | Returns {enabled, embed_fields} |
updatesemanticsearchconfig |
[{enabled: bool, embed_fields: [...]}] |
Update config and toggle embedding |
enablesemanticsearch |
none | Shortcut to enable with default fields |
disablesemanticsearch |
none | Shortcut to disable |
Default embed fields: name, description, keywords, about
When enabled, new events are embedded on save and queries use hybrid search (30% vector, 70% keyword weight). Existing events need reindex to add embeddings.
Architecture
Nostr Client → Khatru Relay (:3334) ─┬→ Typesense (:8108) [search index]
└→ BoltDB [raw event storage]
- Khatru: Nostr relay framework (part of nostrlib fork)
- Typesense: Full-text search engine — queries go here
- BoltDB: Embedded key-value store — raw event persistence for backup/reindexing
- NIP-86: HTTP management API for banning, relay metadata, and stats