Files
archy/docs/archive/demo-deployment-design.md
T
archipelagoandClaude Opus 5 8e814ca06a
Demo images / Build & push demo images (push) Failing after 2m22s
feat(registry): move image and OTA references to the public domain
Replaces the registry host across 86 files: 309 references, covering all 40
app manifests, the orchestrator and container crates, the release and catalog
scripts, both demo-images workflows, the ISO builder, demo-deploy, and the
frontend marketplace data.

Verified the domain actually serves the registry before rewriting anything,
rather than assuming the web host implies the registry:
- TLS verifies clean, HTTP/2 on the web root
- an anonymous token grants a manifest fetch (HTTP 200) with no credentials
- skopeo inspect --no-creds resolves an image and lists its tags

That last check is the one that matters: an outside developer with no account
can now pull, which was the functional blocker for publishing at all.

Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop
crossing the network in the clear.

Deliberately NOT rewritten:
- The public FIPS anchor on port 8444. It is a functional network endpoint
  every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded
  seeds than to leaked infrastructure. The domain does resolve to the same
  host, so it could become a hostname, but that adds a DNS dependency to the
  path used precisely when things are broken. Worth a deliberate decision,
  not a side effect of this change.
- The companion APK on port 2100. The domain returns 404 for that path, so
  rewriting it would swap a working URL for a broken one. The Releases page
  does serve (200), which is where the plan already wants those binaries.
- releases/app-catalog.json, releases/manifest.json and release-manifest.json.
  These carry `signature` and `signed_by`; editing their contents invalidates
  the signature and the fleet refuses artifacts that fail verification. They
  were rewritten in a first pass and reverted — they must be regenerated and
  re-signed through the signing ceremony instead, which needs the mnemonic.

So the catalog still advertises the old host until that ceremony runs. Nodes
resolve images through the signed catalog, not the on-disk manifests, so this
commit alone does not change what a node pulls.

Verified: archipelago-container 75/75; every manifest still parses with a
top-level app block; no signed artifact modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 11:31:20 -04:00

8.1 KiB

Public Demo Deployment — Design

Status: design (2026-06-22) Goal: a public, click-to-play demo of the Archipelago UI that auto-tracks the real code yet stays separated from the private monorepo and its secrets/backend. Deployed via Portainer, mock-data driven, with working file storage and a testnet-flavored Bitcoin sandbox so visitors can play freely.

See also: neode-ui/mock-backend.js (existing mock), docker-compose.demo.yml (existing demo stack), MEMORY → reference_neode_ui_dev_testing, MEMORY → reference_ovh_168_mirror (Portainer/registry host).


1. What already exists (the 70%)

The demo is mostly built. Inventory:

