Sparrow about · v0.38
0 ⌘K
✦ about · sparrow

A reader for the
living broadcast.

Sparrow is a hosted, keyboard-first client for PointCast — El Segundo's ongoing broadcast of blocks across 10 channels. It pairs with Magpie, the local-first publisher Mike uses to push clips into the PointCast graph and its syndication pool (Mastodon · Farcaster · bitchat · Zora · Objkt · more).

TM Read the Technical Memorandum v0.4 · SPA-TM-26-0421 · a Bell Labs / Xerox PARC-style overview of Sparrow Get the native menu-bar companion v0.6 · macOS 13+ · a small ✦ that pulses when new blocks land
✦ v0.38 · what's in the build
✦ roadmap · v0.1 → v1.0
  1. v0.1 shipped 2026-04-21

    Reader home

    • Tuning dial + broadcast reel + channel rosette + beacon strip
    • ⌘K command palette (routes · channels · 60 recent blocks)
    • Atom feed at /sparrow/feed.xml
    • Manifest at /sparrow.json with design-system tokens
    • Blue-hour default theme + dawn toggle + persisted preference
  2. v0.2 shipped 2026-04-21

    Pages, reader, list

    • Per-channel pages /sparrow/ch/<slug> for all 9 channels
    • Block reader /sparrow/b/<id> with view-transition morph from the reel
    • Reading list /sparrow/saved — localStorage, no server
    • Numeric channel jumps (1…9) + G+letter jumps retained
    • Mood filter chips derived from actual block moods
    • Now-tuned IntersectionObserver + S save-toggle
    • Prev/next paging on the reader (K newer · J older)
  3. v0.3 shipped 2026-04-21

    Offline & PWA

    • Scoped service worker at /sparrow/sw.js — precaches home + about + saved + deck + all 9 channel pages + manifest + feed
    • Stale-while-revalidate for the shell, cache-first for block readers (48-entry cap) and Astro / Google Fonts assets
    • Offline fallback page with Sparrow chrome for first-visit cold navigation
    • PWA install via /sparrow/manifest.webmanifest — standalone window, FD/Saved/About shortcuts
    • Offline pill in the HUD (pulses when navigator.onLine flips)
    • Install button appears when the browser fires beforeinstallprompt
    • Last-visited indicator on receipts — subtle desaturation + "read" chip
  4. v0.4 shipped 2026-04-21

    Technical memorandum

    • /sparrow/deck — self-contained overview rendered as a 1980s Bell Labs / Xerox PARC technical memorandum
    • Cream paper + halftone texture, oxblood rubber-stamp, CRT phosphor terminal blocks
    • EB Garamond + Courier Prime + Space Mono typography, numbered sections, figure plates
    • ASCII system-architecture diagram; CSS-rendered dial + receipts figures
    • Full keyboard + data-model + implementation-notes + roadmap tables
    • Appendix B: DALL·E / Midjourney prompts for replacing the CSS figure plates with generated hero art
    • @media print rules — lands cleanly on A4/US-Letter
  5. v0.5 shipped 2026-04-21

    Reader finesse

    • Reading-progress bar on /sparrow/b/<id> driven by CSS view-timeline
    • Keyboard cheatsheet overlay on ? — grouped into Discovery / Reading / Display / Reader extras
    • Copy-as-quote — selecting text inside the article body surfaces an ember ✎ chip; click copies a formatted quote with attribution
    • Hover + focus prefetch on every receipt; requestIdleCallback prefetch of prev + next on the reader
    • Drop caps on the first paragraph of every block; text-wrap: pretty + hanging-punctuation on body copy
    • Jump to top / bottom with 0 / $ (smooth scroll, works everywhere)
  6. v0.6 shipped 2026-04-21

    Native companion

    • Sparrow.app shipped at github.com/mhoydich/sparrow-app — Swift 5.9, AppKit + URLSession + UserNotifications, no external deps
    • Menu-bar ✦ glyph; grows an ember "new count" badge when fresh blocks arrive
    • Polls /sparrow/api/latest.json on a configurable interval (30 s – 1 h, default 5 min)
    • Notification Center alerts (one per block up to 3; digest beyond)
    • Preferences panel — feed URL, poll interval, notifications toggle
    • LSUIElement: no Dock icon, no Cmd-Tab presence
    • Landing page at /sparrow/connect with build + run instructions
    • New data source at /sparrow/api/latest.json — snake_case, summary-only, 24-block window, 2-min CDN cache
  7. v0.7 shipped 2026-04-21

    Named reactions

    • Three-chip reaction toolbar on every block reader — 🔥 lit, 🌿 evergreen, 💜 rare
    • Picks hydrate from localStorage:sparrow:reactions on load; active chips pulse their accent ring
    • Toggles are local to the browser for v0.7 — no server, no Nostr wire yet
    • Shape documented in sparrow.json (ui_primitives.reactions) so alternate clients mirror the same keys
  8. v0.8 shipped 2026-04-21

    Reaction fan-out · NIP-07

    • Detect window.nostr — surface a "connect signer" pill in the reactions toolbar when a NIP-07 extension is present
    • On reaction ADD, sign a kind-7 event r-tagged to https://pointcast.xyz/b/<id> via window.nostr.signEvent
    • Fire-and-forget relay fan-out to the configured pool (default: damus, primal, nos.lol)
    • Emitted log (sparrow:nostr-emitted) prevents duplicate re-broadcasts on reload
    • Four signer states: local · available · connected · emitting — reflected in the pill colour + copy
    • Pubkey cached locally (sparrow:nostr-pubkey); Sparrow never holds secret material
  9. v0.9 shipped 2026-04-21

    Reaction aggregation + unreact

    • Per-reader REQ subscription to the relay pool with filter {kinds:[7], #r:[canonical-block-url]}
    • Count badges paint on each chip as events arrive or from the last 200 stored per relay
    • Client-side dedupe by event id; kind resolved from content glyph or sparrow-<kind> tag
    • Reading works without a signer — aggregation is pure subscription
    • Kind-5 delete events fire on unreact when the local emitted log has the original event id
    • Optimistic local state on both react + unreact; failed relay hops are best-effort
    • beforeunload closes every open WebSocket — no leaks across navigation
  10. v0.10 shipped 2026-04-21

    Cross-reel count badges

    • One REQ per relay with every visible block URL in #r (bulk fan-in, not N-per-page)
    • Compact "🔥 3 · 🌿 1" row paints into each receipt's .sp-r-foot as events arrive
    • Block ID recovered from each event's r-tag via /b/<id> regex
    • Shared dedupe state (reactionCounts Map) — reader + reel stay consistent
    • No new storage keys; no new signer requirement; reading still public
  11. v0.11 shipped 2026-04-21

    Inline reply composer

    • Collapsible <details> panel below the reactions toolbar on every /sparrow/b/<id>
    • Subject + body fields; channel inherits from the parent block; type pinned to NOTE
    • POSTs pc-ping-v1 to https://pointcast.xyz/api/ping with sourceUrl + expand=true
    • States: pending → ok (collapses after 2.2 s) / error (inline message)
    • 3800-char counter flips oxblood when the body is over the /api/ping cap
    • No native / Magpie dependency — works everywhere the page loads
  12. v0.12 shipped 2026-04-21

    Magpie bridge awareness

    • Composer probes 127.0.0.1:38473/health on load (1.2 s abort — non-blocking)
    • On connect, fetches /config.json and paints a readiness chip for every destination (PC + 10 others)
    • Three pill states: probing · connected (moss) · offline
    • Deep link to /magpie for multi-destination composition in the hosted UI
    • Submit path unchanged for v0.12 — still pc-ping-v1 direct; Magpie broadcast routing is v0.13
  13. v0.13 shipped 2026-04-21

    Magpie-routed multi-destination reply (web side)

    • Destination chips become checkboxes; PointCast is locked on
    • Any non-PC checkbox + "connected" Magpie pill triggers POST http://127.0.0.1:38473/compose with { body, title, destinations[], channel, hints }
    • Graceful fallback: any /compose error (404 on older Magpie, network, or all-destinations-failed) routes the reply to direct /api/ping instead
    • Result span painted clearly: "magpie → <publisher list>" or "direct (magpie fallback: <reason>)"
    • Endpoint contract spec'd in sparrow.json.magpie_bridge.endpoint_contract for the native Swift side to implement
  14. v0.14 shipped 2026-04-21

    Native /compose handler (Magpie side)

    • Magpie/Services/MagpieServer.swift: ComposeRequest struct + /compose route + handleCompose
    • Magpie/App/AppState.swift: composeHandler wired + handleComposeRequest — ephemeral ClipItem (id:nil, no DB write) + PublishDraft + PublisherRegistry.publish
    • Returns the same BroadcastResult envelope shape as /broadcast — Sparrow's v0.13 parser works unchanged
    • Preserves input destination order so clients can correlate results 1:1
    • Unknown / typo'd publisher ids surface as explicit "unknown publisher id" errors instead of silent drops
    • No changes required to the Sparrow web client — it just starts hitting the real endpoint once Magpie is upgraded
  15. v0.15 shipped 2026-04-21

    Reading-list mirror (via Magpie peer-node)

    • MagpieServer: GET /reader-state.json returns the stored { saved, ... } blob + schema tag "sparrow-reader-state-v1"
    • MagpieServer: POST /reader-state merges incoming with UserDefaults store using per-key updated_at (newest wins)
    • SparrowLayout debounces 600 ms then POSTs sparrow:saved as { saved: { value, updated_at } }
    • On load, if Magpie is probed alive, Sparrow GETs /reader-state.json and pulls newer-than-local saved with skipMirror flag (no loop)
    • sparrow:saved:updated_at is the local tie-breaker so offline toggles don't lose to stale remote state
    • Only fires when the peer-node has been seen alive this session — no dead-socket spam on solo browsers
  16. v0.16 shipped 2026-04-21

    Visited + reactions on the same rails

    • writeVisited and writeReactions accept opts.skipMirror and trigger the same 600ms-debounced scheduleReaderMirror()
    • mirrorPush() sends all three keys in one POST: saved / visited / reactions each with its own { value, updated_at }
    • mirrorPull() iterates MIRROR_KEYS, applies newest-wins, and re-runs applyVisited + hydrateReactions so the DOM catches up with remote state
    • Three new localStorage tie-breaker keys: sparrow:visited:updated_at, sparrow:reactions:updated_at (joining sparrow:saved:updated_at from v0.15)
    • Magpie server code unchanged — its per-key newest-wins merge already handles any shape
  17. v0.17 shipped 2026-04-21

    OPML import / export

    • Toolbar above the receipts list on /sparrow/saved: "export saved ↓" + "import ↑" (file input hidden behind a pill label)
    • Export groups outlines into Sparrow, Saved blocks, Channels — OPML 2.0, UTF-8, stable filename sparrow-saved-<YYYY-MM-DD>.opml
    • Import uses DOMParser to scrape any outline's htmlUrl or xmlUrl for /b/<id> matches and unions with sparrow:saved
    • Additive merge — non-block outlines (channels, unrelated feeds) are ignored silently; nothing is ever removed by import
    • Updates sparrow:saved:updated_at so the mirror picks the import up on the next debounce
    • Entirely client-side; no bytes leave the browser except the user-initiated file selection
  18. v0.18 shipped 2026-04-21

    Bridge origin ladder

    • resolveMagpieOrigin() probes a ranked list: sparrow:magpie-origin override → magpie.local:38473 → 127.0.0.1:38473
    • First /health responder wins and caches on window.__sparrow.magpieOrigin for the life of the page
    • All three Magpie call-sites (reader-state mirror, bridge pill + /config.json, composer /compose) share the resolved origin
    • localStorage override unblocks LAN, VM, reverse-proxy, and non-default-port setups without code changes
    • Ready for Magpie v0.19 Bonjour: once Magpie advertises, .local just starts working — no Sparrow changes needed
  19. v0.19 shipped 2026-04-21

    Magpie Bonjour advertiser (Swift side)

    • Magpie's NWListener attaches .service with type _magpie._tcp, name "Magpie", port 38473
    • TXT record carries version + /health path + schema id + composer + mirror endpoint hints
    • macOS mDNS responder transparently resolves magpie.local to the host running it
    • Sparrow's v0.18 ladder picks it up on the second rung with no web-side changes
    • Listener stays loopback-bound — advertisement is metadata-only, not a public socket
    • dns-sd -B _magpie._tcp surfaces the active instance for debugging
  20. v0.20 shipped 2026-04-21

    Sparrow.app peer-node (Bonjour + HTTP)

    • Sources/SparrowApp/SparrowServer.swift — loopback NWListener on port 38474 with hand-rolled HTTP/1.1 parser
    • Routes: GET /health, GET /reader-state.json, POST /reader-state — same sparrow-reader-state-v1 shape as Magpie
    • Newest-wins merge logic (mergeReaderState) is byte-identical to Magpie's, so state is portable between peers
    • NWListener.service advertises _sparrow._tcp "Sparrow" with TXT record (version, /health, schema, /reader-state, peer: sparrow-app)
    • Web resolver ladder expands to 5 rungs: user override → magpie.local → 127.0.0.1:38473 → sparrow.local:38474 → 127.0.0.1:38474
    • window.__sparrow.magpiePeerKind records "magpie" vs "sparrow-app" so the bridge pill labels the active peer accurately
    • Composer still Magpie-only (needs PublisherRegistry) — falls back to direct /api/ping when Sparrow.app is the resolved peer
    • Settings.peerServerEnabled toggle (default: on); Settings.peerServerPort override
  21. v0.21 shipped 2026-04-21

    Cross-device sync (NIP-44 + kind 30078)

    • Opt-in "sync · on/off" pill in the HUD — three visual states: unavailable (no NIP-44 signer) / off / on
    • Encrypts the sparrow-reader-state-v1 blob via window.nostr.nip44.encrypt(selfPubkey, plaintext) — self-encryption, only the same npub can decrypt
    • Publishes kind-30078 addressable events (d-tag: sparrow-reader-state-v1) so one event replaces the previous — no garbage pile on the relays
    • On page load + signer connect: REQ limit:1 across each configured relay, takes newest created_at, merges newest-wins per top-level key via the same MIRROR_KEYS apply callbacks the LAN mirror uses
    • Stacked debounce: 600ms LAN mirror → 1.2s Nostr queue → 4s relay floor (sparrow:sync-last-emitted-at) so rapid toggles coalesce
    • Runs alongside the LAN peer-node mirror — both fire on the same scheduleReaderMirror() debounce, state stays coherent because both use newest-wins-by-updated_at
    • Relay pool reuses the one already configured for reactions (damus + primal + nos.lol by default; sparrow:nostr-relays override)
  22. v0.22 shipped 2026-04-21

    Federated reading lists (/sparrow/friends)

    • New route /sparrow/friends with three panels: publisher toggle · following list · their reading
    • Publisher emits a separate kind-30078 event with d-tag sparrow-public-saved-v1 (unencrypted) — narrower scope than the private sync, just saved ids + client profile
    • Two distinct consents: cross-device sync (v0.21, NIP-44) and public list (v0.22, unencrypted) — either can be on without the other
    • sparrow:friends localStorage holds {pubkey: hex, alias?}[]. Add/remove with a tiny form; alias is local-only, never broadcast
    • On load: REQs each configured relay for {kinds:[30078], authors:[friends], "#d":["sparrow-public-saved-v1"]} and renders the newest per author
    • Server ships a block lookup so saved ids resolve to title + channel chip; unknown blocks (from older Sparrow builds) degrade to a stub receipt
    • Hex-only for v0.22; npub1… bech32 decode is the tiny polish that lands next
  23. v0.23 shipped 2026-04-21

    Federation polish — npub1 + profiles

    • Self-contained NIP-19 bech32 codec in /sparrow/friends (no dependency) — npubToHex / hexToNpub / parsePubkey
    • Add-form accepts npub1… alongside 64-hex; bad checksum triggers an inline setCustomValidity nudge
    • HUD self-pubkey display renders as short-npub instead of raw hex for cross-client legibility
    • NIP-01 kind-0 profile lookup on /sparrow/friends load — REQ {kinds:[0], authors:<friends>} against the relay pool
    • sparrow:profiles localStorage cache with 24h TTL; stores {name, display_name, picture, nip05, fetched_at}
    • display_name / name used as auto-alias when the local alias is empty; 🛰 glyph marks federation-sourced names
    • Picture rendering + NIP-05 verification round-trip deferred to v0.24
  24. v0.24 shipped 2026-04-21

    Faces, checkmarks, and an F jump

    • Profile pictures on /sparrow/friends — 28px circles with lazy + no-referrer + onerror-hide; 18px inline on feed cards; ✦ placeholder when no kind-0 picture
    • NIP-05 verification via /.well-known/nostr.json?name=<user>; checks names[user] matches the pubkey (case-insensitive)
    • Verification cache: nip05_verified + nip05_verified_at on the profile, 7-day TTL; any failure (non-2xx, parse, CORS, mismatch) lands as "mismatch" so silent failure can't masquerade as verified
    • ✓ (moss pill) on verified nip05; ! (oxblood pill) on mismatch; dot while pending
    • F keyboard shortcut → /sparrow/friends from anywhere in Sparrow; cheatsheet + palette entries added
    • Reel-lane integration explicitly deferred to v0.25 to keep the sprint coherent
  25. v0.25 shipped 2026-04-21

    Friends lane on the dashboard

    • Compact section between rosette and reel on /sparrow — one row per friend with their freshest save
    • Row layout: avatar · name · "saved" · № id · title · channel chip · "+N more"
    • Up to 6 rows, sorted by event created_at desc so freshest-updating friend leads
    • Server inlines a block lookup (id → title + channel + channelName) for every non-draft block so titles resolve without fetches
    • Shares the kind-30078 consumer flow with /sparrow/friends and reuses the sparrow:profiles cache for names + avatars
    • Dismiss × button sets sparrow:friends-lane-hidden="1" — re-enable from /sparrow/friends
    • Hidden entirely when no friends are followed; empty-state links to /sparrow/friends when following someone who hasn't published
    • Mobile: simplifies grid to avatar · name · title to keep the dashboard breathable
  26. v0.26 shipped 2026-04-21

    Friends in motion — live toasts, chime, mute

    • Streaming kind-30078 REQ in SparrowLayout with since=bootTime; only events published while the tab is open fire a toast
    • Bottom-right toast stack, max 3 concurrent, 7s TTL, slide-in/fade-out animation, click to open the block, × to dismiss
    • Opt-in Web Audio chime (two-note perfect fifth, ~450ms) on new toasts — gated on sparrow:friends-chime-enabled, off by default, no asset request
    • Per-friend mute via new optional `muted` field on sparrow:friends entries; muted friends drop out of every consumer path (subscribe, feed render, dashboard lane, motion watcher)
    • Mute button on each /sparrow/friends row (🔈/🔇); row fades to 45% opacity when muted; "muted" pill next to pubkey
    • Global motion opt-out via localStorage["sparrow:friends-motion-disabled"] for quiet sessions without unfollowing
    • Dedicated activity timeline deferred to v0.27 — live toasts + mute covered the "in motion" promise coherent on their own
  27. v0.27 shipped 2026-04-21

    /sparrow/friends/activity — timeline, live-stitched

    • New route: three-panel layout — kicker + stats · back-link nav · per-event card feed
    • Dual subscription: initial pull (limit: 50) closes on EOSE; live pull (since: bootTime) stays open until beforeunload
    • Per-event card: avatar · name · "saved N blocks" · relative time · 3 preview receipts (title + channel chip) · "+N more"
    • Newest events pulse green (moss border + shadow animation + "new" pill) for 12s, then demote to normal styling
    • Full mute respect — authors filtered pre-subscribe AND re-checked on ingest so a fresh mute drops events immediately
    • Server-shipped block lookup resolves every saved id to title + channel without per-block round-trips
    • Reuses sparrow:profiles cache for avatars + display names — federation UI stays one-pass across /sparrow, /sparrow/friends, and the activity timeline
    • CTA added from /sparrow/friends feed head; palette entry + SW precache included
  28. v0.28 shipped 2026-04-21

    /sparrow/signals — three readings of the circle

    • New route — header kicker · back/forward nav to /sparrow/friends and /sparrow/friends/activity · three stacked aggregation panels
    • Panel 1 — most co-saved: blocks hit by 2+ followed signers, count-sorted desc, top 12, shows up to 4 saver names + "+N" overflow
    • Panel 2 — recent adds: friends who published a fresh saved list in the last 7 days, newest first, avatar + "published N · Xh ago" + freshest receipt
    • Panel 3 — channel distribution: proportional bar across all 9 channels summed from distinct saves; rows link to /sparrow/ch/<slug>
    • Uses a single kind-30078 REQ + the existing sparrow:profiles cache + server-shipped block lookup — no new round-trips
    • Respects mute — authors filtered pre-subscribe AND mid-paint, so a fresh mute drops events immediately
    • Defers opt-in email digest + per-channel top-N on /sparrow/ch/<slug> to v0.29
  29. v0.29 shipped 2026-04-21

    Signals, extended — first-picker, export, channel panel

    • First-picker ⭐ chip on every /sparrow/signals Panel 1 row — earliest created_at among current savers wins attribution
    • ⤓ export JSON button in the signals nav dumps the current recap as sparrow-signals-<YYYY-MM-DD>.json (schema sparrow-signals-v1)
    • Export bundle carries friends, relays, newest events per author, top co-saved list with full saver lists + first-picker timestamps, and notes documenting the caveat that kind-30078 is replaceable
    • New client-side friends panel on every /sparrow/ch/<slug> above the main reel — filters friends' saves to this channel's block set, count-sorted, top 6
    • Each channel panel row: ×N badge + block № + title + first-picker ⭐ chip; opt-out via localStorage["sparrow:ch-friends-hidden"]
    • Hides automatically when no friends are followed, all are muted, or no saves intersect this channel
    • Opt-in email digest deferred to v0.30 — the only remaining v0.28-era scope item, needs dedicated worker infra
  30. v0.30 shipped 2026-04-21

    Ambient friends + digest scaffold

    • Ambient presence: opt-in kind-20078 (ephemeral, NIP-16) tagged t:sparrow-presence emitted every 60s while tab is foregrounded
    • Streaming subscriber tracks friends' presence and paints a fixed bottom-left "✦ here now" strip, avatars of friends seen within 90s
    • Relays never persist presence events — liveness drops the moment you close the tab
    • Opt-in toggle added to /sparrow/friends publisher panel (sparrow:ambient-enabled)
    • Digest sidecar scaffold: subscribe form on /sparrow/signals writes sparrow-digest-subscription-v1 intent to localStorage
    • Intent also POSTed to /api/sparrow/digest-subscribe — 501 "worker not live" is the current placeholder, client-side message makes that clear
    • Full schema documented at sparrow.json.nostr.federated_lists.digest_sidecar so a Cloudflare Worker can pick up the contract when infra ships
    • Worker-side promise: stores only email + npub + relay pool + frequency; never secret material; signals bundle recomputed at send time
  31. v0.31 shipped 2026-04-21

    Participation onramps for the federation layer

    • "Join the federation" checklist on /sparrow/friends — 4 live-updating steps (signer · public list · follow · ambient) with ✓/○ ticks that repaint across tabs via storage events
    • Invite card with copy-to-clipboard shareable link: /sparrow/friends?follow=<npub1…> — shown once signer is connected
    • Follow-by-URL deep link: ?follow=<npub|hex> pre-fills add-form, scrolls into view, pulses the submit button for 2 cycles (user still clicks to confirm)
    • Starter seed list — scaffolded suggested pubkeys with one-click follow; hides when every starter is already followed
    • OPML friends round-trip: export bundles sparrow:friends[] as <outline type="nostr" xmlUrl="nostr:npub1…" x-sparrow-pubkey="<hex>"> elements with alias + mute state
    • Import accepts either x-sparrow-pubkey attribute or nostr:npub1… xmlUrl; unions with existing; preserves mute state
    • parsePubkey shared by add-form + deep-link — accepts npub1… AND hex-64, normalizes to lowercase hex
    • Sparrow.app ambient pickup + digest worker stub both deferred to v0.32 (native work + worker infra deserve their own sprints)
  32. v0.32 shipped 2026-04-21

    Digest worker · federation.json · friends in the mirror

    • POST /api/sparrow/digest-subscribe — Cloudflare Pages Function validates sparrow-digest-subscription-v1 and returns 202; stores in SPARROW_DIGEST_KV when bound, still acks without so the v0.30 client stops retrying
    • GET + HEAD + OPTIONS handlers documented; 400 on malformed body (bad JSON, unknown schema, invalid email, bad frequency); 413 past 8 KB
    • GET /sparrow/federation.json — editorial sparrow-federation-v1 payload with starters: [{hex, alias, note?}] — curator-maintained in src/pages/sparrow/federation.json.ts
    • friends.astro fetches /sparrow/federation.json on boot, sanitizes entries (requires valid 64-hex), falls back to a 2-item internal seed on failure
    • Reader-state mirror extends MIRROR_KEYS: friends (Array<{pubkey, alias?, muted?}>) + profiles ({[hex]: {name?, display_name?, picture?, nip05?, ...}}) each with own :updated_at
    • window.__sparrow.scheduleReaderMirror exposed by SparrowLayout so route-local pages can trigger push after mutating mirror-tracked keys
    • writeFriends + writeProfiles in friends.astro bump their ts-keys AND call the exposed scheduler so every change propagates
    • Sparrow.app ambient pickup deferred to v0.33 — native Swift Nostr client + tray UI work deserves its own sprint
  33. v0.33 shipped 2026-04-21

    Native ambient pickup · digest cron scaffold · unsubscribe

    • Sparrow.app: NostrRelayClient.swift — URLSessionWebSocketTask-based Nostr client; REQ/EVENT/EOSE + CLOSE; zero dependencies; ~170 LOC
    • Sparrow.app: FriendsService.swift — reads followed pubkeys from SparrowServer's sparrow.readerState UserDefaults (mirrored by v0.32), subscribes to kind-20078 presence + kind-30078 saved events, tracks 90s freshness window with 20s decay
    • MenuBarController.setFriendsPresence(count, aliases) adds a "✦ N here · alias1, alias2, +K" menu item above the separator — hidden when zero
    • workers/sparrow-digest/ scaffold: wrangler.toml (weekly Mon 08 UTC cron, SPARROW_DIGEST_KV binding, MailChannels transport)
    • workers/sparrow-digest/src/index.ts: listAllSubscriptions + isDue frequency gate (weekly/biweekly/monthly) + renderDigestEmail placeholder + MailChannels POST + /dry-run fetch route for testing
    • DELETE /api/sparrow/digest-subscribe · two modes: x-unsub-intent: local-clear (web-initiated, 200 ok) and x-unsub-token (email-footer, 501 until v0.34 signing key lands)
    • CORS on the Pages Function now allows DELETE + x-unsub-intent + x-unsub-token headers
  34. v0.34 shipped 2026-04-21

    HMAC unsubscribe tokens

    • workers/sparrow-digest/src/signing.ts · Web Crypto HMAC-SHA256 · signUnsubToken / verifyUnsubToken / buildUnsubUrl
    • Token shape: <urlencoded-email>.<unix-seconds-expiry>.<lowercase-hex-hmac> · 30-day TTL
    • Constant-time HMAC compare so a timing oracle can't leak the correct prefix
    • DELETE /api/sparrow/digest-subscribe now verifies: 400 on malformed/bad-hmac/missing-secret, 410 on expired, 200 on verify + KV delete
    • Token accepted via x-unsub-token header OR ?unsub_token query param — email-footer links work from any browser without custom headers
    • Worker renderDigestEmail is now async and wires buildUnsubUrl into the footer — text + HTML both
    • SPARROW_DIGEST_SIGNING_KEY env binding required on BOTH the cron worker AND the Pages Function; rotating invalidates outstanding tokens
    • Verify helper inlined in the Pages Function (Pages and Workers are separate deploy targets) — byte-identical to the canonical copy in signing.ts
    • Cron dispatch + signals aggregation + Nostr client port + native NIP-01 profile lookup still pending — v0.35
  35. v0.35 shipped 2026-04-21

    Nostr TS port · MailChannels retry · friends-saved email summary

    • workers/sparrow-digest/src/nostr.ts · TypeScript port of NostrRelayClient.swift · Web WebSocket + JSON frames · collectFromRelay + collectAcrossRelays + newestPerAuthorByDTag helpers
    • Per-relay timeout 6s, 500-event safety cap, defensive re-filter by author (relays may echo spam), dedup by event.id across relays
    • workers/sparrow-digest/src/send.ts · MailChannels extracted with retry-with-jitter · 3 attempts max · exponential ceilings 800ms / 1600ms / 3200ms with full jitter · 5xx/429/408 retriable · 401/403/422 fail-fast
    • Retriable failures leave last_sent_at untouched so the next cron tick cleanly retries · permanent failures surface in the run log but also don't touch last_sent_at (ops sees them + can redeploy)
    • renderDigestEmail v0.35 · subscribers with npub + relays get a live fetched "N of M followed signers published public saved lists · K total saved-block references" summary in both text and HTML
    • Subscribers without npub still get the short "open signals" prompt — no regression for early signups
    • Cron email signed-unsubscribe URL (v0.34) stitches cleanly with the new content
    • Full signals aggregation mirror (co-saves + channel distribution) + block-lookup KV prefetch + dead-letter bucket queued for v0.36
  36. v0.36 shipped 2026-04-22

    Dead-letter bucket + ops endpoints

    • workers/sparrow-digest/src/deadletter.ts · KV-backed failure counter (fail:<email>) + dead-letter record (dl:<email>)
    • 3 consecutive retriable failures → dead-letter. Any non-retriable failure (401/403/422) → immediate dead-letter
    • scheduled() skips dead-lettered subs every tick; released subs resume cleanly on the next tick (last_sent_at untouched through failure paths)
    • Successful send clears the failure counter — patience resets on recovery
    • /ops/dead-letter GET · bearer-token gated (SPARROW_OPS_TOKEN) · returns every dl:<email> record sorted newest-first
    • /ops/release POST ?email=<addr> · same auth · clears the dl:<email> + fail:<email> entries, sub row untouched
    • Ops routes return 503 "ops-not-configured" when the secret isn't set — intentional so a forgotten binding is obvious
    • Aggregation module + block-lookup prefetch (rolled back during branch churn) re-land in v0.39
  37. v0.37 shipped 2026-04-28

    HUD layout pass · lane · federation pulse · density

    • src/lib/sparrow-lane.ts — laneFor(pathname) maps Astro.url.pathname to one of six lanes (reading / federation / session / artifact / capability / meta) plus a short label
    • SPARROW_VERSION = "0.37" exported from sparrow-lane so the HUD sub-line and the lane lib can never drift
    • SparrowLayout sub-line drops the hard-coded "pointcast reader · v0.x" string; renders {label} · v{SPARROW_VERSION} with [data-sp-lane="…"] tint per lane
    • Federation pulse dot in the wordmark (.sp-fed-dot) — 800ms moss bloom on every kind-20078 friend-presence event; v0.30 ambient watcher dispatches sparrow:fed-pulse, the dot listens
    • Density toggle next to theme toggle — comfortable (default) ↔ compact; persists under sparrow:density; writes <html data-density>; layout-level baseline tightens HUD chrome in compact mode for free
    • SW_VERSION → sparrow-v0.37.0
  38. v0.38 shipped 2026-04-28

    TV mode · Phase 2 — federation channel

    • /sparrow/tv/friends — federation-channel TV; same ten-foot ambient chrome as /sparrow/tv (slide rotation + channel-tinted wash + surface-detect + here-strip + vmin-scaled type)
    • Slide pool hydrated from friends' kind-30078 sparrow-public-saved-v1 events: WS REQ across the relay pool with { kinds:[30078], authors:<friends>, '#d':['sparrow-public-saved-v1'] }, newest-per-author, flatten saved IDs
    • Server pre-renders 12 fallback slides + a 60-block lookup map so the surface never looks broken before the WS catches up
    • Friend-attribution chip on each slide — moss outline + avatar + "saved by Alice" — hydrated from sparrow:profiles cache (display_name → name → 8-char hex)
    • Federation strip (.sptv-fed-strip) replaces the rosette: avatars of follows currently feeding the rotation + total signer count
    • Empty state when no friends configured: a calm CTA pointing at /sparrow/friends; "all channels" fallback link to /sparrow/tv
    • Privacy default: avatars hidden until sparrow:tv-private-ok="1"; count-only mode shows "N signers" without revealing who
    • /sparrow/tv + /sparrow/tv/friends added to SHELL_URLS for offline-first ambient broadcast
  39. v0.39 next planned

    TV Phase 2 finish + worker aggregation re-add

    • /sparrow/tv/saved — personal-channel TV; opt-in only via sparrow:tv-saved-ok (kitchen TVs don't leak personal lists by default)
    • /sparrow/tv/remote — phone-as-remote handshake (QR pair → state-only WebSocket back to the TV; controls limited to step / lock channel / unlock / wake)
    • Re-land workers/sparrow-digest/src/aggregate.ts (pure-function port of /sparrow/signals Panels 1 + 3) with HTML table rendering
    • Block-lookup prefetch from /blocks.json at cron start · cf.cacheTtl:300 · one fetch per tick, shared across subscribers
    • Native Sparrow.app gains NIP-01 kind-0 profile lookup so aliases auto-populate without going through the web
  40. v1.0 north star

    The reader the broadcast deserves

    • Full offline archive (300+ blocks) in Service Worker + IndexedDB
    • Cross-client read state via Nostr kind 30023 addressable events
    • Agent-mode view served from /sparrow/llms.txt for machine readers
    • Federated reading lists — subscribe to a friend's list as a feed
✦ design system · quick reference

palette

OKLCH, theme-aware via color-mix()

ink blue-hour bone ember moss oxblood lilac

channels

one OKLCH token per channel, computed from pointcast brand hexes

FDCRTSPNGFGDNESCFCTVSTBTLBDY

typography

displayGloock

uiInter Tight

monoDeparture Mono

motion

all animations respect prefers-reduced-motion

  • needle sweep (12s linear infinite)
  • beacon radar sweep (6s)
  • receiving pill pulse (2.4s)
  • tuning progress bar (scroll-driven)
  • reel → reader view-transition morph