Files
bitvid/docs/nsite-mirror-plan.md
T
thePR0M3TH3ANandClaude Opus 4.8 bff716948d ci(nsite): cut a GitHub Release with the site archive on main pushes
Add a `release` job (main only, contents: write) that builds the archive
and publishes it as an immutable GitHub Release tagged
site-<date>-<shortsha>, marked --latest, with bitvid-site.zip attached.
This gives a stable download URL for the current production archive:
  https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zip

Re-runs of the same commit refresh the asset instead of failing. The
Release holds only the public compiled dist/ (same as Vercel serves) and
no Nostr signing key — publishing to nsite stays a manual, client-signed
step. Per-branch artifact upload is unchanged. Docs updated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
2026-07-14 09:35:50 -04:00

17 KiB
Raw Blame History

nsite mirror — censorship-resistant app-shell backup (NIP-5A) — Dev Plan

Status: DECISIONS LOCKED (D1–D8; D2 revised to manual-publish 2026-07-14) — the on-push site-archive workflow is shipped (.github/workflows/nsite-archive.yml

  • npm run archive); the actual nsite publish is a deliberate manual step the maintainer runs when releasing (see "Maintainer steps"). Adds an independently-recoverable mirror of bitvid's compiled static app on Nostr + Blossom, published alongside Vercel. Full CI-signed publishing remains a documented Phase-3 option below.

Related: docs/blossom-plan.md (Blossom storage — nsite reuses the same blob infra) and docs/blossom-torrent-metadata-plan.md.

Executive summary

An nsite (NIP-5A) is a static website whose identity + file map live on Nostr and whose files live on Blossom as content-addressed (sha256) blobs. A signed Nostr manifest maps every path (/index.html, /js/app.js, …) to a blob hash; any gateway reads the manifest from relays, fetches the blobs from Blossom, and serves the site over HTTPS at e.g. https://<npub>.<gateway>. Root sites use kind 15128; a key can also host named sites with kind 35128.

bitvid is a fully static client (verified: no api/ dir, no Vercel functions, outputDirectory: "dist"). So an nsite is not a "degraded emergency shell" — it is a complete, runnable copy of bitvid that any gateway can serve. This is a natural extension of bitvid's decentralization stack:

Layer Decentralized by
Identity Nostr (already)
Distribution WebTorrent P2P (already)
Media storage Blossom / R2 / S3 (already)
App shell nsite — this plan

The mirror is a disaster-recovery / censorship-resistance layer, published alongside — never replacing — the primary Vercel deploy. Git remains the source-of-truth backup; nsite backs up exactly what users run, not how it was built.

Why it fits bitvid (verified facts)

  • Fully static ⇒ the nsite is a full mirror, not a degraded shell.
  • Every file fits the Blossom free tier. The largest file in a fresh dist/ is 1.85 MB (a placeholder gif); everything else < 0.75 MB — all well under Blossom's ~20 MiB free-per-blob cap. No paid Blossom tier needed for the mirror.
  • Reads work from any origin. Nostr relays (wss), WebTorrent webseeds, and Blossom reads are CORS-open, so a viewer at <npub>.<gateway> gets a working player.
  • Synergy with the Blossom work already shipped — same blob servers, same blossom-client-sdk mental model.

Decisions

DECISION 1 — Role. ✅ LOCKED. nsite is a DR/mirror of the production (main) build, not primary hosting. Vercel stays the polished public entry point on bitvid.network. Rationale: parallel + independent; a Vercel outage/censorship still leaves a loadable bitvid on any gateway.

DECISION 2 — Publish trigger: MANUAL, with an automated archive. ✅ LOCKED (revised 2026-07-14). The actual nsite publish is a manual step — a deliberate release ritual, not a push hook — so the signing key never lives in CI. To make that painless, a GitHub Action (.github/workflows/nsite-archive.yml) builds a fresh, downloadable site archive on every push (any branch) and uploads it as a workflow artifact (GitHub serves artifacts as a .zip on download). The maintainer grabs the latest archive (or runs npm run archive locally) and publishes it to an nsite deployer that signs in-browser via NIP-07 (e.g. nsite.run) or via the local nsyte CLI. Rationale: bitvid ships to prod manually and infrequently, and is aggressively key-safe — keeping the nsite key out of CI (no nbunksec, nothing to rotate/leak) matches that. Automating only the archive gives "always-ready to publish" without automating the signing. Never publish from Vercel's build command — a preview or rolled-back promotion could clobber the public mirror.

