# MeshBay — Playlists (design) > Status: **built**, 2026-09-16, in five commits. This document was written as > a proposal and has been corrected against what the code turned out to need — > §0.2 lists every place the proposal was wrong, because a design document that > quietly agrees with the implementation afterwards is a document nobody can > use to check the implementation. > > It was deferred out of the Music application's design, which deferred it for > the right reason: *"a genuinely > new category of per-account node state, not covered by anything E9 already > enumerates — needs its own design pass (ownership, sync across devices, > whether it's node-local or something else)"*. This document is that pass. > > Read `MESHBAY_DESIGN.md` §9.8 first — Music is built, and this adds nothing > to its playback *path*. Read §9.11 second: the cross-group consolidation > this feature needs already exists there, and most of the work is > recognising that. > > **Scope, settled before writing this:** a playlist belongs to **one > account** and is never shared with other group members. That answer is what > keeps §6 small; see §13 for what changes if it is ever reversed. > > Follows the project convention: every claim names the adversary or the > failure it holds against (§12). --- ## 0. What was asked, in one paragraph Somewhere to keep a user's playlists, so that they survive a cache clear and turn up on that person's other devices — and so that one playlist may hold albums from **several different groups on several different nodes**, the way the Search page already searches a consolidated view. With the constraint, stated up front, that nodes go offline for an evening or for a month and that this must not corrupt anything. And — added after the first draft, and now half of this document — **a user interface**: the queue verbs (play, play next, enqueue) that a music player is expected to have, a way to build a playlist from what is on screen, a way to manage playlists from one button, and none of it breaking the album auto-advance and prefetch that already work. ## 0.1 What changed from the first draft, and why The first draft answered the storage question and stopped. Three things in it were wrong or missing, and they are corrected here rather than patched: | | First draft | Now | § | |---|---|---|---| | Size | one blob for everything, capped at 256 KB | **manifest + one blob per playlist**, compressed, stored as a BLOB; caps per unit | §4, §5 | | The stored track reference | "takes it unchanged" into `onPlayQueue` | **it does not** — `size` and `name` are missing and both are required to fetch a track at all | §5.3 | | UI | one route `/playlists` and a sidebar entry | **dropped.** A menu in Music's sticky toolbar, which Search → Music inherits for free | §9, §13 O7 | The 256 KB figure contradicted its own worked example: the draft estimated 1 000 tracks at ~200 bytes as "~200 KB", against a cap applied to `blob_enc`, which was a **base64 TEXT** column — so its own example was already ~267 KB on the wire, over a cap it declared generous. §4 redoes the arithmetic with the fields that actually have to be there. --- ## 0.2 What building it corrected Fourteen things. They are listed rather than edited away because the ones that were *wrong* are more useful than the ones that were right. | | The proposal said | Building it found | |---|---|---| | 1 | MNP MINOR bump to **2.1** | The wire version was already **3.0**; the bump is **3.1**, and `MNP_MIN_SUPPORTED` does not move because nothing here is required | | 2 | `kind` is `playlist:` | That refuses `playlist:favorites` — the one playlist every account has. The pattern is `[A-Za-z0-9_-]{1,64}` | | 3 | The blob is **msgpack**, "same as everything else on MNP" | The node never parses it, so the encoding is a private choice; MNP's codec is private to `transport.js`, a classic script. **JSON**, which is what keeps the merge runnable by `node` with nothing around it | | 4 | Delete confirms inline, "not `window.confirm()`, which blocks the SPA" | Right, for a reason nobody had yet. `prompt()` **throws** in Electron; `confirm()` opens, and leaves the window unable to type once it closes. A *name* needs a field; a *confirmation* uses `ask()` from `ask.js`, drawn by the page | | 5 | Submenus fly out, and are a sheet on a coarse pointer | They **expand in place**, which the account menu's language list already does. No flipping, no hover intent, no separate mobile design. The tracklist level loads when it is expanded | | 6 | Add all four new files to `STATIC_FILES` / `SPLIT_FILES` | Those lists check hook ordering. The four hookless modules do not belong in them, exactly as `source-merge.js` does not; only `menu.js` and `playlist-menu.js` do | | 7 | Four modules | **Five**: `playlist-crypto.js` is split out so seal/open can be executed standalone. A crypto layer that cannot be executed is one nobody has checked | | 8 | "Save the current queue" in the queue panel **and** the toolbar menu | The queue is the player's own state. Mirroring it into a menu in another component would mean lifting that state out of the player for a menu item, so it lives in the queue panel alone | | 9 | — | **The `op` hazard**, missed entirely. `group-page.js` and `search-page.js` both wrap `onPlayQueue` with a two-argument signature and forwarded two, so every "add to queue" arrived at the player as a plain play. §9.6 | | 10 | — | **`user_blob_list` is what makes sync quiet.** Comparing against the merged watermark says nothing about what *that node* holds, so the first version pushed every body on every sync. §7.1 | | 11 | — | **A tombstoned playlist's body must be deleted from each node**, or the account's quota fills with graves. §7.2 | | 12 | The IndexedDB version is bumped "in the existing `onupgradeneeded`" | And must stay there, in one file. Two modules opening one database at versions of their own is a `VersionError` thrown at whichever runs second. §14.2 | | 13 | deflate is worth about three | **4.5×** on realistic data — and the first fixture said 23× because it repeated one content hash on every track, which is a property of the fixture. §4.2 | | 14 | `body_rev` alongside `rev` | Right, and worth saying why: a rename and a track added elsewhere both write n+1 under one counter, and one of two non-overlapping edits is lost. §5.1 | --- ## 1. What this design does not reopen - **Views over the index, never a catalogue** (`MESHBAY_DESIGN.md` §6.5, §9.1). A playlist is a list of *references*; it creates no second identity for a file and no server-side database of content. - **Nothing about content reaches the hub** (H7). §3.1. - **No new streaming path.** `MESHBAY_DESIGN.md` §9.8 stands untouched: a track is fetched through `pipelinedDownload` and handed to `