Files
client/app-stacks/plan.md
T

7.9 KiB

App Stacks — Forward Plan

Current State

www/app-stacks.html has:

  • A publish form that takes a Gitea URL + category and publishes a kind 30078 app-definition event
  • A subscription that displays received app-definition events grouped by category

New Understanding

  • Categories are actually app stacks — kind 30267 events (App curation set)
  • An app stack is a curated collection of apps, e.g. "Nostr Clients", "Bitcoin & Lightning Wallets"
  • An app definition (kind 30078) belongs to an app stack, not a free-text category
  • We need two separate publish flows:
    1. Create an App Stack — publishes a kind 30267 event
    2. Publish App Definition — selects an existing app stack to belong to

Nostr Event Types

App Stack (kind 30267)

{
  "kind": 30267,
  "content": "{\"name\":\"Nostr Clients\",\"description\":\"Nostr-native social and communication clients.\"}",
  "tags": [
    ["d", "nostr-clients"],
    ["t", "app-stack"],
    ["name", "Nostr Clients"],
    ["description", "Nostr-native social and communication clients."],
    // References to apps in this stack:
    ["a", "30078:<pubkey>:app-com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"],
    ["a", "30078:<pubkey>:app-com.example.other", "wss://relay.zapstore.dev"]
  ]
}

App Definition (kind 30078) — updated

{
  "kind": 30078,
  "content": "{\"name\":\"Amethyst\",\"identifier\":\"com.vitorpamplona.amethyst\",\"repository\":\"...\",\"description\":\"\"}",
  "tags": [
    ["d", "app-com.vitorpamplona.amethyst"],
    ["t", "app-definition"],
    ["t", "nostr-clients"],           // ← references the app stack's d tag
    ["name", "Amethyst"],
    ["repository", "https://github.com/vitorpamplona/amethyst"],
    ["gitea", "com.vitorpamplona.amethyst"]
  ]
}

Implementation Phases

Phase 1: App Stack CRUD

Goal: Create, list, and display app stacks (kind 30267 events).

Changes to www/app-stacks.html (JS only):

  1. New subscription — subscribe to kind 30267 with #t: app-stack:

    subscribe({ kinds: [30267], '#t': ['app-stack'], limit: 200 }, ...);
    
  2. New function: parseAppStack(evt) — parse kind 30267 events into { dTag, name, description, appRefs: [], eventId }

  3. New function: renderStacks() — display app stacks in a section above or alongside app definitions

  4. New function: publishAppStack(name, description) — create and publish a kind 30267 event:

    {
      kind: 30267,
      content: JSON.stringify({ name, description }),
      tags: [
        ['d', name.toLowerCase().replace(/\s+/g, '-')],
        ['t', 'app-stack'],
        ['name', name],
        ['description', description],
      ],
      created_at: Math.floor(Date.now() / 1000),
    }
    
  5. New function: showCreateStackForm() — form with name and description fields

  6. Update renderApps() — add a "Create App Stack" button/area

Goal: App definitions select an app stack instead of typing a free-text category.

Changes to www/app-stacks.html (JS only):

  1. Update publish form — replace the free-text category input with a <select> dropdown populated from loaded app stacks

  2. Update doPublish() — use the selected stack's d tag value as the #t category tag

  3. Update renderApps() — group apps by the app stack they reference, not by free-text category

Phase 3: Stack Membership Management

Goal: Add/remove apps from stacks, and display stack membership.

Changes to www/app-stacks.html (JS only):

  1. Update publishAppDefinition() — also add an a tag referencing the app stack:

    ['a', '30267:<pubkey>:' + stackDTag, 'wss://relay.zapstore.dev']
    
  2. Display stack membership — show which stack(s) each app belongs to

  3. Stack detail view — show all apps in a stack

Data Flow

flowchart TD
    subgraph "Publishing"
        A[Create App Stack form] --> B[publishEvent kind 30267]
        C[Publish App Definition form] --> D[Select stack from dropdown]
        D --> E[publishEvent kind 30078<br>with #t: stack-d-tag]
    end
    
    subgraph "Subscriptions"
        F[Subscribe kind 30267 #t: app-stack] --> G[Collect stacks]
        H[Subscribe kind 30078 #t: app-definition] --> I[Collect apps]
    end
    
    subgraph "Rendering"
        G --> J[Render stack list]
        I --> K[Group apps by stack reference]
        K --> L[Render apps under each stack]
    end

Files to Modify

File Change
www/app-stacks.html Add JS for kind 30267 subscription, parsing, rendering, publishing. Update app-definition form to use stack dropdown.
app-stacks/README.md Update with new event structures and flow

What NOT to Change

  • No HTML changes to the template shell
  • No CSS additions
  • No removal of existing functionality

Corrections Discovered During Implementation

1. Stack a Tags Must Use Kind 32267 (Not 30078)

The relay wss://relay.zapstore.dev validates that a tags in kind 30267 events reference kind 32267 (Software Application). Using kind 30078 results in invalid app identifier: expected kind 32267, got 30078.

The a tags in stacks reference the original app listing events (kind 32267) published by app developers, not the app-definition events (kind 30078) that the frontend loads. The frontend independently subscribes to kind 30078 #t: app-definition and groups them by the #t category tag matching the stack's d tag — it does not follow the a tags.

Current state in app-stacks/publish_all.py:

["a", f"32267:{pubkey}:{app_id}", "wss://relay.zapstore.dev"]

The pubkey is still a placeholder (the signer's own pubkey) since we don't have the actual per-app pubkeys from the original kind 32267 events. This is acceptable because the frontend doesn't use the a tags for display — it uses the #t category tag on the app-definition events.

2. Stack Events Should Have JSON Content

The plan specifies content: JSON.stringify({ name, description }) but the original publish_all.py used content: "". The frontend reads name/description from tags, so this is non-critical, but it's inconsistent with the spec.

Fixed in app-stacks/publish_all.py:

"content": json.dumps({"name": stack["name"], "description": stack["description"]}),

3. relay.zapstore.dev Rejects Kind 30078 with d: app-*

The relay wss://relay.zapstore.dev has a whitelist for kind 30078 d tags — only zapstore-device-state and zapstore-device-key-backup are accepted. App definitions with d: app-<identifier> are rejected with invalid 'd' tag.

Workaround: App definitions are published to the other 3 relays (nos.lol, primal.net, laantungir.net/relay). The frontend's NDK subscription queries user-configured relays, so it will find them as long as the user has at least one of these relays configured.

4. Published Event Counts

Event Type Expected Published Relays
App Stacks (kind 30267) 20 20 All 4 relays
App Definitions (kind 30078) 182 174 3 relays (not zapstore.dev)

The 8 missing app definitions are duplicates (e.g., com.flux appears in both "Productivity & Notes" and "Finance & Budgeting", com.unciv.app in both "Utilities & Tools" and "Games"). The script overwrites the previous event with the same d tag, so only the last one persists.

5. Old Stack Had No #t: app-stack Tag

The previously published stack (privacy-approved-notes) was missing the ["t", "app-stack"] tag entirely. All 20 new stacks include it, which is required for the frontend's subscription filter #t: ['app-stack'] to find them.