Full CI publishing (a signing Action with an nbunksec) remains a Phase-3 option if the manual step ever becomes a chore — the manual flow validates everything first, so switching later is low-risk.

DECISION 3 — Directory. ✅ LOCKED. Publish dist/ (bitvid's outputDirectory, the exact bytes Vercel serves), after npm ci && npm run build.

DECISION 4 — Identity. ✅ LOCKED. A dedicated bitvid nsite key (NOT the personal or admin npub), root site kind 15128. Publish the npub + a canonical gateway URL so users/docs can find the mirror. Rationale: project isolation, easy key rotation/hand-off, no exposure of a personal signing identity. Root (15128) is simplest for a single dedicated key (35128 named sites are for one key hosting several sites).

DECISION 5 — Signing credential. ✅ LOCKED (revised 2026-07-14). Default: sign client-side, never in CI — a NIP-07 extension (via a web deployer) or the local nsyte CLI with the dedicated nsite key on the maintainer's machine. Never put an nsec in CI. If Phase-3 CI publishing is later adopted, use an nbunksec (restricted NIP-46 bunker credential) minted with nsyte ci, stored as the GitHub secret NBUNK_SECRET, rotated periodically with permitted kinds restricted where the bunker allows.

DECISION 6 — Redundancy. ✅ LOCKED. ≥3 Nostr relays + ≥2 Blossom servers, publishRelayList + publishServerList on. Reuse Blossom servers bitvid already knows; add a self-hosted Synology Blossom as a personally-controlled copy later. Rationale: one relay + one server is just a second centralized dependency.

DECISION 7 — SPA fallback. ✅ LOCKED. fallback: /index.html. bitvid's routing is hash/query-based (?v=<nevent>, #view=…), which resolves against index.html; the fallback covers gateways that 404 unknown deep-link paths. embed.html and views/*.html are distinct files served directly.

DECISION 8 — Safety: secret scan + snapshots. ✅ LOCKED. Keep the action's secret scan ON (scan_level: medium) as a guard even though bitvid's dist/ is a public static client with no bundled secrets, and take an immutable snapshot per release so historical production builds stay addressable. Note: dist/ currently includes source maps (*.map) — publishing them is harmless (they're already public) but the scan surfaces them.

The one real caveat — uploads & CORS

The mirror loads and plays everywhere. But uploads from a gateway origin depend on the target storage's CORS:

  • Blossom uploads work from the mirror (Blossom servers send Access-Control- Allow-Origin: *).
  • R2 / S3 uploads will fail CORS from <npub>.<gateway> unless the user adds that gateway origin to their bucket's CORS allow-list (buckets are normally scoped to bitvid.network).

So the mirror is positioned as a resilient player / reader / DR layer first; full R2/S3 upload from the mirror is opt-in (add the gateway origin to bucket CORS). Share/canonical links keep pointing at bitvid.network (BITVID_WEBSITE_URL / DNS_URL are hardcoded) — which is correct, not a bug.

What nsite does and does NOT back up

Backs up Does NOT back up
The compiled dist/ (HTML/CSS/JS bundles, images, fonts, manifests) Source + Git history (that's Git)
Exactly what users run Env vars / credentials (none are in dist/)
Per-release snapshots (historical builds) User videos (already on R2/S3/Blossom)
Nostr events (already on relays)

Architecture

push to any branch
  ├── Vercel  → production build → bitvid.network            (primary, unchanged)
  └── GitHub Action (.github/workflows/nsite-archive.yml)   [SHIPPED]
        npm ci && npm run build → dist/
          → any branch: actions/upload-artifact → downloadable site .zip (30d)
          → main only:  GitHub Release (site-<date>-<sha>, --latest) w/ bitvid-site.zip
                        stable URL: /releases/latest/download/bitvid-site.zip
          (no Nostr signing key, does not publish to nsite)

manual release (maintainer, when promoting to prod)
  download latest archive  (or: npm run archive → bitvid-site.zip)
    → nsite deployer that signs in-browser via NIP-07 (e.g. nsite.run)
      or the local `nsyte` CLI
        → publish manifest (kind 15128) + blobs across Blossom servers
        → reachable at https://<bitvid-nsite-npub>.<gateway>

The archive workflow (shipped)

.github/workflows/nsite-archive.yml — runs on push to any branch + manual workflow_dispatch. It holds no Nostr signing key and does not publish to nsite; it only produces the compiled dist/ for a manual publish:

  • Every branch → uploads dist/ as an artifact bitvid-site-<branch>-<shortsha> (30-day retention). GitHub serves it as a .zip on download, index.html at the root — drop-in for an nsite deployer. Grab the latest for a branch with gh run download (see below).

  • main only → additionally cuts an immutable GitHub Release (tag site-<date>-<shortsha>, marked --latest) with bitvid-site.zip attached, giving a stable download URL for the current production archive:

    https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zip
    

    The Release contains only the public compiled app (identical to what Vercel serves), so it's safe to expose permanently; it doubles as the per-release snapshot from D8.

Grab the latest archive for a non-prod branch:

gh run download -R PR0M3TH3AN/bitvid \
  "$(gh run list -R PR0M3TH3AN/bitvid --workflow=nsite-archive.yml \
       --branch unstable --status success -L1 --json databaseId -q '.[0].databaseId')" \
  --dir ./site-archive

Locally, npm run archive produces the same thing as bitvid-site.zip (gitignored) for an offline/manual publish.

The publish step (manual)

Signing happens off CI, in the maintainer's control:

  1. Get the compiled site — download the latest bitvid-site-* artifact, or run npm run archive locally.
  2. Publish it with an nsite deployer that signs client-side:
    • Web: an nsite uploader such as nsite.run, authorizing with a NIP-07 extension (the key never leaves the browser); or
    • CLI: nsyte deploy ./dist … on the maintainer's machine (see recovery / integrity commands below), signing with the dedicated nsite key locally.
  3. Verify with nsyte status --full and by loading https://<npub>.<gateway>.

Optional Phase-3: fully-automated CI publishing (reference only)

If the manual step ever becomes a chore, a decoupled production-only Action can sign in CI with a restricted nbunksec (NIP-46 bunker credential — never an nsec). Not shipped; kept here as the migration target. The manual flow above validates relays/servers/fallback first, so switching later is low-risk.

name: Publish nsite mirror
on:
  push:
    branches: [main]
  workflow_dispatch:
permissions:
  contents: read
concurrency:
  group: nsite-production
  cancel-in-progress: true
jobs:
  publish:
    runs-on: ubuntu-latest
    # Skip entirely until the maintainer adds NBUNK_SECRET.
    if: ${{ github.repository == 'PR0M3TH3AN/bitvid' }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run build
      - name: Guard on secret
        id: guard
        run: echo "have=${{ secrets.NBUNK_SECRET != '' }}" >> "$GITHUB_OUTPUT"
      - name: Publish to Nostr + Blossom
        if: steps.guard.outputs.have == 'true'
        uses: sandwichfarm/[email protected]
        with:
          nbunksec: ${{ secrets.NBUNK_SECRET }}
          directory: ./dist
          sync: true
          fallback: /index.html
          skip_secrets_scan: false
          scan_level: medium
          publish_relay_list: true
          publish_server_list: true
          relays: |
            wss://relay.nsite.lol
            wss://relay.damus.io
            wss://nos.lol
          servers: |
            https://cdn.hzrd149.com
            https://blossom.band

Pin the action + tool version once verified; curate relays/servers to ones whose retention/payment policies are understood (do not blindly trust example public servers). Add the Synology Blossom to servers: when it's up.

Security

  • No secrets in dist/ — bitvid is a static client; instance-config.js carries public flags/thresholds/npubs only. Secret scan stays on as a guard.
  • No signing key in CI (default). The shipped archive workflow holds no secret; publishing is signed client-side (NIP-07 or local nsyte). If Phase-3 CI publishing is ever enabled, it signs with a restricted nbunksec, never an nsec; a dedicated project key limits blast radius and eases rotation.
  • Public + immutable once replicated — assume published blobs/manifests may remain retrievable even after deletion requests (some relays ignore deletions). Never publish a build containing anything you wouldn't put on a public CDN.
  • Gateway origin is untrusted infrastructure — bitvid already treats relay/ storage/tracker responses as untrusted; loading the shell from a gateway changes the origin but not the trust model (all signing is client-side via the user's own signer).

Recovery procedure

If Vercel is lost:

nsyte download ./recovered-site      # pull the whole published build
cd recovered-site && python3 -m http.server 8080   # or re-host anywhere
# or fetch individual files:
nsyte get /index.html --output ./index.html

The mirror is therefore an independently reconstructable production artifact, not merely a failover URL.

Integrity checks (post-release / weekly)

npm run build
nsyte scan ./dist          # catch accidental .env / sourcemaps / secrets
nsyte deploy ./dist --sync # replicate any missing blobs across servers
nsyte snapshot             # immutable release snapshot
nsyte status --full        # manifest + relay + server + per-file coverage
nsyte debug                # relay discovery, manifest availability, sampled blob hashes

Phases

  • Phase 0 — Archive wiring (behavior-neutral). ✅ SHIPPED. .github/workflows/nsite-archive.yml (build + upload artifact on push to any branch) + npm run archive + this doc. Publishes nothing; no secret involved.
  • Phase 1 — Go live (manual publish). Maintainer generates the dedicated nsite key, grabs the latest archive (or npm run archive), and publishes with a client-side-signing deployer (nsite.run + NIP-07, or local nsyte deploy). Verify with nsyte status --full and by loading <npub>.<gateway>. Repeat on each prod release.
  • Phase 2 — Harden. Add the self-hosted Synology Blossom to the server list; run periodic nsyte debug / nsyte status --full integrity checks; document/link the mirror URL in bitvid (e.g. a "decentralized mirror" footer link).
  • Phase 3 — Reach / automate (optional). Move signing into CI with a restricted nbunksec (the reference workflow above) if manual publishing becomes a chore; per-channel mirrors (a beta nsite); a named site (kind 35128); ENS/DNS _nostr discovery.

Maintainer steps (only the human can do these)

Archive automation is already live — these are the human-only publish steps:

  1. Generate a dedicated bitvid nsite key (not personal/admin).
  2. Get the compiled site: download the latest bitvid-site-* GitHub artifact, or run npm run archive locally (→ bitvid-site.zip).
  3. Publish with a client-side-signing deployer — nsite.run authorized via a NIP-07 extension, or nsyte deploy ./dist … locally. The key stays on your machine/extension; do not put an nsec (or nbunksec) in CI for the manual flow.
  4. (Later) stand up a Synology Blossom server and add it to the server list.
  5. (Optional) publish the npub + gateway URL so users can find the mirror.
  6. (Optional, Phase 3) if automating: nsyte ci → nbunksec1…, add it as the NBUNK_SECRET GitHub secret, and enable the reference publish workflow.

Risks / watch-items

  • Uploads from the gateway origin need bucket CORS (R2/S3); Blossom uploads work. Positioned as a player/DR layer first — documented above.
  • Gateway availability/trust — mitigate with multiple relays + servers and by publishing the relay/server lists so any gateway can resolve the site.
  • Blob/relay retention — free servers may prune; the self-hosted Synology Blossom + periodic --sync keep coverage. nsyte status --full reports gaps.
  • Build determinism — the mirror must publish the same npm run build output Vercel serves; the Action rebuilds from the same main commit, so they match.
  • Source maps in dist/ — harmless to publish (already public); strip in build later if undesired.
  • CI credential leakage — restricted nbunksec in the secret manager, rotated; a dedicated key caps the blast radius.

Sources

  • NIP-5A (nsite / Blossom static sites), kinds 15128 / 35128.
  • nsyte CLI + sandwichfarm/nsite-action (deploy, snapshot, status, debug, ci).
  • docs/blossom-plan.md (shared Blossom infrastructure).