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
Refs #127 #135 #136
Decision
Adopt a push-based split runtime for hosted cards:
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
Identity, devices and conflicts
Offline and stale behavior
Scope
Done when
featand therefore a minor release under project versioning.Refs #127 #135 #136