Building a High-Performance Nostr Relay for Recipe Sharing

Setting up a custom Nostr relay optimized for handling recipe events can significantly enhance performance and flexibility. This article outlines an approach to establishing such a relay using either Khatru with Eventstore or by modifying Strfry's configuration.
Building a High-Performance Nostr Relay for Recipe Sharing

Why would you set up a custom relay for Recipes?

Recipes are inherently rich in data, encompassing multiple ingredients, categories, cuisines, nutritional information, and detailed instructions. A custom relay tailored to handle a specific recipe event kind (nominally kind=35000) can:

  • Optimize Data Retrieval: Efficient indexing and storage mechanisms ensure swift access to complex recipe data.
  • Enhance Flexibility: Tailored configurations accommodate the unique structure of recipe events, making query filters more efficient and flexible.
  • Improve Performance: Specialized handling reduces latency, which becomes a very important factor for high volume services.
  • Support Advanced Features: Features like real-time syncing, bulk import/export, and efficient compression cater to the dynamic nature of recipe sharing.

By aligning the relay’s architecture with the structure of recipe events, Nostr developers can build responsive and scalable applications.

Setting Up a Custom Recipe Relay

Option 1: Using khatru and Eventstore

  1. Bootstrap a basic khatru relay:

    • Follow the steps outlined in the khatru docs:
       relay := khatru.NewRelay()
      
  2. Set up Eventstore:

    • Configure khataru to use Eventstore and Postgresql:
       // Initialize Eventstore
      db := postgresql.PostgresBackend{DatabaseURL: "postgres://eventuser:securepassword@localhost:5432/eventstore_db?sslmode=disable"}
      if err := db.Init(); err != nil {
          log.Fatalf("Failed to initialize eventstore: %v", err)
      }
      // Integrate Eventstore with Khatru
      relay.StoreEvent = append(relay.StoreEvent, db.SaveEvent)
      relay.QueryEvents = append(relay.QueryEvents, db.QueryEvents)
      relay.CountEvents = append(relay.CountEvents, db.CountEvents)
      relay.DeleteEvent = append(relay.DeleteEvent, db.DeleteEvent)
      
      
  3. Modify tag handling

    • Update the tags_to_tagvalues function to handle multi-character keys, multiple tag entries, and nested structures:
       _, err = db.DB.Exec(
      CREATE OR REPLACE FUNCTION tags_to_tagvalues(jsonb) RETURNS text[]
          AS $$
          SELECT array_agg(t.value)
          FROM jsonb_array_elements($1) AS elem(tag)
          JOIN LATERAL (
              SELECT
                  CASE
                      WHEN tag->>0 IN ('cuisine', 'category', 'ingredients', 'tags') THEN tag->>1
                      WHEN tag->>0 = 'nutrition' AND jsonb_array_length(tag) >= 3 THEN tag->>2
                      ELSE NULL
                  END AS value
          ) AS t
          ON t.value IS NOT NULL;
          $$ LANGUAGE SQL IMMUTABLE RETURNS NULL ON NULL INPUT;
       )
      
  4. Add custom polices:

    • khatru supports setting custom event policies:
    // Apply custom policies to accept only kind 35000 events
    relay.RejectEvent = append(relay.RejectEvent,
        // Built-in policies
        policies.ValidateKind,
    
        // Custom policy to accept only kind 35000
        func(ctx context.Context, event *nostr.Event) (bool, string) {
            if event.Kind != 35000 {
                return true, "Only recipe events (kind 35000) are accepted"
            }
            return false, ""
        },
    )
    
  5. Pray that fiatjaf doesn’t fire me

Option 2: Modifying Strfry’s Configuration

Strfry is another robust Nostr relay implementation that leverages LMDB for local data storage. Stirfry uses the golpe C++ framework and by adjusting its configuration in golpe.yaml, you can tailor Strfry to handle recipe events optimally.

  1. Optimize Indices:

    • Enhance indexing for specific recipe-related tags to speed up queries:
    indices:
      cuisine:
        comparator: StringUint64
        multi: true
      category:
        comparator: StringUint64
        multi: true
      ingredients:
        comparator: StringUint64
        multi: true
      nutrition:
        comparator: StringUint64Uint64
        multi: true
    
  2. Update indexPrelude:

     ```yaml
     indexPrelude: |
         PackedEventView packed(v.buf);
         created_at = packed.created_at();
         uint64_t indexTime = *created_at;
    
         id = makeKey_StringUint64(packed.id(), indexTime);
         pubkey = makeKey_StringUint64(packed.pubkey(), indexTime);
         kind = makeKey_Uint64Uint64(packed.kind(), indexTime);
         pubkeyKind = makeKey_StringUint64Uint64(packed.pubkey(), packed.kind(), indexTime);
    
         packed.foreachTag([&](char tagName, std::string_view tagVal, std::string_view tagVal2 = ""){
             // General tag indexing
             tag.push_back(makeKey_StringUint64(std::string(1, tagName) + std::string(tagVal), indexTime));
    
             // Specific tag handling for recipes
             std::string tagNameStr(1, tagName);
             if (tagNameStr == "cuisine") {
                 cuisine.push_back(makeKey_StringUint64(std::string(tagVal), indexTime));
             } else if (tagNameStr == "category") {
                 category.push_back(makeKey_StringUint64(std::string(tagVal), indexTime));
             } else if (tagNameStr == "ingredients") {
                 ingredients.push_back(makeKey_StringUint64(std::string(tagVal), indexTime));
             } else if (tagNameStr == "nutrition" && !tagVal2.empty()) {
                 nutrition.push_back(makeKey_StringUint64Uint64(std::string(tagVal), std::string(tagVal2), indexTime));
             } else if (tagNameStr == "tags") {
                 tags_specific.push_back(makeKey_StringUint64(std::string(tagVal), indexTime));
             }
    
             // Existing conditions for 'd' and 'e' tags
             if (tagName == 'd' && replace.size() == 0) {
                 replace.push_back(makeKey_StringUint64(std::string(packed.pubkey()) + std::string(tagVal), packed.kind()));
             } else if (tagName == 'e' && packed.kind() == 5) {
                 deletion.push_back(std::string(tagVal) + std::string(packed.pubkey()));
             }
    
             return true;
         });
    
         if (packed.expiration() != 0) {
             expiration.push_back(packed.expiration());
         }
     ```
    

Conclusion

Setting up a custom Nostr relay tailored for recipe events would enable developers to build highly performant and flexible applications. Whether it is using Khatru with Eventstore or modify Strfry’s configuration, the key lies in aligning the relay’s architecture with the features of recipe data. This approach not only improves performance but also lays a robust foundation for building feature-rich clients and applications within the Nostr ecosystem.


No comments yet.