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
17 KiB
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-sdkmental 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.zipon download). The maintainer grabs the latest archive (or runsnpm run archivelocally) and publishes it to an nsite deployer that signs in-browser via NIP-07 (e.g. nsite.run) or via the localnsyteCLI. Rationale: bitvid ships to prod manually and infrequently, and is aggressively key-safe — keeping the nsite key out of CI (nonbunksec, 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'soutputDirectory, the exact bytes Vercel serves), afternpm 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
nsyteCLI with the dedicated nsite key on the maintainer's machine. Never put annsecin CI. If Phase-3 CI publishing is later adopted, use an nbunksec (restricted NIP-46 bunker credential) minted withnsyte ci, stored as the GitHub secretNBUNK_SECRET, rotated periodically with permitted kinds restricted where the bunker allows.
DECISION 6 — Redundancy. ✅ LOCKED. ≥3 Nostr relays + ≥2 Blossom servers,
publishRelayList+publishServerListon. 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 againstindex.html; the fallback covers gateways that 404 unknown deep-link paths.embed.htmlandviews/*.htmlare 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'sdist/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 tobitvid.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 artifactbitvid-site-<branch>-<shortsha>(30-day retention). GitHub serves it as a.zipon download,index.htmlat the root — drop-in for an nsite deployer. Grab the latest for a branch withgh run download(see below). -
mainonly → additionally cuts an immutable GitHub Release (tagsite-<date>-<shortsha>, marked--latest) withbitvid-site.zipattached, giving a stable download URL for the current production archive:https://github.com/PR0M3TH3AN/bitvid/releases/latest/download/bitvid-site.zipThe 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:
- Get the compiled site — download the latest
bitvid-site-*artifact, or runnpm run archivelocally. - 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.
- Verify with
nsyte status --fulland by loadinghttps://<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.jscarries 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 restrictednbunksec, never annsec; 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 localnsyte deploy). Verify withnsyte status --fulland 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 --fullintegrity 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_nostrdiscovery.
Maintainer steps (only the human can do these)
Archive automation is already live — these are the human-only publish steps:
- Generate a dedicated bitvid nsite key (not personal/admin).
- Get the compiled site: download the latest
bitvid-site-*GitHub artifact, or runnpm run archivelocally (→bitvid-site.zip). - 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 annsec(ornbunksec) in CI for the manual flow. - (Later) stand up a Synology Blossom server and add it to the server list.
- (Optional) publish the npub + gateway URL so users can find the mirror.
- (Optional, Phase 3) if automating:
nsyte ci→nbunksec1…, add it as theNBUNK_SECRETGitHub 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
--synckeep coverage.nsyte status --fullreports gaps. - Build determinism — the mirror must publish the same
npm run buildoutput Vercel serves; the Action rebuilds from the samemaincommit, so they match. - Source maps in
dist/— harmless to publish (already public); strip in build later if undesired. - CI credential leakage — restricted
nbunksecin the secret manager, rotated; a dedicated key caps the blast radius.
Sources
- NIP-5A (nsite / Blossom static sites), kinds 15128 / 35128.
nsyteCLI +sandwichfarm/nsite-action(deploy, snapshot, status, debug, ci).docs/blossom-plan.md(shared Blossom infrastructure).