- Added new dependencies: `adler2`, `crc32fast`, `flate2`, `miniz_oxide`, and `libredox`. - Updated existing dependencies: `tokio-rustls` to version 0.26.4 and `filetime` to version 0.2.27. - Removed the `backup.rs` file as it is no longer needed. - Introduced tests for configuration and credential management. - Enhanced the `identity` module to generate W3C compliant DID documents. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
Decentralized App Marketplace Protocol
Overview
Archipelago's community marketplace enables developers to publish app manifests to Nostr relays, where nodes discover and install them without a central app store. Trust is established through DID-signed manifests and community reputation.
Architecture
Developer Node Nostr Relays User Node
│ │ │
│── Publish signed manifest ──► │ │
│ (NIP-78, kind 30078) │ │
│ │ ◄── Query app manifests ── │
│ │ (filter by d-tag) │
│ │ │
│ │── Return signed manifests ──► │
│ │ │
│ │ [Verify DID signature] │
│ │ [Check trust score] │
│ │ [Display in marketplace] │
│ │ │
│ │ [User clicks Install] │
│ │ [Pull container image] │
│ │ [Start container] │
Manifest Schema
App manifests published to Nostr relays follow the existing apps/{app-id}/manifest.yml schema (see docs/app-manifest-spec.md), serialized as JSON within a Nostr event.
Marketplace Manifest Fields
{
"app_id": "my-bitcoin-tool",
"name": "My Bitcoin Tool",
"version": "1.2.0",
"description": {
"short": "A useful Bitcoin utility",
"long": "Detailed description of what this app does..."
},
"author": {
"name": "Developer Name",
"did": "did:key:z6Mkh...",
"nostr_pubkey": "npub1..."
},
"container": {
"image": "docker.io/developer/my-bitcoin-tool:1.2.0",
"ports": [{ "container": 8080, "host": 8180, "protocol": "tcp" }],
"volumes": [{ "name": "data", "path": "/data" }],
"env": {
"NETWORK": "mainnet"
},
"capabilities": [],
"readonly_root": true,
"no_new_privileges": true,
"run_as_user": 1000
},
"category": "money",
"icon_url": "https://example.com/icon.png",
"repo_url": "https://github.com/developer/my-bitcoin-tool",
"license": "MIT",
"min_archipelago_version": "0.1.0",
"dependencies": [],
"signatures": {
"manifest_hash": "sha256:abc123...",
"did_signature": "base64-encoded-signature"
}
}
Required Fields
| Field | Type | Description |
|---|---|---|
app_id |
string | Unique identifier, lowercase kebab-case |
name |
string | Human-readable display name |
version |
string | Semantic version (major.minor.patch) |
description.short |
string | One-line description (max 120 chars) |
author.did |
string | Developer's DID (did:key method) |
container.image |
string | Full container image reference with tag (never latest) |
category |
string | One of: money, commerce, data, networking, home, community, other |
Security-Required Fields
| Field | Default | Description |
|---|---|---|
container.readonly_root |
true | Container root filesystem is read-only |
container.no_new_privileges |
true | Prevent privilege escalation |
container.run_as_user |
1000 | UID to run as (must be > 1000) |
container.capabilities |
[] | Required Linux capabilities (drop all, add only needed) |
Nostr Event Format
Event Kind
App manifests use NIP-78 application-specific data with event kind 30078 (replaceable parameterized). This matches the existing node discovery pattern in nostr_discovery.rs.
Event Structure
{
"kind": 30078,
"tags": [
["d", "archipelago-app:<app_id>"],
["t", "archipelago-marketplace"],
["t", "category:<category>"],
["version", "<semver>"],
["image", "<container_image>"],
["L", "archipelago"],
["l", "app-manifest", "archipelago"]
],
"content": "<JSON-serialized manifest>",
"created_at": 1710000000,
"pubkey": "<developer's secp256k1 pubkey hex>",
"sig": "<schnorr signature>"
}
Tag Semantics
| Tag | Purpose |
|---|---|
d |
Unique identifier for NIP-33 replaceable events. Format: archipelago-app:<app_id> |
t |
Searchable topic tags for relay filtering |
version |
Allows version-specific queries |
image |
Container image for quick display without parsing content |
L/l |
NIP-32 labeling namespace for structured queries |
Publishing a Manifest
- Developer creates/updates their app manifest
- Serialize manifest as JSON
- Compute SHA-256 hash of the serialized manifest
- Sign the hash with the developer's DID key
- Embed manifest + signature in Nostr event content
- Sign the Nostr event with the node's secp256k1 key
- Publish to all configured Nostr relays
Discovering Manifests
- Node queries configured relays with filter:
{ "kinds": [30078], "limit": 100, "#t": ["archipelago-marketplace"] } - For each returned event: a. Verify Nostr event signature (standard NIP-01) b. Parse manifest JSON from content c. Verify DID signature on manifest hash d. Check manifest against security requirements e. Calculate trust score
- Return manifests sorted by trust score
Trust Model
Trust Score Calculation
Each discovered app receives a trust score (0-100) based on:
| Factor | Weight | Description |
|---|---|---|
| DID Verification | 30 | Manifest is signed by a valid DID key |
| Relay Consensus | 20 | Manifest found on multiple independent relays |
| Federation Trust | 20 | Developer's DID is in the user's federation network |
| Version History | 15 | App has multiple published versions (shows maintenance) |
| Security Compliance | 15 | Manifest follows all security requirements |
Trust Tiers
| Score | Tier | UI Treatment |
|---|---|---|
| 80-100 | Verified | Green badge, install with one click |
| 50-79 | Community | Yellow badge, install with confirmation |
| 20-49 | Unverified | Orange badge, install with warning dialog |
| 0-19 | Untrusted | Red badge, requires explicit security override |
Federation-Based Trust
When a developer's DID appears in the user's federation network (trusted peer), the app automatically receives +20 trust points. This creates organic trust propagation: if you trust a node operator, you're more likely to trust their published apps.
ADR: Nostr Relays over Centralized Registry
Decision: Use Nostr relays as the app discovery layer instead of a centralized registry.
Context: A centralized app store contradicts Archipelago's sovereignty principles. Nostr relays provide censorship-resistant, decentralized event distribution.
Consequences:
- (+) No single point of failure for app discovery
- (+) Developers publish without permission or review gates
- (+) Multiple relay sources increase availability
- (+) Leverages existing Nostr infrastructure and key management
- (-) No global content moderation (each node decides trust locally)
- (-) Spam is possible (mitigated by DID verification and trust scoring)
- (-) Relay availability varies (mitigated by querying multiple relays)
Signing Protocol
Manifest Signing (DID Layer)
1. Serialize manifest to canonical JSON (sorted keys, no whitespace)
2. Compute: manifest_hash = SHA-256(canonical_json)
3. Sign: did_signature = Ed25519_Sign(did_private_key, manifest_hash)
4. Attach to manifest:
{
"signatures": {
"manifest_hash": "sha256:<hex>",
"did_signature": "<base64>"
}
}
Event Signing (Nostr Layer)
Standard NIP-01 Schnorr signature over the event ID (hash of serialized event fields). This is handled by the Nostr client library.
Verification Flow
Receiving Node:
1. Verify Nostr event signature (NIP-01) → Proves event authenticity
2. Extract manifest JSON from event content
3. Compute SHA-256 of manifest content
4. Compare with manifest.signatures.manifest_hash → Proves content integrity
5. Resolve DID document for manifest.author.did
6. Verify did_signature with DID public key → Proves developer identity
7. Check container.image tag is pinned (not :latest)
8. Validate security fields meet minimums
RPC Endpoints
Marketplace Discovery
| Method | Description | Auth |
|---|---|---|
marketplace.discover |
Query relays for app manifests, verify, score, return sorted | Local |
marketplace.publish |
Publish an app manifest to configured relays | Local |
marketplace.get-manifest |
Get full manifest for a specific app by ID | Local |
marketplace.verify |
Verify a manifest's signatures and security compliance | Local |
Manifest Management
| Method | Description | Auth |
|---|---|---|
marketplace.list-published |
List manifests published by this node | Local |
marketplace.unpublish |
Remove a published manifest from relays | Local |
Security Requirements
Container Security Enforcement
Before installing a community app, the node validates:
- No
latesttag: Image must use a specific version tag - Read-only root:
readonly_rootmust be true (or explicitly overridden by user) - No root:
run_as_usermust be > 1000 - No new privileges:
no_new_privilegesmust be true - Minimal capabilities: Only allowed capabilities are accepted (CHOWN, NET_BIND_SERVICE, etc.)
- No host networking: Apps cannot use
--network host - Volume restrictions: Apps cannot mount system paths (/, /etc, /var, /usr)
Image Verification
- Container images are pulled from registries, never transferred between nodes
- Future: Cosign signature verification for container images (leverages
core/security/) - Image digest pinning recommended for production apps
UI: Community Marketplace Tab
Route
Extends existing /dashboard/marketplace page.
Layout
Two tabs at the top of Marketplace.vue:
- Curated (existing): Built-in apps maintained by Archipelago team
- Community (new): Apps discovered from Nostr relays
Community Tab Components
- App Grid: Same card layout as curated tab, with trust score badge
- Search & Filter: Category filter + text search across community apps
- Trust Indicators: Color-coded badges (Verified/Community/Unverified/Untrusted)
- App Detail: Shows full manifest, developer DID, relay sources, version history
- Install Flow: Trust-level-dependent confirmation (one-click for Verified, warning for Untrusted)
Publishing UI
Accessible from Settings or a "Developer" section:
- Select a local app container to publish
- Fill in manifest metadata (description, category, icon)
- Review security compliance
- Sign and publish to relays
- View published manifests and their discovery status
Data Storage
/var/lib/archipelago/marketplace/
├── cache/
│ ├── manifests.json # Cached discovered manifests
│ └── trust-scores.json # Cached trust scores
├── published/
│ └── <app-id>.json # Manifests published by this node
└── config.json # Marketplace preferences (auto-refresh interval, etc.)
Implementation Notes
Relay Query Strategy
- Query all enabled relays in parallel (from
nostr_relays.rsconfig) - Deduplicate manifests by
app_id+version - If same manifest found on multiple relays, boost trust score
- Cache results with 15-minute TTL
- Background refresh every 30 minutes
Version Comparison
- Use semantic versioning for all version comparisons
- When multiple versions exist for the same
app_id, show the latest - Keep version history available in app detail view
- Flag apps with versions older than 6 months as potentially unmaintained