# dailysplice — handoff

> Append-only, **newest first**. What the last session did, what the next should
> pick up, what is blocked. Read this second (after `brief.md`) when orienting.
> Clear-after-read: once an entry is acted on it can be trimmed.

---

## 2026-08-05 — landing page stripped; Spotify connect built (v0.2.0)

### Blocked on rian — one step, then Spotify works

**Register a Spotify app and fill in two placeholders.** Nothing else is
outstanding; the whole flow is built and deployed behind them.

1. <https://developer.spotify.com/dashboard> → Create app.
   Redirect URI, **exactly**: `https://dailysplice.com/spotify/callback`
   (it is derived from one place in the code, so it must match byte for byte).
2. Edit `/srv/apps/dailysplice/.app.env` in your own editor — **do not paste the
   values into a chat window** — replacing `SPOTIFY_CLIENT_ID` and
   `SPOTIFY_CLIENT_SECRET`.
3. Confirm without printing anything:
   `grep -cE '^[A-Z_]+=REPLACE_WITH_' /srv/apps/dailysplice/.app.env` → `0`
4. `srv-gw deploy --project dailysplice`
5. In the Spotify dashboard, add each person's **Spotify account email** under
   *User Management*. Only allowlisted accounts can connect (see below).

**Your own Spotify account must be Premium**, or the app stops working for
everyone — that is a Spotify rule about the app *owner*, new in February 2026.

### Platform: Apple Music is the only one that permits the full product (§3d)

**YouTube is out for the player architecture** — checked its API Terms directly.
Three clauses each independently forbid it: no player other than YouTube's own,
no "alternate audio tracks" applied to content, and no background play. It
remains usable for the *playlist* model, never the radio model. Do not plan on it
as a second home.

That leaves **Apple Music** (MusicKit plays full tracks for subscribers, no user
cap) as a single point of failure for the full product. Same class of risk
Spotify just demonstrated — the `MusicProvider` boundary in `agents.md` does not
remove it, only bounds the cost of a forced move.

**On "grow to 250k, then qualify for Spotify":** coherent, but do not architect
toward it. At 250k MAU the gate no longer matters; clearing it only makes you
*eligible* under a discretionary review; and success on Apple Music makes
DailySplice read as a Spotify **competitor**, which cuts against approval. Full
reasoning in §3d. Treat Spotify Web API access as a lottery ticket nothing
depends on.

### The podcast delivery mechanism changed — read plan §3c before building F3

The original shape ("DailySplice hosts podcasts, Spotify reads them by RSS")
**cannot work for per-user content.** Verified: **Spotify does not let listeners
subscribe to arbitrary or private RSS feeds** — its own paid-podcast private
feeds are explicitly usable only in *other* apps. The only route into Spotify is
a public catalogue show, which for personalized episodes means publishing "your
dad Jim has been listening to…" as a publicly searchable episode, with no
programmatic show creation and a mass-AI-upload pattern that is exactly what
Spotify's spam enforcement targets. Feed ingestion latency (hours) breaks
"daily" regardless.

**The replacement architecture: DailySplice is its own player.** Episodes become
ordinary private audio files, interleaved with music streamed from the user's own
subscription — no RSS, no catalogue, no privacy leak, and a genuinely better
experience (the DJ can talk over an intro and cross-fade, which a playlist
cannot). **Apple Music/MusicKit permits this with no user cap.**

**Prototype this ONE thing before building on it:** interleaving DRM-protected
music with your own audio in a seamless queue is *not* a documented operation.
Browser DRM is finicky (Chromium can drop to 30-second previews); expect two
players and a cross-fade, and note a native iOS app has more control. The whole
product rests on that interaction feeling smooth.

