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:
- Create an App Stack — publishes a kind 30267 event
- 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):
-
New subscription — subscribe to kind 30267 with
#t: app-stack:subscribe({ kinds: [30267], '#t': ['app-stack'], limit: 200 }, ...); -
New function:
parseAppStack(evt)— parse kind 30267 events into{ dTag, name, description, appRefs: [], eventId } -
New function:
renderStacks()— display app stacks in a section above or alongside app definitions -
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), } -
New function:
showCreateStackForm()— form with name and description fields -
Update
renderApps()— add a "Create App Stack" button/area
Phase 2: Link App Definitions to Stacks
Goal: App definitions select an app stack instead of typing a free-text category.
Changes to www/app-stacks.html (JS only):
-
Update publish form — replace the free-text category input with a
<select>dropdown populated from loaded app stacks -
Update
doPublish()— use the selected stack'sdtag value as the#tcategory tag -
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):
-
Update
publishAppDefinition()— also add anatag referencing the app stack:['a', '30267:<pubkey>:' + stackDTag, 'wss://relay.zapstore.dev'] -
Display stack membership — show which stack(s) each app belongs to
-
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.