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
Add .github/workflows/nsite-archive.yml (push to any branch +
workflow_dispatch): npm ci + npm run build, then upload dist/ as an
artifact (bitvid-site-<branch>-<shortsha>, 30d retention). GitHub serves
it as a .zip with index.html at the root, ready for a manual nsite
publish. The workflow holds no signing key and publishes nothing.
Also add an `archive` npm script for the same output locally
(bitvid-site.zip, now gitignored), and land docs/nsite-mirror-plan.md
revised to the manual-publish + auto-archive model: sign client-side via
NIP-07 or local nsyte, never an nsec/nbunksec in CI. The prior CI-signing
workflow is retained as a labeled Phase-3 reference option.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DwbE3uTiFaKRNjx5vP2muV
Blossom servers reject a .torrent (415), so a Blossom video's magnet is webseed-only and can't bootstrap WebTorrent. This restores P2P without hosting a .torrent:
- Upload hardening: correct Content-Type (overrides empty/generic application/octet-stream), HTTP status surfaced in errors, and best-effort .torrent sidecar so a rejected sidecar no longer sinks the upload (URL-first fallback).
- Publish: when the .torrent can't be hosted, publish the piece-map as an infohash-keyed companion event (kind 30078, d=bitvid:torrent:<infohash>) and ship a ws= magnet. Gated by FEATURE_BLOSSOM_TORRENT_METADATA (default off = no trace); size-capped.
- Playback: torrentMetadataService lazily fetches the companion, VERIFIES the reconstructed infohash === the magnet's btih (untrusted content), sources the webseed from the video event, and hands the .torrent buffer to WebTorrent so it streams with zero peers. Relay reads go through the subscription manager.
Inherited by any upload-based media (shorts, audio-publish); excludes ingest/live. See docs/blossom-torrent-metadata-plan.md. Includes a Prettier reflow in css/tailwind.source.css.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Writes docs/blossom-plan.md (house format) from the July 2026 research: use the
maintained blossom-client-sdk v5 (only dep @noble/hashes, already vendored); the
spec's current modular BUD map (BUD-01 retrieval, 02 /upload, 11 auth kind-24242,
12 list/delete, 03 kind-10063 discovery, 04 mirror, 05 /media, 06 preflight);
the existing service.uploadFile seam as the integration point; and 10 decisions
(SDK vs hand-roll, multi-server+mirror, discovery, /upload vs /media, signer
reuse, torrent parity, management, keyless config UI, FEATURE_BLOSSOM_STORAGE,
public-only privacy). Notes Blossom removes the encrypted-cred-sync surface
(no secret — server URLs + signer) and the lift is medium (config UI is the main
cost). Expands TODO #30 with the plan pointer + decisions.
Local commit (kept with the unpushed Bitcoin Connect batch); not pushed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Requested: integrate getAlby/bitcoin-connect as a smooth NWC "connect wallet"
front-end, keeping manual URI entry as backup. Plan (docs/bitcoin-connect-plan.md)
grounds it in bitvid's existing NWC stack: BC's NWC connector exposes the raw
pairing URI (provider.client.nostrWalletConnectUrl) which feeds the existing
validateWalletUri → per-npub store → zap-split/pay_invoice pipeline unchanged.
BC is a connect adapter, not a payment layer. Covers delivery (vendor+lazy import
vs esm.sh; CSP already allows esm.sh), persistence ownership, NWC-only connector
scope, least-privilege requestMethods, UI placement, FEATURE_BITCOIN_CONNECT flag,
and the security constraints (bearer spending secret, Alpha lib pin/vendor).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the Audio/Music/Podcast feature flag to the instance config so a deployer
enables/disables it in one place like the other flags. Follows the exact
FEATURE_NIP71_INGEST wiring: export in config/instance-config.js → re-export in
js/config.js → DEFAULT_FLAGS + runtimeFlags snapshot + `export let` in
js/constants.js. Defaults FALSE ("off = no trace") — only an explicit `true`
lights it up; with it off nothing audio renders and the shipped audio guard keeps
audio-only notes out of the video feeds. No consumers yet (build only).
Also locks docs/audio-integration-plan.md decisions D1–D10 to the agreed
recommendations (status → DECISIONS LOCKED).
Verified: constants + config re-export FEATURE_AUDIO_INGEST === false by default.
Lint + build clean. docs/audio-integration-plan.md, TODO #60.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Pulls the audio thread together into docs/audio-integration-plan.md, matching the
house format of the shorts/live plans: kind/NIP landscape (NIP-A0 voice 1222/1244,
NIP-73 podcast GUIDs, NIP-53 live audio, NIP-71 audio variants, native 30078
m audio/*, non-standard music), 10 open decisions (D1–D10) with recommendations,
config flags (FEATURE_AUDIO_INGEST, off = no trace), the shared media-category
resolver (video|audio|live|short) used by sidebar + channel + My Content tabs,
audio player model (modal → persistent mini-player), UX best practices, flag-gated
phases, moderation, V4V/zaps, risks. Links it from TODO #60.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First slice of structured submissions (replacing the kind-4 encrypted DM forms).
Pure, hermetic core — no UI or network yet.
- nostrEventSchemas.js: SUBMISSION_KIND=30083 (+ NOTE_TYPES.SUBMISSION, BASE_SCHEMAS
entry) and buildSubmissionEvent(). PUBLIC + addressable; data in tags: d (id),
k (type), applicant (npub), p (admin), t=bitvid-submission; free-text in content.
- js/submissions/submissionService.js: SUBMISSION_TYPES + labels, parseSubmissionEvent,
buildSubmission, generateSubmissionId. Pure — unit-testable with no relays/signer.
- constants.js: FEATURE_SUBMISSIONS (default OFF until the admin UI + forms ship).
- tests/submission-service.test.mjs: 8 scenarios (build tags/type/default, d-tag
required, parse structure/reject/unknown-type→other, build→parse round-trip of
every type, id generation).
- check-file-size.mjs: bump nostrEventSchemas grandfather 3250→3541 (adding a new
event type to the schema registry is the prescribed pattern).
- docs/nostr-event-schemas.md: catalogue row.
Schema validation ✨, lint, build all pass; tests 8/8.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First slice of creator playlists. Adds a bitvid-native addressable playlist
event and the pure, hermetic core that builds/parses/mutates it. No UI or network
yet — those land in follow-up commits.
- nostrEventSchemas.js: PLAYLIST_KIND=30082 (+ NOTE_TYPES.PLAYLIST, BASE_SCHEMAS
entry) and buildPlaylistEvent(). Data lives in tags: d (id), title/description/
image, and ordered `a` refs (30078:pubkey:d) to videos; order = tag order;
repeated refs deduped. Deliberately NOT NIP-51's 30005 "video curation set" —
bitvid already reads 30005 as legacy interest-sets, so reuse would collide.
- js/playlists/playlistService.js: parsePlaylistEvent, buildPlaylist, and
immutable add/remove/reorder item helpers + videoCoordinate() (reuses the
canonical buildVideoAddressPointer, same 30078 contract as the zap fix) +
generatePlaylistId(). Pure — fully unit-testable with no relays/signer.
- tests/playlist-service.test.mjs: 12 scenarios (build tags + order + dedup,
d-tag required, parse structure/rejects/defaults, build→parse round-trip,
add/remove/reorder immutability + clamping, coordinate derivation incl. the
kind-0 internal-object case).
- docs/nostr-event-schemas.md: catalogue row for the new kind.
Schema validation, lint, and build all pass; tests 12/12.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The store now supports p: (profile/channel) pointers — every zap tagged to a
creator pubkey (direct profile zaps AND zaps to their videos, both carry a `p`
tag), from 9735 receipts + verified tallies:
- pointerKey accepts "p"; new profilePointer(pubkey) helper.
- runBatch batches p: pointers into a #p filter; both folds count #p-tagged
events (deduped by payment_hash like a:/e:).
- Channel page: a "⚡ N sats zapped" total under the lightning address, wired
to the store via js/channelZapTotal.js (own module for channelProfile.js's
size budget), hidden until nonzero.
Completes the D2 video + profile scope; the bitvid-native zap tally feature is
now feature-complete (publish + count + chart + video + profile). Tests:
profilePointer/pointerKey + #p counting in tests/zap-totals.test.mjs. Lint
(size cap ok) + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The per-video Popularity modal now plots a second, orange zaps-over-time line
beside the red views line, with a legend and — new — VISIBLE x-axis date ticks
(the old chart only put the range in the aria-label).
- buildZapSatsTimeSeries: buckets cumulative SATS from 9735 receipts + verified
bitvid tallies, deduped by payment_hash (same rule as the badge store) so the
chart and badge agree.
- buildPopularityChartSvg: two token-colored cumulative lines on a shared time
x-axis, each with its OWN y scale (independent per-series — D5), plus date-tick
<text> labels. Views = .text-accent, zaps = .text-zap (new --color-zap
utility). Legend is plain DOM in the modal; headline gains "· N sats zapped".
- js/zapEventsFacade.js: lists a pointer's [9735, ZAP_TALLY_KIND] events via the
L1 SubscriptionManager for the chart; modal re-renders on onZapTotalsChanged.
- All flag-gated (FEATURE_ZAP_TALLY); the existing views-only chart + its test
are untouched (buildViewCountChartSvg kept).
Verified live earlier that imports resolve (app builds/runs). Tests:
tests/zap-time-series-chart.test.mjs (5) + existing view-count-chart green.
Lint + build green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds an integration test exercising the REAL verifier against a genuinely
valid, amount-bearing bolt11 (real bech32 decode + nip57 amount parse + real
preimage/description-hash math, no injected fields) — proves the tally pieces
compose. Marks the shipped slices in docs/zap-tally-plan.md §11 and lists the
remaining v1 work: profile-zap #p counting + channel-page UI, the retry-path
publish hook, and the popularity chart (§5.9).
Co-Authored-By: Claude Fable 5 <[email protected]>
First slice of docs/zap-tally-plan.md (decisions D1-D5 resolved in the doc):
- ZAP_TALLY_KIND = 30081 (addressable, d=payment_hash → one canonical event
per real payment) + NOTE_TYPES.ZAP_TALLY schema + buildZapTallyEvent
(nostrEventSchemas.js). Video + profile pointers.
- FEATURE_ZAP_TALLY flag (constants.js), default ON per D4.
- zapReceiptValidator.js: extractBolt11Fields (payment_hash + description_hash
from the existing bech32 walker), verifyPaymentPreimage
(sha256(preimage)==payment_hash), verifyBitvidZapTally — the security core:
proves the invoice was paid AND bound to this zap request (description-hash
binding blocks reusing a leaked preimage for another video). Amount is
bolt11-derived, never the tag; pointers come from the embedded zap request.
- Schema-doc row for kind 30081.
Tests: tests/zap-tally-verify.test.mjs (8) — preimage check, bech32 field
round-trip, valid/bad-preimage/replay/zero-sats decisions, builder tag shape
(video + profile). Lint + build green.
No publishing or counting wired yet — next slices per the plan checklist.
Co-Authored-By: Claude Fable 5 <[email protected]>
Extends docs/zap-tally-plan.md §5.9: a second orange zaps-over-time line on the
per-video Popularity chart beside the existing red views line, a legend
(red=Views / orange=Zaps sats), and VISIBLE date labels (the current chart only
puts the range in the SVG aria-label). Reuses the --color-zap token from the
badges, buckets cumulative sats from 9735 + verified tally events (deduped by
payment_hash, same rule as the store), and can partially ship on 9735-only data
before the tally event lands. Adds checklist items, a chart Y-axis open
decision (D5), test targets, and updates goals/scope + TODO #47.
Co-Authored-By: Claude Fable 5 <[email protected]>
Full spec for making Most Zapped + zap badges global/durable/cross-user
without depending on external wallets to publish NIP-57 receipts (which
custodial providers like Strike don't). Key insight: the payer holds the
preimage after NWC pay_invoice, and the invoice's description hash commits
to the zap request's pointer+amount — so a payer-signed event carrying
{zapRequest, bolt11, preimage} is verifiable with the same cryptographic
strength as a recipient-signed 9735, reusing bitvid's existing bolt11 decode
in zapReceiptValidator.js.
docs/zap-tally-plan.md covers: the event schema, the verify algorithm,
cross-source dedup by payment_hash (9735-preferred), file-by-file code
changes, security/edge cases, a feature-flagged rollout, a test plan, open
decisions, and an ordered task checklist so the build stays neat across
sessions. TODO #47 points at it. No code changes yet.
Co-Authored-By: Claude Fable 5 <[email protected]>
New-user onboarding per docs/onboarding-plan.md: on the first login of a pubkey
on a device, a skippable guided tour escorts the user around bitvid — a scrim
dims the page with a spotlight CUTOUT over each step's anchor, ringed by a
pulsing glow driven by the theme's accent tokens, plus an anchored popover with
Back/Next/Skip, progress dots, and arrow/Esc keyboard nav.
- js/ui/onboarding/tourEngine.js: dependency-free engine. Spotlight = one
element whose 200vmax box-shadow (--surface-overlay-darker) dims everything
except the cutout; an ::after ring animates between --color-accent and
--color-accent-strong. Positioning via CSS custom properties set from JS (the
sanctioned dynamic-var pattern; visual styling lives in an injected token-only
stylesheet). Steps with missing anchors auto-skip, so one script serves all
layouts. Repositions on resize/scroll.
- js/ui/onboarding/bitvidTour.js: the step script (welcome → feeds →
subscriptions → upload → profile → setup CTAs that deep-link into the storage
and wallet panes via profileController.show).
- js/services/onboardingService.js: once-per-pubkey-per-device flags
(bitvid:onboarding:v1, settingsRestorePrompt-style).
- app.maybeOfferOnboardingTour fires 3.5s after login (feed already rendered
thanks to the warm cold-boot work; never blocks login);
app.startOnboardingTour({force}) + a "Take the tour" button in the profile
modal footer make it re-launchable.
- Lint allowlists: tourEngine.js added to the inline-style allowlist (CSS-var
positioning, VideoCard precedent) and the design-token ALLOWED_FILES
(injected stylesheet geometry, RevertModal precedent) — all colors are tokens.
Tests: tests/onboarding-tour.test.mjs (5 — flag lifecycle, tour render/advance/
finish, skip teardown, missing-anchor exclusion, once-vs-force offering, final
CTA deep-link + completion). Lint + build green.
Phases 2-4 (getting-started checklist, empty-state upgrades, fresh-npub create-
account + key backup + kind-0 bootstrap) tracked in the plan + TODO #59.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Maintainer principle: put as much effort into the static bitvid client as
possible before branching into any separate application.
- bitvid-bridge-plan: prominent "PRIORITY PRINCIPLE — static bitvid first" banner
up top; the Bridge (a second app) is deliberately DEFERRED until the static
client's live features (Go-Live Tier-1/2 MVP) ship and are maximally developed;
build it only when live proves demand for its sovereignty/power features.
- live-publish-plan: new "Build order & the static-first principle" section with
the recommended sequence — 1) Shorts (#16b), 2) Live ingest (#16 watch
foundation), 3) Go-Live MVP (#16c P0-1, Tiers 1&2, client-side only, no
Bridge/VPS), then 4) the Bridge later only if warranted.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Answering "can we avoid the VPS / use a free service like StreamYard?": yes.
live-publish-plan:
- DECISION 1 broadened into a THREE-TIER provider spectrum (bitvid hosts none):
(1) platform embed (StreamYard/OBS -> YouTube/Twitch, free, iframe),
(2) serverless RTMP->HLS API (Livepeer/Mux/Cloudflare, no server, NATIVE HLS) --
the recommended no-VPS default; Livepeer most aligned,
(3) self-hosted node (Bridge headless on VPS) -- sovereignty + WebTorrent VOD.
- New "Provider options" table/section; StreamYard clarified as a studio (not a
CDN). Same NIP-53 event + player across all tiers; only a per-tier playback
adapter differs. Phase 1 reframed as provider-agnostic (Tiers 1 & 2, no infra).
bitvid-bridge-plan:
- Repositioned as the Tier-3 advanced/sovereignty option (serverless is the no-VPS
default), not the default on-ramp.
- Added the HYBRID that dissolves the NAT/VPS problem: a local Bridge records
locally + forwards (outbound) to a Tier-2 serverless provider that serves public
HLS -> home box needs no public reachability, yet keeps local recording +
full-WebTorrent VOD. Made serverless a first-class restream preset (Phase 3);
updated the reachability risk + NAT notes to point at the hybrid.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Maintainer blanket rule: bitvid.network runs NO servers / media infra, ever. All
media receiving/routing is self-hosted by the creator (the Bridge run headless) or
handled by an external provider.
live-publish-plan:
- Add the blanket rule as a non-negotiable principle in the Core Decision.
- Lock all 6 decisions: D1 self-host OR external only (managed bitvid node
REMOVED); D2 signing A-then-NIP46, managed account (D) removed; D3 pass-through;
D4 HLS-first; D5 Bridge IS the self-host path; D6 keep specific flags.
- Sweep every "managed / public BitVid node" reference: the Media Node is always
self-hosted (= Bridge headless per DECISION 7) or external; Phase 3 reframed as
a reference node the CREATOR runs; risks reframed (burden is the self-hoster's,
not bitvid's). Status: decisions locked.
bitvid-bridge-plan:
- Remove "public Media Node" implications; the Bridge is the self-hosting vehicle.
- Add the reachability/NAT reality: a home-only Bridge can't serve public HLS →
headless-on-VPS is the path (why DECISION 7 matters).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
live-publish-plan:
- Expand the config-flags section into a full enumerated "off = no trace" list:
every Go-Live surface (Studio/entry points, My Videos Live sub-tab, live-event
builders, Bridge/session UI, restream UI, archive->VOD flow, routes) is absent
when FEATURE_LIVE_PUBLISH is off. Bridge/restream have their own sub-flags.
- Clarify watch/publish independence (an instance can allow watching without
broadcasting).
- New gap closed: WHO may go live — publishing should respect the whitelist/WoT
(operators can restrict broadcasting to whitelisted creators; don't be an open
ingest relay).
bitvid-bridge-plan:
- Invariant 6: the Bridge respects the connected instance's config (embedded
webview inherits FEATURE_LIVE_PUBLISH; go-live degrades gracefully when off).
- Invariant 7: bind local ingest to loopback (127.0.0.1) only; auth required for
any wider bind. Matching risks added.
- Architecture: Studio webview loads the instance's deployed web app (remote,
cached) so it auto-inherits instance flags and never drifts; instance URL
configurable.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Expands live-publish-plan Phase 7 into docs/bitvid-bridge-plan.md: a super-simple
Windows/macOS/Linux app that makes going live turnkey.
Core design: native media core (MediaMTX + FFmpeg + rclone; never sees the Nostr
key) + an embedded bitvid "Studio" webview that signs — talking over in-app IPC,
NOT browser->localhost (dodges mixed-content/PNA and keeps signing in the web
layer, honoring "BitVid signs; Bridge routes").
Surfaced 10 decisions: framework (Wails/Go vs Tauri), control model (embedded
webview vs website-drives-localhost), capture vs receive, webview signer support
(NIP-46 + encrypted nsec), ingest protocols (RTMP MVP), upload (bundled rclone),
one-codebase-two-modes (Bridge desktop == self-hosted Media Node headless),
pass-through-only transcode, code-signing/notarization ownership (the real cost of
"easy install"), license/telemetry.
Phases 0-7 (shell+core -> receive MVP -> record/VOD -> restream -> turnkey/packaging
-> built-in capture -> SRT/WHIP/WebRTC -> headless node + BitVid Box), invariants,
packaging/signing, risks, sources. Linked from the publish plan.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Answering "can live use WebTorrent?": classic WebTorrent can't carry live (its
infohash needs the complete file), but P2P *live* is real via HLS + peer segment
sharing.
- live-ingest-plan: new "P2P-assisted playback" section — P2P Media Loader
(hls.js-compatible, historically WebTorrent-tracker-based) shares HLS segments
over WebRTC to offload origin bandwidth. Gated on real concurrency (DECISION 1's
whitelist scope makes live sparse), so v1 = plain hls.js, P2P later. VOD keeps
full WebTorrent.
- live-publish-plan: new "Why BitVid Live — advantages" section (P2P offload,
full-WebTorrent VODs, Nostr-native/no-lock-in, built-in zaps, BYO storage/relays/
self-host, one-canonical-stream+mirrors, archive = first-class video, self-
sovereign identity). Playback section cross-references the P2P offload + VOD
WebTorrent.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Third live/short plan, sequenced after #16 (ingest) + #16b (shorts). Turns the
maintainer's BitVid Live design into a repo-accurate, convention-aligned plan:
- Core decision: Nostr-native control layer, NOT a hosting backend. Static client
stays static (signs, no keys, no server); the one thing a browser can't do
(receive/route live media) is isolated in an OPTIONAL server-side Media
Node/Bridge (MediaMTX). External-provider-only mode (Phase 1) needs zero infra.
- Cross-linked to docs/live-ingest-plan.md (#16): the watch side (Live tab,
player, chat, zaps, hls.js) is a dependency and reused; this doc adds the
builders + Studio + Media Node/Bridge. Shared Phase 0 schema in nostrEventSchemas.
- Verified against real code: NIP-98 HTTP-Auth (kind 27235) builder exists for
bridge auth; s-tag + js/utils/storagePointer.js -> .info.json is exactly how
archive->VOD maps (ended stream becomes an ordinary kind-30078 video + 34235
mirror); canonical video kind 30078.
- Decisions surfaced: hosting model, signing-during-stream (tab-open vs NIP-46),
pass-through vs transcode, WebRTC scope, local Bridge scope, flag reconciliation
(FEATURE_LIVE_INGEST/PUBLISH/BRIDGE/RESTREAM).
- 9 phases (0 flags+schema → 1 external-provider no-infra → 2 mock bridge → 3
Media Node MVP → 4 recording/VOD → 5 restream → 6 OBS companion → 7 local
Bridge → 8 WebRTC), 5 engineering-rule invariants, risks, sources.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- D1 whitelisted hosts only (empty-when-nobody-live accepted).
- D2 read + post + zaps as the end state, phased: Phase 3 read chat + host zaps,
Phase 3b post chat (own sub-flag + moderation stance).
- D3 hls.js, lazy-loaded + vendored behind the flag (native HLS is Safari-only).
- D4 one Live tab: live first, then recent ended (VOD) — also mitigates the
sparse-live empty-tab concern.
- D5 user read relays + a configurable set of live-discovery relays (widens
where, not who).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- D8: native short = kind 30078 + isShort marker (mirrors to 34236); marker is
fine since a single isShort boolean reliably distinguishes shorts everywhere.
- D5: Option C phased — Phase 1 grid + portrait card + main-feed exclusion,
Phase 2 immersive feed later.
Shorts plan decisions now locked except D6 (card: VideoCard variant vs dedicated
ShortCard) which is a build-time implementation choice.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shorts plan decisions from maintainer walkthrough:
- D1 whitelist scope, D2 inherit moderation (+ NSFW no-autoplay safeguard),
D3 Shorts tab ONLY (exclude from main feeds), D4 kind is the definer.
- D7 (agreed): upload-modal content-type selector + explicit isShort marker on
the kind-30078 schema (native shorts need it; kind-22 only covers ingested).
- D8 (leaning marker): native short = 30078 + isShort marker mirrored to 34236,
OR a distinct kind that also mirrors to 34236 — both yield the same external
result (WebTorrent-rich native + standard 34236 mirror so other NIP-71 apps
see bitvid shorts). Marker recommended; maintainer to confirm.
- Publish/management UI is flag-gated (off = no trace): no upload selector, no
My Videos Shorts sub-tab when FEATURE_SHORTS off.
- My Videos gets Videos/Shorts/Live sub-tabs (reusing the admin sub-tab pattern);
Live sub-tab tied to publish (#16c), not ingest. Noted in both plans.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold July-2026 live-streaming watch UX into docs/live-ingest-plan.md:
- Desktop cinematic layout: 16:9 player + collapsible chat side rail, host/
LIVE badge/viewer count below, theater + fullscreen, keyboard controls.
- Mobile: player-top / chat-below, resizable chat overlay in fullscreen.
- Live-specific: LIVE badge + viewer count, auto-scroll chat w/ pause-on-scroll,
live vs ended seeking (VOD scrubber in Phase 4).
- Latency reality baked in: HLS ~6-30s behind → chat runs ahead of video; don't
promise low-latency.
- bitvid angle: live zaps to the host (zap.stream-style) fit the existing zap
system and can ship even before/with read-only chat.
- Phase 2 now includes the cinematic layout; sources cited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fold July-2026 short-form UX research into docs/shorts-plan.md:
- Immersive one-at-a-time 9:16 feed (grid = entry point), muted autoplay + loop,
preload next 2-3 (no spinner between shorts), overlaid action rail
(Zap/Comment/Share/follow — zaps as the bitvid differentiator), captions when
present, prefers-reduced-motion.
- Mobile: full-screen + swipe up/down.
- Desktop: centered letterboxed player (never stretch 9:16) + NATIVE keyboard nav
(up/down prev/next, Space, M, Esc) + side action rail — YouTube Shorts' desktop
keyboard-nav gap is an opportunity to do it right.
- Phase 2 reframed as the responsive immersive feed; sources cited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shorts (docs/shorts-plan.md):
- Add DECISION 6 + architecture/phase items for a NEW portrait 9:16 card design.
The existing VideoCard.js is hardcoded 16:9 (ratio-16-9), so shorts need either
a "short" variant of VideoCard or a dedicated ShortCard — a first-class Phase 1
deliverable, not an afterthought.
Live ingest (docs/live-ingest-plan.md):
- Reframe past streams (VOD) and chat from "deferred/maybe" to committed phases:
Phase 3 read-only chat (+ optional 3b post), Phase 4 ended/recording VOD
playback through the same HLS path. So the plan's phases fully account for
live AND past zap.stream streams AND chat.
- Add a distinct LiveCard design (LIVE badge, viewer count, ended state).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two flag-gated, watch-only content types, each spec'd following the repo's
docs/*-plan.md convention with open decisions embedded as DECISION callouts.
- docs/shorts-plan.md (#16b): NIP-71 kind 22. Ingest already exists
(nip71IngestAdapter handles kind 22), so this is an isShort marker + a Shorts
sidebar tab + vertical UI + a shorts feed source. Behind FEATURE_SHORTS.
- docs/live-ingest-plan.md (#16): NIP-53 kind 30311 (+1311 chat), HLS playback.
Leads with a Phase 0 research spike (confirm tag shapes from zap.stream/
shosho.live, de-risk hls.js + CORS). Behind FEATURE_LIVE_INGEST; publishing
(#16c) is explicitly out of scope with its own future flag.
Both docs specify the "off = no trace" wiring (no tab, no feed registration, no
subscriptions, no route, no lazy assets) per the locked shared requirement.
TODO #16/#16b now link their plans.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Makes the card liveness-visibility behavior switchable so it can be A/B
feel-tested, and probes ahead of the scroll for a snappier result.
- CARD_LIVENESS_POLICY (config/instance-config.js): "show-pending" (default,
unchanged) | "hide-foreign" (foreign/ingested cards hidden until a source
verifies; native cards stay show-pending) | "hide-all". Owner's own cards are
always visible. Flip live for testing without a rebuild:
window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" then refresh/scroll.
Implemented via a data-foreign provenance flag on the card +
getCardLivenessPolicy() in cardSourceVisibility.js.
- LIVENESS_PROBE_PREFETCH_MARGIN (default 600px): both liveness probes were
already viewport-gated (IntersectionObserver, on-screen-first, WebTorrent
priority-queued + concurrency-capped); this expands their rootMargin so cards
just below the fold verify before the user reaches them.
- Tests: three policy scenarios in tests/video-card-source-visibility.test.mjs;
also un-broke 3 pre-existing VideoCard tests by adding the missing
IntersectionObserver JSDOM stub.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Finishes the remaining #3 zap audit items.
- zapSplit: when the platform fee would pay the SAME Lightning address as the
creator (e.g. the creator is the platform operator), collapse to a single
full-amount payment instead of sending two invoices to one wallet — avoids an
extra routing fee and a confusing duplicate receipt for the same total.
- Audited split correctness: floor(amount*fee/100) with the creator taking the
remainder loses no sats; a sub-1-sat fee rounds to 0 and the whole zap goes to
the creator (no zero-amount platform send); fee is clamped 0–100 and a junk
override falls back to the default. The earlier "fee landed in my own wallet"
report is expected behavior (sender is the platform operator, or a self-zap),
not a bug.
- Cheat-resistant tests: testCreatorEqualsPlatformCollapsesToOnePayment and
testSplitRoundingPreservesEverySat in tests/zap-split.test.mjs.
- Receipt validation (9735) and NWC budget/retry UX confirmed already solid;
audit plan + TODO #3 closed.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Record this session's fixes (delete/zombie, NIP-71 mirror teardown,
watch-history bucketing, the zap chain, the test-runner quarantine work)
and the test spec-corrections.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Defer the Capacitor native-app plan and the NIP-46 roadmap enhancements
(current NIP-46 login works well) while the pre-launch backlog clears.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- docs/zap-audit-plan.md: architecture map, findings (LNURL CORS send-error,
comment box confirmed working, popover mis-position), and prioritized next
steps (CORS/LNURL proxy decision first, then popover, fee-split correctness,
receipt validation, NWC UX). Notes the injectable-fetcher/deps test seams.
- TODO #3 points to the doc; #9 adds two unverified learnings: confirm the
community-blacklist batching actually dropped the kind-30000 storm (still saw
=32 in a later load), and trace the `from_hex` malformed-hex source via the
new sanitizer dev-log.
Planning/docs only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Audited the card liveness/health system. Policy (cardSourceVisibility) is
already correct — hides only when neither CDN nor WebTorrent is healthy,
un-hides when WebTorrent flips green. Documents the real gaps (slow probes;
opaque HEAD no-cors inaccuracy for cross-origin NIP-71 CDNs; single-source
adapter dropping extra imeta) and a 3-phase plan: multi-source capture,
video-element playability probe across sources, and speed tuning. Adds
docs/video-liveness-plan.md + expands pre-launch TODO #5. Planning only.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Imports not produced by bitvid's storage system (reference imports / external
URLs) must be explicitly flagged externally-managed so storage tooling never
treats them as bitvid-owned: external badge (never "missing from bucket"),
excluded from orphan reconciliation, delete "file left behind" warning
suppressed, liveness unverifiable. Adds import modes (full re-host vs
reference) + Phase 2b to the plan and TODO 17c. Notes the existing
isUrlUnderBase heuristic is the baseline but a durable flag is needed.
Planning only; no code changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds docs/nip71-onboarding-plan.md and pre-launch TODO 17c for the agreed
two-way NIP-71 interop:
- Phase 1 (first): cross-ecosystem dedup — prefer the bitvid (30078) version,
show exactly one in every grid; identity = author + import-link/hash/infohash.
- Phase 2: creator-initiated self-import (model A) — re-host to the creator's
storage (CDN) + WebTorrent, publish a signed 30078 with an imported-from
provenance tag. No forging others' notes; no instance auto-rehost.
Planning only; no code changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Decided to keep the dual-event model (kind 30078 canonical + additive 34235/36
mirror) rather than collapse to a single canonical 34235 note. Captures the
rationale: private videos can't be public NIP-71 (no encryption), and kind 30078
is the addressing root (watch-history/comments/reactions/views/playlists/naddr/
moderation + hardcoded video filters) so switching it is a large forward-only
migration not worth the interop win right now. Accepted trade-off: keep the mirror
in sync on edit/delete (Phase 1 polish). Phase 0/1 builder/parser remain reusable
if single-canonical is ever revisited.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Audit (verified in code) of how moderation, web-of-trust, mute/block lists, and the
site ALLOW_NSFW_CONTENT config interact with NIP-71 interop:
- Controls key on author pubkey + event id, so they apply to NIP-71 videos
automatically IF ingested videos share the feed pipeline (blacklist-filter,
moderation/WoT, suppression, kids, nsfw).
- Phase 1 (outbound): gate the mirror on ALLOW_NSFW_CONTENT (don't mirror NSFW from
an instance that forbids it); builder stays pure + always sets content-warning.
- Phase 2 (inbound) gap: mergeNip71MetadataIntoVideo does NOT map content-warning →
isNsfw, so foreign NSFW would bypass the site filter — must add that mapping; plus
route ingest through the full pipeline and trust-gate unknown authors.
- Kids feed already safe (allowlist on isForKids).
No code changes yet (both items land in their phases); captured as checklist items.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Improvements from a second pass over the plan + code:
- Add "Discovery & relays" section: the mirror must publish to the user's NIP-65
OUTBOX write relays (publish kind 10002 first if missing) or outbox clients
(Amethyst) can't find it — discovery is a relay problem, not just format. bitvid
already has publishRelayList/outbox; reuse it.
- Architecture: bitvid never double-renders its OWN mirror (merged onto 30078 by
videoRootId via nip71Cache) — dedup applies from Phase 1, not just Phase 2.
- 3rd hard rule: url must be publicly reachable (gate on verifyPublicAccess).
- Capture: wire existing upload `duration` into imeta; add a loadedmetadata probe
for `dim` + 34235-vs-34236 selection (pulled into v1; trimmed from backlog).
- Mapping: split `.content` (full) vs `alt` (short a11y text); note published_at is
read from the original 30078 so it's stable across edits.
- Surface the mirror toggle in the My Videos tab (existing management surface).
- Delete nuance: NIP-09 (a/k tags) AND empty-replace the addressable event so
NIP-09-ignoring clients still see it cleared.
- Phase 0: add an interop golden-shape test to guard against silent drift.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Document that the explore/kids feed ALREADY consumes NIP-71 hashtags (both raw
["t",…] tags and video.nip71.hashtags) — feed algo needs zero changes; only
capture is missing (no per-video hashtags field on 30078 today).
- Add a "Converting existing videos" section: opting in is an edit that maps every
available 30078 field into its NIP-71 home (incl. hashtags → t) and writes the
34235/36 mirror with d=videoRootId; non-destructive, lockstep edits.
- Pull hashtag capture + field conversion explicitly into Phase 1.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds docs/nip71-migration-plan.md — the agreed spec to build from:
- Dual-event, opt-in (off by default): keep canonical kind 30078; add addressable
34235/34236 mirror (d=videoRootId, edits in lockstep).
- Research correction: 34235/36 are NOT deprecated — current NIP-71 designates the
addressable pair for content that needs post-publication updates (exactly bitvid's
edit-in-place model).
- WebTorrent rides STANDARD NIP-94 imeta fields (magnet, i) rather than a hack;
private videos never mirrored; HTTPS url required to mirror.
- Phases: 0 retarget builders → 1 outbound publish/edit/delete → 1.5 NIP-89 "Open in
bitvid" handler + NIP-51 kind 30005 portable playlists → 2 inbound ingest.
Also refreshes docs/nips/71.md (was stale: only 21/22; now the current 233-line
spec with 34235/36 addressable kinds, waveform, origin, multi-audio). Updates
TODO #17 to point at the plan.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- README "Login with Nostr": recommend a well-maintained NIP-07 signer
(nos2x/Alby) and add a troubleshooting note for the locked/asleep-signer
case (unlock/reload + refresh), since a flaky extension was the root cause
of the encrypted-list load failures.
- KNOWN_BUGS #0: mark RESOLVED (2026-06-16) — confirmed in real env after the
user switched to nos2x; DMs/watch-history/lists decrypt in ms with no
timeouts. Documented the DM decrypt-resilience + receiver fixes and the
post-login profile-panel freeze fix (defer panel rendering to selectPane).
Recorded the remaining low-priority follow-ups (liveness-ranked relays,
opt-in queue concurrency, optional DM double-decrypt routing).
- Retitle the section "## Open" → "## Tracked" so resolved entries (#0/#1/#2)
aren't mislabeled as open; status is now per-entry.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
KNOWN_BUGS #1: logged out, the video grid stayed empty after dismissing the
disclaimer until a manual page refresh.
Reproduced deterministically with scripts/perf/disclaimer-grid.mjs (fresh
context, real relays) once it actually waits for and dismisses the
disclaimer: post-dismiss card timeline was 0,0,0,… and only a reload
yielded 132 cards.
Root cause: bootstrapInterface (js/index.js) opens the disclaimer — which
makes the background inert via staticModalAccessibility — and THEN awaits
handleHashChange(), which loads the feed view and renders into that
inert/hidden background, so the initial grid mount/render is dropped.
Dismissing the disclaimer never re-triggered a render.
Fix: disclaimer.js hide() now dispatches a `bitvid:disclaimer-dismissed`
event; index.js listens once and calls application.loadVideos(true) so the
feed re-renders into the now-interactive background. Only fires when a shown
disclaimer is dismissed, so returning visitors (who never see it) get no
redundant reload.
Harness after fix: post-dismiss timeline 132,132,… immediately. Full build
+ unit suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>