278 lines
7.8 KiB
Markdown
278 lines
7.8 KiB
Markdown
|
|
# Archipelago App Developer Guide
|
||
|
|
|
||
|
|
Build and publish containerized apps for the Archipelago ecosystem.
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
Apps run as Podman containers on user nodes. You publish app manifests to Nostr relays, where nodes discover and install them through the community marketplace.
|
||
|
|
|
||
|
|
## App Manifest
|
||
|
|
|
||
|
|
Every app needs a manifest (YAML for local apps, JSON for marketplace publishing).
|
||
|
|
|
||
|
|
### Template Manifest
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
# apps/my-app/manifest.yml
|
||
|
|
app:
|
||
|
|
id: my-app # Unique, lowercase kebab-case
|
||
|
|
name: My App
|
||
|
|
version: 1.0.0 # Semantic versioning
|
||
|
|
|
||
|
|
container:
|
||
|
|
image: docker.io/myorg/my-app:1.0.0 # Never use :latest
|
||
|
|
ports:
|
||
|
|
- container: 8080
|
||
|
|
host: 8180
|
||
|
|
protocol: tcp
|
||
|
|
volumes:
|
||
|
|
- name: data
|
||
|
|
path: /data
|
||
|
|
env:
|
||
|
|
APP_MODE: production
|
||
|
|
capabilities: [] # Only add if absolutely necessary
|
||
|
|
readonly_root: true # Required
|
||
|
|
no_new_privileges: true # Required
|
||
|
|
run_as_user: 1000 # Must be >= 1000
|
||
|
|
|
||
|
|
metadata:
|
||
|
|
description:
|
||
|
|
short: "One-line description (max 120 chars)"
|
||
|
|
long: "Detailed description of what this app does and why."
|
||
|
|
author:
|
||
|
|
name: "Your Name"
|
||
|
|
did: "did:key:z6Mk..." # Your Archipelago node DID
|
||
|
|
category: money # money | commerce | data | networking | home | community | other
|
||
|
|
icon_url: "https://example.com/icon.png"
|
||
|
|
repo_url: "https://github.com/myorg/my-app"
|
||
|
|
license: MIT
|
||
|
|
min_archipelago_version: "0.1.0"
|
||
|
|
dependencies: [] # e.g., ["bitcoin-knots"] if this app needs Bitcoin
|
||
|
|
```
|
||
|
|
|
||
|
|
### Required Fields
|
||
|
|
|
||
|
|
| Field | Description |
|
||
|
|
|-------|-------------|
|
||
|
|
| `app.id` | Unique identifier, lowercase, kebab-case only |
|
||
|
|
| `app.name` | Human-readable name |
|
||
|
|
| `app.version` | Semantic version (major.minor.patch) |
|
||
|
|
| `container.image` | Full image reference with pinned version tag |
|
||
|
|
| `metadata.description.short` | One-line description, max 120 characters |
|
||
|
|
| `metadata.author.did` | Your node's DID (get via `node.did` RPC) |
|
||
|
|
|
||
|
|
## Security Requirements
|
||
|
|
|
||
|
|
These are enforced by the marketplace and the node. Non-compliant apps are flagged.
|
||
|
|
|
||
|
|
### Mandatory
|
||
|
|
|
||
|
|
1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0`
|
||
|
|
2. **Read-only root filesystem** — `readonly_root: true` (use volumes for writable data)
|
||
|
|
3. **Non-root user** — `run_as_user: 1000` or higher
|
||
|
|
4. **No privilege escalation** — `no_new_privileges: true`
|
||
|
|
5. **Minimal capabilities** — Drop all caps, only add required ones
|
||
|
|
|
||
|
|
### Allowed Capabilities
|
||
|
|
|
||
|
|
Only these Linux capabilities may be requested:
|
||
|
|
|
||
|
|
| Capability | When Needed |
|
||
|
|
|-----------|-------------|
|
||
|
|
| `CHOWN` | App needs to change file ownership |
|
||
|
|
| `NET_BIND_SERVICE` | App binds to ports below 1024 |
|
||
|
|
| `DAC_OVERRIDE` | App needs to bypass file permissions |
|
||
|
|
| `SETUID`, `SETGID` | App manages user switching (e.g., nginx) |
|
||
|
|
|
||
|
|
### Forbidden
|
||
|
|
|
||
|
|
- `--network host` — Apps cannot share the host network
|
||
|
|
- Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys`
|
||
|
|
- `SYS_ADMIN`, `SYS_PTRACE`, or any privileged capability
|
||
|
|
- Hardcoded secrets in environment variables or images
|
||
|
|
|
||
|
|
## Container Best Practices
|
||
|
|
|
||
|
|
### Volumes
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
volumes:
|
||
|
|
- name: data # App data persists across restarts
|
||
|
|
path: /data
|
||
|
|
- name: config # Configuration files
|
||
|
|
path: /config
|
||
|
|
```
|
||
|
|
|
||
|
|
Data is stored at `/var/lib/archipelago/{app-id}/` on the host.
|
||
|
|
|
||
|
|
### Health Checks
|
||
|
|
|
||
|
|
Define a health check endpoint in your container:
|
||
|
|
|
||
|
|
```dockerfile
|
||
|
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||
|
|
CMD curl -f http://localhost:8080/health || exit 1
|
||
|
|
```
|
||
|
|
|
||
|
|
### Logging
|
||
|
|
|
||
|
|
- Log to stdout/stderr (Podman captures container logs)
|
||
|
|
- Never log secrets, passwords, or keys
|
||
|
|
- Use structured logging (JSON) for machine parsing
|
||
|
|
|
||
|
|
### Networking
|
||
|
|
|
||
|
|
Apps get their own network namespace. To connect to other Archipelago apps:
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
# If your app needs to talk to Bitcoin
|
||
|
|
dependencies:
|
||
|
|
- bitcoin-knots
|
||
|
|
|
||
|
|
container:
|
||
|
|
env:
|
||
|
|
BITCOIN_RPC_HOST: bitcoin-knots # Container DNS name on archy-net
|
||
|
|
BITCOIN_RPC_PORT: "8332"
|
||
|
|
```
|
||
|
|
|
||
|
|
The `archy-net` Podman network provides DNS resolution between containers.
|
||
|
|
|
||
|
|
## Publishing to the Marketplace
|
||
|
|
|
||
|
|
### 1. Build and Push Your Image
|
||
|
|
|
||
|
|
```bash
|
||
|
|
podman build -t docker.io/myorg/my-app:1.0.0 .
|
||
|
|
podman push docker.io/myorg/my-app:1.0.0
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Get Your Node's DID
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||
|
|
-d '{"method":"node.did"}'
|
||
|
|
# Returns: {"result":{"did":"did:key:z6Mk..."}}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Publish via RPC
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{
|
||
|
|
"method": "marketplace.publish",
|
||
|
|
"params": {
|
||
|
|
"app_id": "my-app",
|
||
|
|
"name": "My App",
|
||
|
|
"version": "1.0.0",
|
||
|
|
"description": {"short": "A useful tool", "long": "Detailed description..."},
|
||
|
|
"author": {"name": "Dev Name", "did": "did:key:z6Mk...", "nostr_pubkey": ""},
|
||
|
|
"container": {
|
||
|
|
"image": "docker.io/myorg/my-app:1.0.0",
|
||
|
|
"ports": [{"container": 8080, "host": 8180, "protocol": "tcp"}],
|
||
|
|
"volumes": [],
|
||
|
|
"env": {},
|
||
|
|
"capabilities": [],
|
||
|
|
"readonly_root": true,
|
||
|
|
"no_new_privileges": true,
|
||
|
|
"run_as_user": 1000
|
||
|
|
},
|
||
|
|
"category": "other",
|
||
|
|
"icon_url": "",
|
||
|
|
"repo_url": "https://github.com/myorg/my-app",
|
||
|
|
"license": "MIT",
|
||
|
|
"min_archipelago_version": "0.1.0",
|
||
|
|
"dependencies": []
|
||
|
|
}
|
||
|
|
}'
|
||
|
|
```
|
||
|
|
|
||
|
|
The manifest is published to all configured Nostr relays as a NIP-78 event (kind 30078).
|
||
|
|
|
||
|
|
### 4. Verify Discovery
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||
|
|
-d '{"method":"marketplace.discover"}'
|
||
|
|
# Your app should appear in the results
|
||
|
|
```
|
||
|
|
|
||
|
|
## Trust Model
|
||
|
|
|
||
|
|
Published apps receive trust scores (0-100) based on:
|
||
|
|
|
||
|
|
| Factor | Points | How to Maximize |
|
||
|
|
|--------|--------|-----------------|
|
||
|
|
| Valid DID in author | 30 | Always include your node's DID |
|
||
|
|
| Found on multiple relays | 5-20 | Configure many relays in your node |
|
||
|
|
| Developer in federation | 20 | Have federated peers who trust you |
|
||
|
|
| Proper semver version | 10 | Use `major.minor.patch` format |
|
||
|
|
| Repository URL present | 5 | Include your repo URL |
|
||
|
|
| Security compliance | 15 | Meet all security requirements |
|
||
|
|
|
||
|
|
### Trust Tiers
|
||
|
|
|
||
|
|
| Score | Tier | User Experience |
|
||
|
|
|-------|------|----------------|
|
||
|
|
| 80-100 | Verified | One-click install |
|
||
|
|
| 50-79 | Community | Install with confirmation |
|
||
|
|
| 20-49 | Unverified | Install with warning |
|
||
|
|
| 0-19 | Untrusted | Requires explicit override |
|
||
|
|
|
||
|
|
## Testing Your App
|
||
|
|
|
||
|
|
### Local Testing
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run your container locally
|
||
|
|
podman run -d --name my-app \
|
||
|
|
-p 8180:8080 \
|
||
|
|
--read-only \
|
||
|
|
--security-opt no-new-privileges \
|
||
|
|
--user 1000:1000 \
|
||
|
|
docker.io/myorg/my-app:1.0.0
|
||
|
|
|
||
|
|
# Verify it works
|
||
|
|
curl http://localhost:8180/health
|
||
|
|
|
||
|
|
# Check logs
|
||
|
|
podman logs my-app
|
||
|
|
```
|
||
|
|
|
||
|
|
### On an Archipelago Node
|
||
|
|
|
||
|
|
1. Install via the marketplace UI or RPC:
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||
|
|
-d '{"method":"package.install","params":{"id":"my-app","dockerImage":"docker.io/myorg/my-app:1.0.0"}}'
|
||
|
|
```
|
||
|
|
2. Verify the container is running:
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://192.168.1.228/rpc/v1 \
|
||
|
|
-d '{"method":"container-list"}'
|
||
|
|
```
|
||
|
|
3. Check the UI at `http://192.168.1.228/app/my-app/`
|
||
|
|
|
||
|
|
### Validate Manifest
|
||
|
|
|
||
|
|
```bash
|
||
|
|
curl -b cookies.txt -X POST http://localhost/rpc/v1 \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"method":"marketplace.verify","params":{...your manifest...}}'
|
||
|
|
# Returns: {"result":{"valid":true,"issues":[],"trust_score":65,"trust_tier":"community"}}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Updating Your App
|
||
|
|
|
||
|
|
1. Build and push the new version: `docker.io/myorg/my-app:1.1.0`
|
||
|
|
2. Publish an updated manifest with the new version
|
||
|
|
3. NIP-33 replaceable events: the latest publish overwrites the previous one on relays
|
||
|
|
4. Nodes running your app can see the update in their marketplace
|
||
|
|
|
||
|
|
## App Icon
|
||
|
|
|
||
|
|
- Provide a URL to your app icon (PNG, WebP, or SVG)
|
||
|
|
- Recommended size: 256x256 pixels
|
||
|
|
- Square aspect ratio
|
||
|
|
- If no icon URL, a generic placeholder is shown in the marketplace
|