**Ship the appreciation splices first** ("understanding jazz", "a lifetime of
Nick Cave"). No per-user generation, no private data, shareable — and they can
go out on Spotify **today with zero Web API access**: one public show via
Spotify for Creators plus a companion playlist.

Risk table (voice cloning, hallucination, consent for relayed listening,
speech-only episodes, per-user generation cost) is in plan §3c. The
"acquire a grandfathered app" route is assessed there too: fragile, lawyer-first,
**not roadmap material**.

### Audio features are NOT lost — do not design around their absence

Probed the old app: it has the same reduced surface as a new one (no
`audio-features`, no `recommendations`). **That does not block tempo/energy
ordering.** Verified live against **ReccoBeats**: the full Spotify feature schema
(`tempo`, `energy`, `valence`, `danceability`, …) looked up **by Spotify track
id**, batched 20 at a time, free and unauthenticated:

```
GET https://api.reccobeats.com/v1/audio-features?ids=<spotify_id>,…
```

Two traps recorded in plan §3b: it **403s a default `Python-urllib` User-Agent**
(set one, or misconfiguration looks like a catalogue miss), and the 40/40 result
measured here used ids drawn from ReccoBeats' own index — **biased**, so it
proves the mechanism, not coverage. **Measure real coverage against an actual
library before any splice rule depends on a feature, and make the engine degrade
gracefully when one is missing.**

So: the splice engine's musical intelligence is *not* gated. Only the final
write into another person's Spotify account is. Keep the two problems apart.

### Run this before deciding anything about Spotify

`main/scripts/spotify-probe.py` reports what a given Spotify app can **actually**
reach — client-credentials only, no user login, no browser, nothing sensitive
printed. It exists because rian has a dormant app from an earlier experiment and
the grandfathering rules are too tangled to reason about safely.

```
cd /srv/apps/dailysplice/main && .venv/bin/python scripts/spotify-probe.py
```

It reads `SPOTIFY_CLIENT_ID`/`SPOTIFY_CLIENT_SECRET` from `.app.env` (or the
environment) and refuses to run on placeholders. **The result that matters:**
whether it retains **audio-features / recommendations**. Those were cut for new
apps on 2026-11-27 and are exactly what a playlist builder wants. If the old app
kept them it is genuinely valuable — do not delete or replace it. If not, the
old app offers nothing over a fresh one.

### Soundiiz question — answered 2026-08-05, see plan §3b

"Soundiiz connects to Spotify without 250k users, so how?" — **because it was
founded in January 2013 and is grandfathered.** Spotify's May 2025 announcement:
developers *previously* granted extended access are unaffected. Same at the
February 2026 cut. The objection is factually right and points the wrong way:
existing apps kept access, new ones cannot get it. Do not reason from "that app
can do it" to "we can".

### The constraint that changes the product — read plan §3b

"A Connect to Spotify button anyone can use" is **not reachable on Spotify's
current policy**, and this is the most important thing in this session:

- Development Mode allows **5 users**, each hand-added in the dashboard.
- Extended quota now requires a **registered business**, a launched service, and
  **250,000 monthly active users** — individuals may no longer apply at all.
  It is circular: you need 250k users to be allowed more than 5.
- New apps also permanently lost **Recommendations and Audio Features**
  (Nov 2024), so the smart-mix logic has to be ours.
- **Episodes *can* go in playlists** — but only if they are published on
  Spotify. A raw uploaded audio file can never be spliced into a Spotify
  playlist. That decides §3 question 1.

**What is NOT blocked, and it is most of the product:** podcast distribution to
Spotify goes through **RSS via Spotify for Creators** — a separate system with
no quota, no allowlist, no MAU test. Hosting the shows, getting them onto
Spotify, and having episodes be playlist-addable all work. The blocked operation
is narrow: writing a playlist into an arbitrary user's Spotify account.

Full detail, the five options, and the verified alternatives (Apple Music
MusicKit, YouTube Data API, TIDAL) are plan §3b. Do not re-derive it from
memory; it changed three times in 18 months and every change tightened.

### Built this session

- Landing page is now wordmark + "Welcome to DailySplice" + Sign in. The
  `<meta description>` pitch is gone. **Note the SPA-bundle caveat in
  `agents.md`** — signed-in copy ships to anonymous visitors, so component text
  is public writing. Verified the distinctive idea is not in the bundle.
- Spotify OAuth, `state` anti-CSRF, Fernet-encrypted tokens, refresh with a 90s
  margin, connect/disconnect, and a card covering all four states (not
  configured / not connected / connected / revoked).
- `/spotify/*` is behind the default-deny middleware. Gated *browser* paths now
  redirect to `/` rather than answering a navigation with JSON.
- `gate-f0.sh` is up to **23/23**, including a check that no unencrypted token
  is ever stored.

### Not done, deliberately

- **Nothing calls the Spotify API beyond `GET /me`.** No playlist reads or
  writes yet — that waits on plan §3, because what gets spliced decides what to
  fetch.
- **No re-encryption path for `TOKEN_ENCRYPTION_KEY`.** Rotating it means users
  reconnect. Accepted at five users; see plan §6.

## 2026-08-04 — F0 foundation built and deployed (v0.1.1)

**State: the foundation is done and live. The product is not started.**
<https://dailysplice.com> serves a public landing page, sign-in with a BW
account works end to end, and invite-only admission is enforced. There is no
Spotify integration, no episode model, and no splice engine.

### What the next session should do first

**Fill `.logs/planning/dailysplice-2026.md` §3 with rian.** It is deliberately
empty — five questions, in the order they block code, the biggest being *what a
"user-created podcast episode" actually is*. Guessing that answer is how this
project stalled before. **Do not start F2 (Spotify) until §3 has answers**; the
Spotify work depends on decisions §3 makes (whose account holds the playlist,
what gets spliced in).

### Verified working

- `main/scripts/gate-f0.sh` passes **17/17**, both through `https://dailysplice.com`
  and against the bridge. Run it after any change; it is the F0 contract.
- **Sign-in proven with a real account**, not just curl: rian completed the OAuth
  round-trip, the invite matched on email, and `users` holds
  `rian / superadmin`. `invites.redeemed_at` is stamped.
- `srv-gw deploy --project dailysplice --build` works (see the caveat below).
- Auth boundary: 41 pytest cases green; `ruff check` clean.

### Open / needs rian

- **`jim` has no password yet.** The BW account exists
  (`jim` / jsbowden58@gmail.com) and his invite is seeded, but `has_password` is
  false, so he cannot sign in. Rian must run this himself — it is interactive on
  purpose, so the value never reaches a transcript:
  ```
  srv-gw id-user-set-password --user jim
  ```
  He can optionally link Google afterwards at
  <https://auth.bowden.works/account>. **No `id-user-grant` is needed** —
  dailysplice.com is un-gated, so site grants do not apply; admission is this
  app's own invite table.
- **`jim`'s surname is an assumption.** Set from the email as "Jim Bowden".
  Correct with `srv-gw id-user-create`-adjacent tooling if wrong; it is display
  only and touches nothing else.

### Traps that will bite you

- **Never gate this host.** `import id-auth` on `dailysplice.com` 404s
  `/login` and `/auth/*` — the paths this app owns. To hide it, use
  `srv-gw id-site-set --host dailysplice.com --lan-only true`. Full reasoning in
  `agents.md`; the F0 gate asserts the conf stays un-gated.
- **`.bw-auth.env` is `600 srv-gateway`** and rian cannot read it. That is
  correct. It means container recreates must go through `srv-gw deploy` — a raw
  `docker compose up` run as rian fails on the unreadable `env_file`.
- **`srv-gw fix-permissions` re-widens that file to `660` group-readable** every
  time it runs. Re-tighten it afterwards; the exact three-command recovery is in
  `agents.md`. Hit and corrected this session.
- **Do not pre-stage `.bw-auth.env` with placeholders.** It seems right (and
  `coding.md` says to stage secret files that way) but `app-client-register
  --secret-out` cannot overwrite a file it does not own, and it fails *after*
  creating the client. See `.logs/v8-feedback.md`.
- **The security audit reports this project as "built but NOT deployed"** with a
  ~419-minute drift. It is a false positive — a UTC-offset bug in the audit that
  hits every project identically. Verified against the running image. Details in
  `.logs/v8-feedback.md`; ignore that specific warning until the audit is fixed.

### Deliberately not built yet

- **No router.** There is exactly one screen, so `react-router-dom` would be an
  unused dependency. Add it when the second screen lands.
- **No accessible-primitive library vendored.** Nothing here needs a dialog,
  menu or toast yet. The first one that does vendors shadcn/Radix as source
  (`frontend.md`) rather than hand-rolling.
- **No invite write UI.** `GET /api/admin/invites` is read-only; issuing and
  revoking are F6. Today invites are rows, seeded by `main/scripts/seed.py`.
- **No backups.** The `pgdata` volume is unbacked. Fine with no user data;
  resolve before anyone's playlists matter.
