Skip to content

feat: push privacy-filtered local playback state to hosted cards #140

Description

@rowkav09

Decision

Adopt a push-based split runtime for hosted cards:

media server / Spotify / browser plugin
              |
              v
local nowplaying app
  |-- local normalized state --> Discord Rich Presence (never leaves PC)
  `-- privacy-filtered card state --> authenticated ingest API
                                          |
                                          v
                              Vercel or self-hosted state store
                                          |
                                          v
                                    public SVG endpoint

The hosted card service must not connect back to a user's Plex, Jellyfin, Navidrome, Emby, browser or Discord client. The local app detects once, sends only the fields the user explicitly enabled for the card, and uses the same full local state for Discord Rich Presence.

Why this changes the Vercel fit

A request-driven Vercel function becomes practical when it only accepts small authenticated updates, reads short-lived sanitized state from a durable store, and renders/caches an SVG. It no longer needs LAN/tailnet access, provider credentials, background media-server polling or long-lived processes. A self-hosted service can implement the same protocol.

Privacy and security contract

  • Provider credentials and raw provider responses stay on the PC.
  • Card-field allowlist is applied before upload; disabled title, artist, user, server, artwork, progress or links never reach the host.
  • Discord formatting/state remains local and may use fields that the public card is not allowed to receive.
  • Uploads use per-install/device credentials, TLS, replay resistance, bounded payloads and schema/version validation.
  • Hosted logs, errors and metrics contain no titles, artists, usernames, artwork URLs or raw payloads.
  • State has a short expiry and explicit delete/disconnect; no listening history by default.
  • Artwork needs a separate decision: sanitized bounded bytes, approved public URL, or omit. Never let the host fetch arbitrary client-supplied URLs.

Identity, devices and conflicts

  • One account can register multiple device IDs.
  • Each update carries account, device, monotonic sequence, observed-at and expiry.
  • Reject old/replayed/out-of-order updates.
  • Define active-device policy. Initial default: most recent actively-playing device wins; paused cannot replace another currently-playing device until that state expires. Let users pin or name a preferred device later.
  • A device can clear only its own state. Account-level revoke clears every device credential/state.
  • Public card URLs use an opaque, rotatable card ID, never email or media-server identity.

Offline and stale behavior

  • Hosted state expires after a documented TTL if heartbeats/updates stop.
  • Local app sends state transitions plus a low-frequency heartbeat, not constant progress ticks.
  • Renderer derives progress from uploaded position + observed-at while state is fresh.
  • After expiry, serve the configured idle/private card or 204, never stale media forever.
  • Queue updates briefly during transient network failure; bound queue size/time and never persist history by accident.

Scope

  • Write a versioned provider-neutral ingest schema and threat model/ADR.
  • Implement local privacy projection separate from Discord projection.
  • Implement authenticated ingest, durable TTL state and public SVG read path behind a storage adapter.
  • Ship one Vercel adapter and keep framework-neutral/self-hosted compatibility.
  • Add device registration/revocation and multi-device resolution.
  • Document costs/caching and a migration path from direct provider polling.

Done when

  • Hosted service has zero provider credentials and needs no inbound connection to the PC/LAN.
  • Packet/payload tests prove fields disabled for card output never leave the local process.
  • DRP still works with network upload disabled or host offline.
  • Authenticated updates are schema-validated, size-bounded, replay-resistant and rate-limited.
  • Multi-device ordering, simultaneous playback, pause, clear, replay and clock skew are tested.
  • Stale state expires into configured idle/private behavior.
  • Vercel deployment uses durable TTL storage; no correctness depends on function memory.
  • SVG route has ETag/cache behavior and never leaks ingest/auth identifiers.
  • Disconnect/revoke deletes state and invalidates device credentials.
  • Plain-language setup/privacy docs show exactly what leaves the PC.
  • Shipping core architecture is a feat and therefore a minor release under project versioning.

Refs #127 #135 #136

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions