feat: one-command crew coordinator installer

This commit is contained in:
SkateSpots 2026-11-01 12:14:17 +07:00
parent ae1d7275e6
commit f7316060ae
5 changed files with 123 additions and 4 deletions

View file

@ -1418,7 +1418,9 @@ session with **no other memory of this project**. So:
it in when known. it in when known.
**Profile discovery (built 2026-09-29):** the carrier is the existing kind-0 **Profile discovery (built 2026-09-29):** the carrier is the existing kind-0
**`cordn`** field (app convention, documented in NIP.md — not a new kind). **`cordn`** field (app convention, documented in NIP.md — not a new kind).
**Crew-owned coordinator kit (2026-10-31):** `deploy/cordn/` is the **Crew-owned coordinator kit (2026-10-31, install helper 2026-11-01):**
`deploy/cordn/install.sh` is the one-command installer (generates `.env`,
starts SQLite, prints nprofile/add link). `deploy/cordn/` remains the
production hand-off for crews: Docker Compose, SQLite volume, `.env` template, production hand-off for crews: Docker Compose, SQLite volume, `.env` template,
backup/restore scripts and a runbook. Cordn connects **outbound** to ContextVM backup/restore scripts and a runbook. Cordn connects **outbound** to ContextVM
relays; no inbound port, domain or TLS terminator is required for the relays; no inbound port, domain or TLS terminator is required for the
@ -3489,6 +3491,16 @@ These components follow a consistent pattern using React's `forwardRef` and use
The project includes a **`skills/`** directory containing specialized implementation guides for specific features. Each skill has a comprehensive `SKILL.md` file with complete patterns, code examples, and best practices. **Always reference the appropriate SKILL.md file when implementing related functionality.** The project includes a **`skills/`** directory containing specialized implementation guides for specific features. Each skill has a comprehensive `SKILL.md` file with complete patterns, code examples, and best practices. **Always reference the appropriate SKILL.md file when implementing related functionality.**
### Deferred: NIP-05 as a wallet address
**Do not set `lud16` to `name@skate-spots.org` until an LNURL-pay service exists.**
NIP-05 is a Nostr identity record; a Lightning address is a separate
`/.well-known/lnurlp/<name>` service. A Minibits-style wallet address needs the
NIP-05 server **and** an LNURL/mint/claim service (plus rate limits, backups,
privacy copy and a clear mint-trust model). Optional future paths:
self-hosted LNURL + Cashu mint, or a deliberate npub.cash/Minibits-style
integration. Until then keep NIP-05 as identity and use NWC/built-in Cashu;
never overwrite a user's custom `lud16`.
### Wallet Skills ### Wallet Skills
- **`skills/bitcoin-wallet/SKILL.md`**: Use when implementing Bitcoin wallet features - provides complete architecture for on-chain Bitcoin transactions, UTXO management, address derivation from Nostr keys, and transaction handling with proper security patterns. Read when building Bitcoin wallet functionality, adding on-chain transaction support, implementing UTXO management, or integrating Bitcoin with Nostr-based applications. - **`skills/bitcoin-wallet/SKILL.md`**: Use when implementing Bitcoin wallet features - provides complete architecture for on-chain Bitcoin transactions, UTXO management, address derivation from Nostr keys, and transaction handling with proper security patterns. Read when building Bitcoin wallet functionality, adding on-chain transaction support, implementing UTXO management, or integrating Bitcoin with Nostr-based applications.

View file

