feat: production crew-owned Cordn coordinator kit
This commit is contained in:
parent
609b15be08
commit
c2750eae85
9 changed files with 230 additions and 2 deletions
|
|
@ -1418,6 +1418,15 @@ session with **no other memory of this project**. So:
|
|||
it in when known.
|
||||
**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).
|
||||
**Crew-owned coordinator kit (2026-10-31):** `deploy/cordn/` is the
|
||||
production hand-off for crews: Docker Compose, SQLite volume, `.env` template,
|
||||
backup/restore scripts and a runbook. Cordn connects **outbound** to ContextVM
|
||||
relays; no inbound port, domain or TLS terminator is required for the
|
||||
coordinator itself. The app still does **not** embed a coordinator. Settings →
|
||||
Coordinator links to the kit; the crew's `coordinator` tag/operator remains
|
||||
the adoption path. Keep the coordinator key separate from crew/admin keys.
|
||||
Do not claim multi-coordinator failover; this is one trusted delivery service
|
||||
with backups, not BFT.
|
||||
`EditProfileForm` accepts a pointer (nprofile / cordn.net link / npub / hex /
|
||||
http URL), **normalises a bare pointer to Cordn's canonical add-link** before
|
||||
saving (`coordinatorAddUrl`), and has **"Use my coordinator"**. `NIP19Page`
|
||||
|
|
|
|||
19
deploy/cordn/.env.example
Normal file
19
deploy/cordn/.env.example
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Copy this file to .env, then generate a key:
|
||||
# openssl rand -hex 32
|
||||
# Keep .env mode 600 and back it up with the coordinator data.
|
||||
CORDN_SERVER_PRIVATE_KEY=
|
||||
|
||||
# ContextVM relays the coordinator connects to. Add a second one if you want
|
||||
# more reachability; the coordinator does not need an inbound port.
|
||||
CORDN_RELAY_URLS=wss://relay.contextvm.org,wss://relay2.contextvm.org
|
||||
|
||||
# Publicly announce the coordinator on its relays.
|
||||
CORDN_ANNOUNCED=true
|
||||
CORDN_SERVER_NAME=my-crew-coordinator
|
||||
|
||||
# Per-identity abuse protection. Raise for larger crews; keep it bounded.
|
||||
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
|
||||
127
deploy/cordn/README.md
Normal file
127
deploy/cordn/README.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Crew-owned Cordn coordinator
|
||||
|
||||
This kit runs a **Cordn MLS delivery-service coordinator** that a crew owns. It
|
||||
is transport only: the coordinator sees ciphertext and routing metadata (who,
|
||||
when, which group), never message plaintext. It can withhold or delay messages;
|
||||
it cannot forge members' messages.
|
||||
|
||||
This is a **server** kit. SkateSpots remains a Cordn *client*; it does not embed a
|
||||
coordinator in the APK.
|
||||
|
||||
## What you need
|
||||
|
||||
- A small Linux server or always-on home machine with Docker + Compose.
|
||||
- Outbound HTTPS/WSS access. **No inbound port is required** — the coordinator
|
||||
connects out to its ContextVM relay(s).
|
||||
- A domain is **not** required for the coordinator itself.
|
||||
- A crew willing to trust the operator with availability and metadata.
|
||||
|
||||
## 1. Install
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
Put the generated value into `CORDN_SERVER_PRIVATE_KEY`. Set
|
||||
`CORDN_RELAY_URLS` to the relay(s) you want; the default is ContextVM’s public
|
||||
relay. Set a useful `CORDN_SERVER_NAME`.
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose logs -f cordn
|
||||
```
|
||||
|
||||
The server prints its coordinator public identity/add link. It may show an
|
||||
`nprofile` or a `cordn.net/chat/coordinators?c=…` link.
|
||||
|
||||
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
|
||||
relay hints. Keep the private key on the server.
|
||||
|
||||
## 2. Adopt it in SkateSpots
|
||||
|
||||
Either:
|
||||
|
||||
- Settings → **Coordinator** → paste the `nprofile`/add-link → **Add**; or
|
||||
- Crew edit → **Coordinator** → paste it and save; or
|
||||
- scan the coordinator QR.
|
||||
|
||||
Use exactly the relay hints from the server. A mismatch produces an honest
|
||||
“Not seen” health state even when the server is actually running.
|
||||
|
||||
The coordinator should show **Online** (or “Stale” if its heartbeat is old).
|
||||
“Offline” means presence was not found, not necessarily that the service is
|
||||
broken.
|
||||
|
||||
## 3. What the crew should know
|
||||
|
||||
- The coordinator can **censor or drop messages**.
|
||||
- It cannot **read** MLS plaintext.
|
||||
- It can see **who, when and which group**.
|
||||
- It can disappear; local messages remain on members' devices, but delivery
|
||||
pauses until the coordinator returns or the crew switches.
|
||||
- The operator should **not** also become a crew admin automatically.
|
||||
- A loss of the database is a loss of delivery history; back it up.
|
||||
|
||||
## 4. Backup
|
||||
|
||||
```bash
|
||||
./backup.sh
|
||||
```
|
||||
|
||||
Backups land in `./backups/` as a compressed copy of the coordinator volume.
|
||||
Store them off the server, encrypted.
|
||||
|
||||
## 5. Restore
|
||||
|
||||
Stop the coordinator, restore the archive into the `cordn-data` volume, start it
|
||||
again:
|
||||
|
||||
```bash
|
||||
docker compose stop cordn
|
||||
./restore.sh backups/cordn-YYYYMMDD-HHMMSS.tgz
|
||||
docker compose start cordn
|
||||
```
|
||||
|
||||
## 6. Upgrade
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The image is pinned only by the `latest` tag. If you need reproducible upgrades,
|
||||
pin a digest and record it in your crew runbook.
|
||||
|
||||
## 7. Move to a new coordinator
|
||||
|
||||
MLS delivery state is tied to the coordinator’s database and identity. Before
|
||||
switching:
|
||||
|
||||
1. back up the current coordinator;
|
||||
2. export/retain the crew’s local chat state on members’ devices;
|
||||
3. start the new coordinator;
|
||||
4. paste its `nprofile` into the crew record;
|
||||
5. have members reopen the private chat and re-establish delivery;
|
||||
6. update the crew operator/notice.
|
||||
|
||||
Do not claim the switch is seamless unless the old and new coordinator state
|
||||
have actually been migrated.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Never commit `.env`; it contains the coordinator signing key.
|
||||
- The coordinator key is **not** a crew admin key and must not be reused as one.
|
||||
- Run it behind a trusted host/network; outbound relay traffic is metadata.
|
||||
- Keep Docker and the image current.
|
||||
- Do not expose an OpenCode or coordinator admin port publicly.
|
||||
- This is **not** a BFT or multi-coordinator system. One coordinator is a
|
||||
delivery service under the crew's social trust.
|
||||
|
||||
## Files
|
||||
|
||||
- `docker-compose.yml` — persistent SQLite deployment.
|
||||
- `.env.example` — required environment.
|
||||
- `backup.sh` / `restore.sh` — volume snapshots.
|
||||
14
deploy/cordn/backup.sh
Executable file
14
deploy/cordn/backup.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
# Snapshot the Cordn coordinator data volume. Run from this directory.
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
mkdir -p backups
|
||||
stamp="$(date +%Y%m%d-%H%M%S)"
|
||||
out="cordn-$stamp.tgz"
|
||||
docker run --rm \
|
||||
-v cordn_cordn-data:/data:ro \
|
||||
-v "$PWD/backups":/backup \
|
||||
alpine:3.20 \
|
||||
sh -c "tar czf /backup/$out -C /data ."
|
||||
echo "Backup: $PWD/backups/$out"
|
||||
echo "Store it off this server and keep it encrypted."
|
||||
19
deploy/cordn/docker-compose.yml
Normal file
19
deploy/cordn/docker-compose.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
# No inbound port is required: the coordinator connects *out* to the
|
||||
# ContextVM relay(s) configured in CORDN_RELAY_URLS.
|
||||
|
||||
volumes:
|
||||
cordn-data:
|
||||
17
deploy/cordn/restore.sh
Executable file
17
deploy/cordn/restore.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/sh
|
||||
# Restore a Cordn coordinator backup into the cordn-data volume.
|
||||
# Usage: ./restore.sh backups/cordn-YYYYMMDD-HHMMSS.tgz
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
[ "$#" -eq 1 ] || { echo "usage: $0 backups/cordn-YYYYMMDD-HHMMSS.tgz" >&2; exit 1; }
|
||||
archive="$1"
|
||||
[ -f "$archive" ] || { echo "no such backup: $archive" >&2; exit 1; }
|
||||
printf 'This replaces the current coordinator volume. Type "restore" to continue: '
|
||||
read -r answer
|
||||
[ "$answer" = restore ] || { echo "aborted" >&2; exit 1; }
|
||||
docker run --rm \
|
||||
-v cordn_cordn-data:/data \
|
||||
-v "$(cd "$(dirname "$archive")" && pwd)":/backup:ro \
|
||||
alpine:3.20 \
|
||||
sh -c "rm -rf /data/* /data/.[!.]* /data/..?* 2>/dev/null || true; tar xzf /backup/$(basename "$archive") -C /data"
|
||||
echo "Restored $(basename "$archive")."
|
||||
|
|
@ -21,6 +21,7 @@ import { toast } from '@/hooks/useToast';
|
|||
import {
|
||||
CORDN_APP_URL,
|
||||
CORDN_DOCKER_COMMAND,
|
||||
CORDN_DEPLOY_KIT_URL,
|
||||
CORDN_RELAY_URL,
|
||||
coordinatorAddUrl,
|
||||
parseCoordinatorInput,
|
||||
|
|
@ -314,6 +315,14 @@ export function CoordinatorSettingsCard() {
|
|||
<Copy className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<a href={CORDN_DEPLOY_KIT_URL} target="_blank" rel="noopener noreferrer">
|
||||
<Button size="sm" variant="outline">
|
||||
{t('common.learnMore')}
|
||||
<ExternalLink className="ml-1.5 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<p className="flex items-start gap-2 text-[11px] text-amber-400/90 leading-relaxed">
|
||||
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
||||
{t('socialUi.coordinator.androidSleep')}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { useCrewGoal } from '@/hooks/useCrewGoal';
|
|||
import { useCoordinator, useCoordinatorHealth } from '@/hooks/useCoordinator';
|
||||
import { normalizeUrlInput, sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
|
||||
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub, SKATESPOTS_COORDINATOR } from '@/lib/coordinator';
|
||||
import { coordinatorAddUrl, crewCoordinatorInputValue, parseCrewCoordinatorInput, shortNpub, SKATESPOTS_COORDINATOR, CORDN_DEPLOY_KIT_URL } from '@/lib/coordinator';
|
||||
import { useRelays } from '@/lib/relays';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server, ChevronDown, Zap } from 'lucide-react';
|
||||
|
|
@ -1184,6 +1184,15 @@ export function CrewForm({ existing }: CrewFormProps) {
|
|||
>
|
||||
{t('socialUi.coordinator.cordnCoordinator')}
|
||||
</a>
|
||||
{' · '}
|
||||
<a
|
||||
href={CORDN_DEPLOY_KIT_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-teal-400 underline underline-offset-2 hover:text-teal-300"
|
||||
>
|
||||
{t('common.learnMore')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ export const CORDN_ADD_BASE = 'https://cordn.net/chat/coordinators';
|
|||
export const CORDN_HOME = 'https://cordn.net';
|
||||
|
||||
/** The self-host one-liner advertised on cordn.net. */
|
||||
export const CORDN_DOCKER_COMMAND = 'docker run --rm ghcr.io/cordn-msg/cordn:latest';
|
||||
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';
|
||||
|
||||
/** Production deployment kit (persistent SQLite volume + backup/restore). */
|
||||
export const CORDN_DEPLOY_KIT_URL =
|
||||
'https://git.skate-spots.org/skatespots/SkateSpots/tree/main/deploy/cordn';
|
||||
|
||||
/** Where a user gets a coordinator they can run on a phone. */
|
||||
export const CORDN_APP_URL = 'https://cordn.net';
|
||||
|
|
|
|||
Loading…
Reference in a new issue