Asset Path State
Mock backend (Node/Express + ws) neode-ui/mock-backend.js (~3,862 lines) 95+ JSON-RPC methods: auth, package lifecycle, Bitcoin/LND wallet, mesh, federation, identity, monitoring, mock filebrowser
Mock data mockData / walletState / MOCK_FILES in mock-backend.js rich; 10 pre-installed apps, 30+ marketplace apps, wallet balances, seeded files (Music/Documents/Photos/Videos)
Demo compose docker-compose.demo.yml neode-backend (mock, :5959) + neode-web (nginx, :4848); header already says "Deploy via Portainer"
Backend image neode-ui/Dockerfile.backend Node 22 Alpine → node mock-backend.js
Web image neode-ui/Dockerfile.web multi-stage vite build → nginx
Demo nginx neode-ui/docker/nginx-demo.conf proxies /rpc/v1, /ws, /app/* to the mock backend
Precedent indee-demo Portainer stack separate stack referencing a pre-built image — the pattern we extend

Gaps for a public (not dev) demo: state is global (visitors collide), uploads are no-ops, Bitcoin block height is hardcoded, no CI image pipeline, no separated public deploy repo.


2. Architecture: source in monorepo, demo ships as images, public repo is thin

The tension — "must update as I update the real code" and "sort of separated" — is resolved by separating at the deploy layer, not the source layer.

  monorepo (private — single source of truth)
    neode-ui/ + mock-backend.js
            │  push to main
            ▼
  CI: build archy-demo-web + archy-demo-backend
            │  push :demo / :latest
            ▼
  registry (source.archipelago-foundation.org / vps2)
            │  Portainer webhook / re-pull
            ▼
  archy-demo (public repo — tiny)
    docker-compose.yml  ──referencing pre-built images──▶  Portainer ▶ demo.<host>
    .env.example
  • Single source of truth = the monorepo. neode-ui/ and mock-backend.js stay where they are, so the demo tracks real code automatically — no fork to sync, no drift.
  • Separation = the public repo never holds source. archy-demo contains only a docker-compose.yml (image refs) + .env.example + README. No Rust backend, no secrets, no UI source. Safe to make public.
  • Auto-update flow: edit code → push → CI rebuilds demo images → Portainer redeploys. The public compose file is touched rarely (only when service shape changes).

Why not a true fork / git subtree split? It works but needs a sync job and re-exposes UI source publicly. The image pipeline gives stronger separation (zero source leak) and zero manual sync. (Decided 2026-06-22.)


3. Work items

3.1 CI image pipeline

  • On push to main (path filter: neode-ui/**), build:
    • archy-demo-backend from neode-ui/Dockerfile.backend
    • archy-demo-web from neode-ui/Dockerfile.web (build:docker)
  • Tag :demo + :<git-sha>, push to the registry.
  • Trigger Portainer redeploy (stack webhook) on success.

3.2 Public archy-demo repo

  • docker-compose.yml mirroring docker-compose.demo.yml but image: references instead of build: (pull :demo, no build context).
  • .env.example (ANTHROPIC_API_KEY, VITE_DEV_MODE=existing, session TTL, upload quota).
  • README: one-paragraph "deploy in Portainer → web editor paste / deploy from repo," access on :4848.
  • No source. This is the only public surface.

3.3 Multi-user: per-session sandbox (reset on idle) ⟵ decided

The biggest code change. Today mockData / walletState / MOCK_FILES are global singletons → visitors corrupt each other's view.

  • Issue a demo-session cookie on first hit (the mock already sets a session on login; extend it to anonymous visitors).
  • Key state by session id: sessions[sid] = { mockData, walletState, files }, each deep-cloned from a pristine seed on creation.
  • Reap on idle (e.g. 30 min no activity) + hard cap concurrent sessions; on reap, free memory + temp dir.
  • RPC dispatch + WS patches resolve the per-session state instead of the global.
  • Keeps the demo a true playground: install/uninstall/spend freely, reset by reconnecting.

3.4 File storage: persisted per session ⟵ decided

Today filebrowser upload/delete/rename are 200-OK no-ops.

  • Back each session with a temp dir (e.g. /tmp/demo/<sid>/), seeded from MOCK_FILES.
  • Make POST/DELETE/PATCH /app/filebrowser/api/resources/* and GET …/raw/* read/write that dir. Enforce a per-session quota (e.g. 50 MB) and reject oversize/odd MIME.
  • Cleaned when the session is reaped — no standing public writable volume, no real filebrowser container to harden.

3.5 Bitcoin: testnet-flavored mock ⟵ decided

  • Relabel wallet/chain as testnet/signet: tb1q… addresses, "testnet" chain in bitcoin.getinfo, scripted-but-plausible block height + confirmations.
  • Keep dev.faucet as the in-UI "get test sats" button (instant, free).
  • No real bitcoind → no sync, no disk, no public RPC attack surface.
  • Future upgrade path: swap to a real signet node + LND in the stack if we ever want movable real test sats (out of scope now).

3.6 Mock containers / app lifecycle

  • The mock already simulates package.install/uninstall/start/stop/restart asynchronously. For the demo, force simulation mode (never touch a real Docker socket — rootless/safe and host-independent). Confirm no path in mock-backend.js reaches for a real runtime when DEMO=1.

3.7 Mock-data refresh

  • Update mockData static apps + marketplace to current app set/versions, refresh wallet figures, seeded mesh messages, and files so the demo feels current. This is ongoing and rides the same image pipeline.

4. Invariants / guardrails (public exposure)

  • No real secrets, no real backend, no real Docker socket in the demo image or public repo. Mock password stays a known demo credential, clearly labeled.
  • Per-session isolation is a hard requirement before going public — without it the demo is unusable for strangers.
  • Resource caps: session count, per-session memory + upload quota, idle reap; the box can't be DoS'd into OOM by upload spam or session churn.
  • ANTHROPIC_API_KEY (chat) is injected via Portainer env, never committed; rate-limit / budget-cap demo chat usage.
  • Read-only registry creds for the Portainer host to pull :demo.

5. Files / seams

Concern Where
Per-session state, file persistence, testnet labels, sim-mode neode-ui/mock-backend.js
Build contexts (reused as-is) neode-ui/Dockerfile.backend, neode-ui/Dockerfile.web, neode-ui/docker/nginx-demo.conf
Demo stack (in-repo, dev) docker-compose.demo.yml (keep build:)
Public stack (new repo) archy-demo/docker-compose.yml (image: refs), .env.example, README
CI pipeline new workflow (path filter neode-ui/** → build + push :demo → Portainer webhook)

6. Open questions

  1. Demo host — which Portainer instance (OVH .168? a dedicated VPS)? Public DNS + TLS for demo.<domain>?
  2. Registry for :demo imagessource.archipelago-foundation.org vs vps2; public-pull or creds baked into Portainer?
  3. Session TTL + concurrency cap — concrete numbers (30 min / N sessions / 50 MB)?
  4. Chat in the demo — enable Claude chat (needs key + budget cap) or stub it?
  5. Sync cadence — rebuild :demo on every neode-ui/** push, or nightly?