@ -18,6 +18,21 @@ coordinator in the APK.
## 1. Install ## 1. Install
### Easiest: one command
```bash
curl -fsSL https://git.skate-spots.org/skatespots/SkateSpots/raw/branch/main/deploy/cordn/install.sh | bash
```
It checks Docker, generates the coordinator key, writes a private `.env`, starts
SQLite persistence, waits for the coordinator to announce, and prints the
`nprofile`/add link.
No inbound port, domain or TLS setup is required. The coordinator connects
outbound to the ContextVM relays in `CORDN_RELAY_URLS`.
### Manual: from this directory
```bash ```bash
cp .env.example .env cp .env.example .env
chmod 600 .env chmod 600 .env
@ -40,6 +55,17 @@ If your build does not print an add link, derive the public key from the private
key using the Cordn CLI or `nostr-tools`, then encode an `nprofile` with the same key using the Cordn CLI or `nostr-tools`, then encode an `nprofile` with the same
relay hints. Keep the private key on the server. relay hints. Keep the private key on the server.
### Can I run it on a phone?
Not with this Docker kit. A phone can **administer** a coordinator, and Cordn's
own phone app is the hand-off path, but an always-on coordinator needs a machine
that stays awake and reachable. Android may suspend a foreground coordinator;
do not promise crew members that a phone is a reliable coordinator host.
For a crew that wants ownership without a VPS, an old Raspberry Pi or a
small always-on home server is the simplest reliable option. The coordinator
still needs no inbound port.
## 2. Adopt it in SkateSpots ## 2. Adopt it in SkateSpots
Either: Either:

77
deploy/cordn/install.sh Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# One-command crew coordinator installer.
#
# curl -fsSL https://git.skate-spots.org/skatespots/SkateSpots/raw/branch/main/deploy/cordn/install.sh | bash
#
# Creates a persistent Cordn coordinator with SQLite storage, starts it, and
# prints the nprofile/add link when the server announces itself. The coordinator
# connects OUT to ContextVM relays; it needs no inbound port or TLS domain.
set -euo pipefail
cd "$(dirname "$0")" 2>/dev/null || true
umask 077
need() { command -v "$1" >/dev/null || { echo "missing: $1" >&2; exit 1; }; }
need docker
need openssl
docker compose version >/dev/null 2>&1 || { echo "Docker Compose v2 is required (docker compose)." >&2; exit 1; }
if [ ! -f docker-compose.yml ]; then
cat > docker-compose.yml <<'YAML'
name: cordn
services:
cordn:
image: ghcr.io/cordn-msg/cordn:latest
container_name: cordn
restart: unless-stopped
env_file:
- .env
environment:
CORDN_STORAGE_BACKEND: sqlite
CORDN_SQLITE_PATH: /data/cordn.sqlite
volumes:
- cordn-data:/data
volumes:
cordn-data:
YAML
fi
if [ ! -f .env ]; then
NAME="${CORDN_SERVER_NAME:-skatespots-crew}"
RELAYS="${CORDN_RELAY_URLS:-wss://relay.contextvm.org,wss://relay2.contextvm.org}"
KEY="$(openssl rand -hex 32)"
cat > .env <<EOF
CORDN_SERVER_PRIVATE_KEY=$KEY
CORDN_RELAY_URLS=$RELAYS
CORDN_ANNOUNCED=true
CORDN_SERVER_NAME=$NAME
CORDN_STORAGE_BACKEND=sqlite
CORDN_SQLITE_PATH=/data/cordn.sqlite
CORDN_RATE_LIMIT_ENABLED=true
CORDN_RATE_LIMIT_REFILL_PER_MINUTE=250
CORDN_RATE_LIMIT_BURST=80
CORDN_MAX_KEY_PACKAGES_PER_IDENTITY=50
CORDN_MAX_LAST_RESORT_KEY_PACKAGES_PER_IDENTITY=1
EOF
chmod 600 .env
echo "Generated .env (keep it private; it contains the coordinator key)."
fi
docker compose up -d
echo "Waiting for the coordinator to announce itself…"
for _ in $(seq 1 30); do
logs="$(docker compose logs --no-color 2>&1 || true)"
if printf '%s\n' "$logs" | grep -q 'nprofile'; then
printf '\n%s\n' "$logs" | grep -A12 -B2 'CORDN COORDINATOR'
echo
echo "Back up .env and the cordn-data volume before inviting the crew."
echo "Adopt it in SkateSpots: Settings → Coordinator, or Crew edit → Coordinator."
exit 0
fi
sleep 2
done
echo "The container started but no announcement appeared within 60s." >&2
echo "Inspect: docker compose logs --no-color" >&2
exit 1

View file

@ -20,7 +20,7 @@ import { useTranslation } from '@/contexts/language';
import { toast } from '@/hooks/useToast'; import { toast } from '@/hooks/useToast';
import { import {
CORDN_APP_URL, CORDN_APP_URL,
CORDN_DOCKER_COMMAND, CORDN_INSTALL_COMMAND,
CORDN_DEPLOY_KIT_URL, CORDN_DEPLOY_KIT_URL,
CORDN_RELAY_URL, CORDN_RELAY_URL,
coordinatorAddUrl, coordinatorAddUrl,
@ -315,11 +315,11 @@ export function CoordinatorSettingsCard() {
</p> </p>
<div className="flex items-center gap-2 rounded-xl bg-black/40 border border-white/5 px-3 py-2"> <div className="flex items-center gap-2 rounded-xl bg-black/40 border border-white/5 px-3 py-2">
<code className="flex-1 min-w-0 truncate text-[11px] font-mono text-gray-300"> <code className="flex-1 min-w-0 truncate text-[11px] font-mono text-gray-300">
{CORDN_DOCKER_COMMAND} {CORDN_INSTALL_COMMAND}
</code> </code>
<button <button
type="button" type="button"
onClick={() => void copyText(CORDN_DOCKER_COMMAND, t('socialUi.coordinator.commandCopied'), t('socialUi.coordinator.copyFailed'), t('socialUi.coordinator.copyManually'))} onClick={() => void copyText(CORDN_INSTALL_COMMAND, t('socialUi.coordinator.commandCopied'), t('socialUi.coordinator.copyFailed'), t('socialUi.coordinator.copyManually'))}
className="shrink-0 text-muted-foreground hover:text-white transition-colors" className="shrink-0 text-muted-foreground hover:text-white transition-colors"
aria-label={t('socialUi.coordinator.copyCommand')} aria-label={t('socialUi.coordinator.copyCommand')}
> >

View file

@ -27,6 +27,10 @@ export const CORDN_HOME = 'https://cordn.net';
export const CORDN_DOCKER_COMMAND = export const CORDN_DOCKER_COMMAND =
'docker run -d --name cordn --restart unless-stopped -v cordn-data:/data -e CORDN_STORAGE_BACKEND=sqlite -e CORDN_SQLITE_PATH=/data/cordn.sqlite -e CORDN_RELAY_URLS=wss://relay.contextvm.org -e CORDN_ANNOUNCED=true -e CORDN_SERVER_PRIVATE_KEY=YOUR_64_HEX_KEY ghcr.io/cordn-msg/cordn:latest'; 'docker run -d --name cordn --restart unless-stopped -v cordn-data:/data -e CORDN_STORAGE_BACKEND=sqlite -e CORDN_SQLITE_PATH=/data/cordn.sqlite -e CORDN_RELAY_URLS=wss://relay.contextvm.org -e CORDN_ANNOUNCED=true -e CORDN_SERVER_PRIVATE_KEY=YOUR_64_HEX_KEY ghcr.io/cordn-msg/cordn:latest';
/** Self-contained server installer: creates .env, starts SQLite, prints nprofile. */
export const CORDN_INSTALL_COMMAND =
'curl -fsSL https://git.skate-spots.org/skatespots/SkateSpots/raw/branch/main/deploy/cordn/install.sh | bash';
/** Production deployment kit (persistent SQLite volume + backup/restore). */ /** Production deployment kit (persistent SQLite volume + backup/restore). */
export const CORDN_DEPLOY_KIT_URL = export const CORDN_DEPLOY_KIT_URL =
'https://git.skate-spots.org/skatespots/SkateSpots/tree/main/deploy/cordn'; 'https://git.skate-spots.org/skatespots/SkateSpots/tree/main/deploy/cordn';