Changelog
All notable changes to SoundAssist. Newest first. We follow loose Keep a Changelog conventions but without strict semver yet — beta means everything is in flux.
Tags map to git tags. Commit hashes are short SHAs.
[0.1.2] - 2026-06-16
Changed
- The Circle is now the landing page. Anonymous visitors hitting
/see The Circle front door directly — the upload form, the pitch, the community invite — instead of the LandingBillboard. No redirect; rendered in-place inside AppShell so the sidebar and navigation remain visible. The/circleroute continues to render the same component unchanged.
Shared Folders hub — /shared-maps is now for collaborating on folders (2026-06-02)
The old "My Relationships" roster (a permanent "dedicated room" per client/peer/collaborator) is retired. The Shared Folders tab now does what its name says: make folders you co-own with other people, invite them in, and work on the files together — in a live session or anytime.
What a user notices
- "Shared Folders" in the sidebar (was "Shared Maps"). It lists the folders you share — "Shared by you" + "Shared with you" — as cards.
- Create + invite in one flow. "New shared folder" → name it → the invite panel opens right away.
- Two ways to invite: search a person by name or @handle and add them instantly, or invite by email (they get a join link). Every member can add + download files; new uploads show up for everyone.
- One system, two places. Folders shared during a live session and folders made here are the same thing — no second mechanism to learn.
Under the hood
- /shared-maps rebuilt on
listFolders+folder_members+shared_folder_invites— no new tables, no migration. NewAddMemberSearch(reuses/api/profiles/search) +NewSharedFolderModal. - Removed the dead relationship-roster code the tab no longer uses:
ClientCard, the/shared-maps/[clientId]detail route, and the orphanedlib/clientsCRUD wrappers (updateClient / updateClientRole / deleteClient / ensureClientRootFolder). Therelationshipstable, the /music dashboard, and the session/upload backend are intentionally left for a later, deliberate retirement. - Deferred: a "start a live session from a shared folder" bridge — it threads through the live-session core and needs two-participant verification, so it's a separate, careful step.
v3.0.5 — DB layer closed, OG fixes, bundle ratchet, undoable deletes (2026-05-27 evening)
Second push of the day after v3.0.4 landed. Closes the database hardening layer per the plumbing → DB → backend → frontend volgorde Andre keeps, adds perf + safety scaffolding, and fixes the WhatsApp share-preview bug that Andre reported live.
What a user notices
- WhatsApp / iMessage / Slack share previews work end-to-end.
Three real bugs landed in one round of fixes: the Next.js 16
params-Promise shape change broke every OG image generator
(params.token was always undefined → fallback rendered); the
PostgREST
host:profiles!tracks_host_user_id_fkeyshortcut failed silently because tracks.host_user_id references auth.users(id) not profiles(id); and the rendered PNG was 780 KB which WhatsApp's link-preview fetcher silently drops above ~600 KB. Now: 500 KB PNG with artwork + title + artist. (a29a143 + 7b17fa0 + ddb90e2) - Two browser tabs no longer play through each other. When you press play in tab B while tab A is playing, tab A pauses automatically. Implemented via a BroadcastChannel courier in lib/playback-coordinator.ts; wired into PipPlayer (dashboard) + the /listen WaveSurfer player. (cb065b5)
- Undo for destructive actions. Delete track, delete folder,
leave shared folder, delete post, delete comment — all now show
a 5-second "Undo" toast instead of an immediate destructive
commit. If you click Undo within the window, the row comes back
- no server-side mutation fires. Otherwise the actual API call runs. (36806ec + 9db572c)
- Dashboard activity feed updates live. Was: one fetch on mount, never refreshed. Now: Supabase Realtime channel + 30s poll fallback when the channel can't connect. Same hook applied to the dropdown feed. (0d7b3bf)
Behind the scenes — database layer
- PostgREST schema-cache reload policy (P10.15) — docs/migrations-policy.md captures the full apply flow (Supabase SQL Editor → npm run db:types → NOTIFY pgrst → smoke query) with the 2026-05-27 PGRST205 incident as the worked example. (379d2b1)
- Schema drift detector (P10.16) — scripts/audit-schema-drift.mjs replays migrations in SQL text order, parses lib/database.types.ts via brace-counting, fails CI when the types file diverges from the migrations folder. Caught one real-but-acceptable orphan during the initial run. (e41f55d)
- GDPR Article 17 cascade audit (P10.17) — scripts/audit-gdpr-cascade.mjs cross-references every FK to auth.users / public.profiles with lib/account-deletion.ts. Every user-PII column must be covered by ON DELETE CASCADE, an explicit delete call, or an EXEMPT entry with reason. 41 FKs, all covered. (0cc8615)
- DB-audit gate restored (P10.18) — audit:db-indexes had been excluded from audit:all with a stale comment. Root cause was a SQL comment containing a semicolon truncating the CREATE TABLE body regex. Fixed and re-added. (379d2b1)
- Slow-query weekly digest (P10.19) — supabase/migrations/20260527_slow_query_digest.sql enables pg_stat_statements + adds two SECURITY DEFINER helpers (slow_query_top, slow_query_reset). New /api/cron/slow-query-digest runs Monday 09:00 UTC, emails top 5 slowest queries to ENGINEER_NOTIFY_EMAIL, logger.warn on any query with mean > 500ms AND calls > 100. Migration pending Andre to apply. (e239a19)
Behind the scenes — perf + safety
- Realtime resilience hook (P8.1) — hooks/useRealtimeWithFallback wraps the subscribe-or-poll pattern. Slow poll (5min) while the channel is live, fast poll (30s) when it isn't, tab-visibility guarded, immediate refetch on reconnect. Scheduling policy extracted to four pure helpers with 21 unit tests. (0d7b3bf + 8beb3dc)
- next/image migration complete (P12.3) — 23 → 0 raw
<img>migration candidates across 18 files. Audit ratcheted: any new raw<img>fails CI unless it falls into an explicit exempt category (blob preview, og-image, local SVG, data: URI). (bc33df5 + 507fd65 + 6c0c5b5 + a7c50ba) - Bundle-size baseline + ratchet (P12.2) — 250 → 212 KB gz on the
initial bundle by dropping
Sentry.replayIntegration()(recorder was loaded even at 0% sampling; tracked as tech-debt #9 with the lazy-load recipe to re-add when needed). New audit:bundle-size fails CI at > 220 KB gz; docs/operations/bundle-size.md captures the per-chunk breakdown + what's already lazy. (a97940c) - Lazy-dep import guard — scripts/audit-lazy-deps.mjs ensures wavesurfer.js / livekit-client / @livekit/components-react are never statically imported into the client bundle paths. ALLOWED set documents the two session-only hooks that legitimately static-import (per-route code-split keeps them out of the initial bundle). (2cd8e88)
- Undoable toast pattern (P6.14) —
toast.undoable({ message, onUndo, onCommit })extends lib/toast with a deferred-commit shape. Wired into 5 destructive actions; the pattern documented inline so future destructive actions adopt it in three lines. (36806ec + 9db572c)
Audits: 36 → 38
New ratchets shipped this push:
- audit:schema-drift (P10.16)
- audit:gdpr-cascade (P10.17)
- audit:bundle-size (P12.2)
- audit:lazy-deps
Migrations to apply (Andre)
Open Supabase SQL Editor and paste:
supabase/migrations/20260527_slow_query_digest.sql— enables pg_stat_statements + adds the SECURITY DEFINER helpers the weekly digest cron uses. Thennpm run db:types+ commit the regen.
v3.0.4 — Content moderation, account lifecycle, help, hardening (2026-05-27)
A long autonomous-night push: 40+ commits adding visible product surfaces, moderation infrastructure, account-management endpoints, audit gates, and 250+ tests. Whole picture: SoundAssist now has the safety + ops scaffolding a paying-customer product needs.
What a user notices
- /help knowledge base — 18 articles across 6 categories (getting started, tracks & uploads, sharing, sessions, Connect, account & privacy). Server-rendered + indexable so product-aware searches like "how to share a folder soundassist" land directly on the relevant article. (22818b0 + 386db8f)
- Report inappropriate content — flag icon on posts (more surfaces coming). Reason picker + optional message. Reports land in /admin/moderation. 3+ distinct reporters auto-flag a target for fast-track review. CSAM is immediate, zero tolerance. (7f72c9b + 4b896bb + e4cbf95 + 19e8b3b)
- /settings/account — change your email + username from the UI. Email change is a double-confirm flow (Supabase sends link to the new address; original gets a "change requested" notice). Username change takes effect immediately (old URLs 404 — see the help article for the trade-off + workaround). (1f03097 + 676e464 + 26dfe80)
- /legal/tos — engineering-draft Terms of Service. Pending lawyer review, marked accordingly. (2e9bb55)
Behind the scenes
- Math.random room-code fix — booking + intake + public-feedback
- workspace-provisioning all generated 4-char room codes from Math.random (~13 bits of entropy). Now uses crypto.randomBytes via the shared lib/room-code.ts. 32^8 ~ 1.1T values. (7a7f1fe)
- Bulk track operations — POST /api/tracks/bulk moves or deletes up to 100 tracks in one call with per-id outcomes (not_owner / r2_failed / db_failed). Ready for the bulk-select UI to wire. (d0a5b7c)
- Content moderation backend — supabase/migrations/20260615_content_reports.sql, lib/content-reports.ts, POST /api/reports, GET+PATCH /api/admin/reports, /admin/moderation queue page, ReportButton component.
- Notification coverage audit — every ActivityType in lib/activities.ts must appear in either the in-app bell OR the email digest (or be on a justified opt-out list). Caught 7 real gaps; baseline allow-listed with REVIEW markers. (b5563bf)
- Hardcoded-URL audit ratchet — scans the source tree for
literal
app.soundassist.onlinereferences. Baseline 54; the audit fails CI when the count grows, encouraging gradual cleanup. (e971402) - Cron schedule sync audit — verifies every cron route on disk is registered in vercel.json (and vice versa) + every cron has a maxDuration entry. (ac79094)
Test coverage + tooling
- +250 tests (968 → 1218+ across the night).
- npm run ship-ready — single command, ~9s, typecheck + lint + tests + 30 audits. Pre-push verification standard.
- npm run audit:all — all 30 audit scripts in ~1.5s.
- npm run vendor:cost-snapshot — projected monthly vendor spend from local DB sums + free-tier limits.
- npm run r2:storage-estimate — same idea for R2 storage alone.
Ops + docs added
- docs/operations/secret-rotation.md — yearly cadence + per-vendor procedure for all 14 secrets.
- docs/operations/cron-heartbeats.md — BetterStack setup guide; all 8 cron routes wired to no-op heartbeats.
- docs/operations/r2-lifecycle.md — Cloudflare R2 lifecycle rules
- storage-estimate command.
Migrations to apply
Two SQL files added under supabase/migrations/; both need to be run via the Supabase SQL Editor:
- 20260610_processed_webhook_events.sql (webhook idempotency)
- 20260615_content_reports.sql (moderation queue + auto-suspend RPC)
After applying, npm run db:types regenerates lib/database.types.ts
so the temporary as any casts can be removed.
v3.0.3 — The "stop showing users our internals" marathon (2026-05-26)
A single-day push that touched 27 commits across the platform. The through-line: everywhere a non-technical user could see system internals — Postgres errors, Supabase rate-limit text, raw fetch failures, undecodable audio crashing the renderer, blind uploads with no feedback — gets replaced with plain-language UX and defence-in-depth.
What a user notices
- Storage quota in /files header — colour-coded "X GB / 5 GB" bar (grey → amber 70% → red 90%) with per-bucket hover breakdown. Auto-refreshes when an upload finishes. (188e33d)
- Upload banner became adult — size, smoothed transfer speed, ETA, and a Cancel button. Failed file in a batch no longer kills the whole queue; end-of-batch summary toast tells you what stuck. (333bfc0)
- Share-link revocation — "Rotate link" button next to Copy link on every track row (owner-only). Old URL stops working immediately; new URL copied to clipboard. For when a link leaks. (84952c8)
- Folder rename + delete + create — was missing entirely from the API; the UI buttons fell back to direct Supabase writes that bypassed every standard gate. Now full CRUD with same-origin + Bearer + rate-limit + ownership checks. Tracks in deleted folders fall back to root (not lost). (9142531 + 7d8fce9)
- Daily canary cron at 05:00 UTC hits 13 anon-reachable surfaces (auth pages, marketing, all legal pages, sitemap/robots) and fires a Sentry alert per broken surface. On-demand "Run now" from /admin/canary. Caught the first regression within hours of shipping: /legal/privacy was redirecting to /login (GDPR Art 13 violation). (105d803 + 51af8b5 + 7cde9b0 + 5f3d857)
Auth security
- Password reset hardening —
signOut({ scope: "others" })after reset so any attacker session on the old password is killed instantly. Min length bumped 8 → 12 (NIST 2017). Trivial-dictionary reject. (79b3171) - Magic-link anti-enumeration — sendMagicLink no longer surfaces raw Supabase error text (which leaked account existence via per-account rate-limit messages). PKCE callback failures now render an amber "Sign-in didn't complete" banner on /login instead of a silent fail. (3a79f81)
- Forgot-password anti-enumeration — same fix as magic-link; always show "Check your inbox" unless transport error. (79b3171)
- Sign-out helper enforcement — three pages bypassed lib/signout and left user-scoped localStorage on the device for the next visitor. All three converted; new CI gate prevents regression. (5a717b8)
Audience + copy
/legal/privacy, /legal/music-rights, and the /share/[token]
footer no longer describe SoundAssist as "for mastering engineers
and their clients". Now: "for everyone working with music — artists,
producers, engineers (mixing/mastering/recording), labels, A&R,
and the people they share music with". CLAUDE.md gets a "Doelgroep"
section so future code matches. (5f91e60)
Pipeline robustness
- Waveform renderer survives pro-DAW edge cases — Pro Tools /
Sony WAVs with JUNK/fact/LIST chunks before
data, WAVE_FORMAT_ EXTENSIBLE multi-channel, 8-bit unsigned PCM, 64-bit float, NaN-poisoned float samples, truncated-size-claim files — all parse correctly now. The SVG renderer guards against empty arrays- NaN values that would otherwise crash with NaN attribute values. 18 new unit tests on synthetic WAV buffers. (c7fea3c)
- Audio-analyze retry counter — backfill skips tracks that have failed 3 times so permanently-broken masters don't burn ~30s per attempt forever. Migration: 20260606_audio_analyze_attempts.sql. (0577d0e + 3978987)
Error-message UX (the big one)
lib/user-error.ts — central friendlyError(err, context?)
classifier. Maps any thrown value to 9 kinds (network / auth /
forbidden / not_found / validation / conflict / rate_limited /
server / unknown) with plain-language title + action sentence.
Context label tailors copy ("delete-track" → "Couldn't remove that").
Anti-enumeration is part of the contract: never echo account- existence / per-account-rate-limit text from Supabase.
Adoption: 270+ legacy toast.error(err instanceof Error ? err.message : ...) sites converted to the helper across 5 commits. Audit script
scripts/audit-user-errors.mjs flipped from informational (exit 0)
to hard CI gate (exit 1 on any new offender). New CI job
user-errors in .github/workflows/security.yml. (f1930b0 →
2cdede8)
Empty states + dashboard polish
5 high-traffic empty states converted from hand-rolled "No items"
divs to the canonical <EmptyState> component: NotificationBell,
ChatPanel, PostFeed, group TracksPanel, inbox conversation list +
thread view. Each gets icon + title + context-aware description.
(900a460)
/admin landing page gets a Canary card (Radar icon, cyan). The
existing cards (invite, email-events, email-dead-letter, costs,
email-preview, status) now have a smoke-test sibling. (5f3d857)
Migration
20260606_audio_analyze_attempts.sql — added integer NOT NULL
DEFAULT 0 column on public.tracks + partial index for the backfill
query. Non-destructive — existing rows get attempts=0 and behave
as if they've never been tried.
Audits + gates
npm run audit:user-errors— informational → hard gate (NEW)npm run audit:signout— already a hard gate- Both wired into
.github/workflows/security.yml
Late-pass cleanups (post-dinner curl sweep)
- /changelog was redirecting to /login — same regression class as the /legal/* GDPR fix from earlier today. Public marketing surface missing from proxy PUBLIC_PREFIXES. Canary's second save of the day. (ec4ac3c)
- /hire and /feedback also private — /hire is the warm-sell marketing page, /feedback is the support form including the WhatsApp emergency line. Both now anon-reachable. The canary now monitors 16 surfaces. (1f877e7)
- Mobile polish across /files and 4 admin tables — storage quota bar now visible on phones (was hidden under sm), upload banner flex-wraps so size/speed/ETA chips drop to a 2nd row instead of pushing Cancel off-screen, admin tables get overflow-x-auto + min-w sizing. (d5186cb + 05814ec)
- Login redirect preserves destination — anon users clicking /admin/canary or /workspace now end up at /login?redirect=%2Fadmin%2Fcanary instead of just /login. After signing in they land where they wanted. Was always supposed to work this way; /login form already reads ?redirect= but the proxy was dropping it. (8c36017)
v3.0.2 — Session tool Phase 2 (request/reply protocol) (2026-05-26)
Session-tool-hardening Phase 2.1 + 2.2 shipped. Connect v3.2.0 released to R2 the same day. End-to-end verified by Andre via broadcast + stop cycle in /room with Connect 3.2.0 installed.
Web side (commit 3760e68)
useStudioAudioReceiver.sendCommand(type, opts?) — new helper that
wraps sendData with request/reply correlation:
- Generates a v4 uuid
reqIdper command, attaches it to the outbound payload - Maintains a per-connection pending-requests map; resolves the returned promise when Connect echoes back a matching ack
- 1-second timeout per attempt, 3 attempts total (max 3s wall time; vs the old blind 9s scheduleRetry loop)
- Snapshot-inference fallback: if no ack arrives within timeout but the most-recent <5s session-state snapshot shows the expected post-command state, synthesise an ack and resolve. Keeps the contract working against pre-Phase-2 Connect builds.
- Pending commands rejected with "disconnected" reason on LiveKit disconnect, so callers can distinguish unreachable- Connect from ack-timeout.
/room/[roomId] migrated 4 call sites:
startStream/stopStream— optimistic UI + sendCommand in background, log on failure- Auto-recovery loop (1132-1173) — inner stream-start retries delegate to sendCommand's ack-based retry, escalates to republish after 3 cycles
- "Restart Connect" button — sendCommand with .catch logging
Connect side (v3.2.0)
useLiveKit.sendAck(reqId, ok, opts?) — emits ack back on the
LiveKit data channel after each command's FSM transition completes.
Wired into all 6 inbound command handlers (stream-start, stream-
stop, ptt-mute, ptt-unmute, republish, restart-connect).
restart-connect: emits ack first, then queueMicrotask, then
relaunch(). Replaces the v3.1.x setTimeout(..., 250) blind
delay — no more magic number waiting for the ack to flush.
session-state snapshots now include protocolVersion: 2 so web
can detect Phase 2-capable Connect builds and skip the inference
fallback.
Backward compat
- Old web (no reqId): Connect handlers skip sendAck, behave as before. Verified by running new Connect against the still-cached old web build at deploy time.
- Old Connect (no ack emit): web's pending-map times out at 1s, then falls back to snapshot inference. Verified during the ~5-minute window between PR 1 web ship and Connect 3.2.0 release.
Tests
All 24 broadcast-session.test.ts pass unchanged (FSM untouched). Web-side sendCommand unit tests deferred to a follow-up — manual smoke test by Andre in /room confirmed full flow works.
Spec
docs/architecture/session-data-channel-protocol.md captures the
design, decisions (Andre 2026-05-26), and migration order.
v3.0.1 — Security hardening + ops tooling (2026-05-26)
Defensive engineering sprint while pre-revenue. No new user-facing features; mostly invisible upgrades that catch a class of bugs or attacks before they happen.
Public /status page (P13.7)
Customer-readable uptime page at /status. Server component fetches
/api/health/env + /api/health/email and renders verdicts for 3
services (Web app + CDN, Environment variables, Email pipeline)
with a colour-coded overall pill. revalidate=60, public route,
robots index+follow - the whole point is customers can reach it when
auth is what's down. Self-built free alternative to BetterStack's
Status Pages tier (which is billable). Subscription instructions
point to info@soundassist.online manual signup until self-serve
lands. Commits 8bf93ee + 8c95c27.
Spending discipline (CLAUDE.md)
New standing rule codified: pay for nothing until paying users
exist. Self-hosted free-tier alternatives win over paid SaaS
features. Caught Resend's "Pro plan required for 2nd domain"
($20/mo) before we triggered it. Commit 8bf93ee.
Image optimisation (P12.3)
6 raw <img> tags converted to next/Image across 4 hot-path
files (/p/[username], /files, group track picker,
SessionFolderPanel). Vercel optimiser now serves AVIF/WebP variants
sized per render dimension instead of raw 4MB originals. Blob/data
URL previews in upload flows stay as raw <img> because the
optimiser can't read ephemeral URLs. Commits bf145b4 + 1506ee0.
Audio file content verification (P2.7-wire)
lib/audio-sniff.ts was lib-only since the overnight run; today
it's wired into 3 upload routes: profile-tracks/upload-complete,
f/[token]/upload-complete, r2/attachment-complete. Each route
range-reads the first 256 bytes from R2 after the upload completes
and matches the magic-byte signature against the declared MIME. On
mismatch: deleteObject + reject with 400. On R2 fail-closed:
delete + 500, ask user to retry. Catches .exe files declared as
audio/wav. Smoke-tested on prod by Andre - real WAV upload still
works, no false positives. Commit 83c3a72.
Idempotency-Key (P2.9-wire)
withIdempotency() middleware wired into 3 mutating routes:
POST /api/posts (scope posts.create), POST /api/posts/[id]/comments
(scope comments.create:<post-id>), POST /api/admin/invite
(scope admin.invite). Clients sending Idempotency-Key get the
cached 2xx response on retry within 5 min instead of double-creating
the resource. Clients NOT sending the header behave exactly as
before (opt-in, no breaking change). Commit 608ad1b.
Email rendering audit + test-send UI (P18.20)
/api/dev/email-preview got a "Send to inbox" UI: type a test
email, click "Send all 6 to this inbox", lib/email.ts sends the
templates via the real Resend pipeline so you can eyeball rendering
in actual mailbox clients. Subject prefixed with [TEST] so they
can't be confused with genuine flows. Host-gated, CSRF-protected,
separate rate-limit bucket. Used to verify all 6 auth templates
render correctly in Gmail. Commit 43f964e.
Misc
- BetterStack uptime monitoring marked done - Andre's account was already configured (P13.2).
- DMARC reports in manual-mode marked done - aggregate reports
arrive in
info@soundassist.online; automated XML ingestion deferred until volume justifies it (P18.2). /statuswhitelisted inproxy.tsPUBLIC_PREFIXES so anon visitors reach it when auth is down (same fix pattern as the/trendingbug from 2026-05-21).
v3.0.0 — Platform shift: relationships + shared folders + DJ tooling (2026-05-21)
Major version bump because the underlying model changes. Goes from "mastering engineer + clients" to "a network of relationships (clients, peers, collaborators) with shared folders for any kind of collaboration". And bolts on the DJ-DJ b2b prep loop on top of the existing LiveKit session view.
Database (5 migrations)
relationships(the follow-graph table) →follows. Frees the name for the renamed roster table. 1:1 rename, indexes + FK constraints + policies renamed alongside. Commit30d6911.clients→relationships+ newrolecolumn (client | peer | collaborator, defaultclient). FK constraints on bookings / sessions / service_orders preserve the cascade. Commit30d6911.folder_members+shared_folder_invitestables. Composite PK on (folder_id, user_id) for members; XOR check on invites (existing-user vs email-token). RLS via SECURITY DEFINERis_folder_member()to avoid policy recursion (caught the hard way mid-deploy). Commits30d6911+3a18f1d.bpm numeric(5,2),musical_key text,key_camelot texton tracks. Sharps-only key notation. CHECK constraints on all three. Commit30d6911.tracksadded to thesupabase_realtimepublication so /room SessionFolderPanel receives INSERT + UPDATE events live.
BPM + key detection — in-house DSP, MIT-licensed
Krumhansl-Schmuckler key profile (1990 paper, public domain) + onset envelope autocorrelation with fractional-lag interpolation for BPM. fft.js for the FFT, no AGPL/GPL libraries. 23 unit tests on synthetic Gaussian-pulse signals (90, 120, 128, 175 BPM all within ±2 BPM, all 24 keys round-trip cleanly).
Runs in lib/audio-analyze.ts::analyzeFromR2Key(), hooked into
every audio upload path:
/api/profile-tracks/upload-complete— My Tracks/api/r2/attachment-complete— My Files + Connect drag-drop/api/f/[token]/upload-complete— public shared folder/api/upload/[token]— public-feedback link
Plus admin-gated /api/admin/backfill-audio-analyze for the
historical catalog. Commits fc53ac4 + 4d2c0d8 (analyzer
everywhere bundled with email-suppression work).
Shared folder backend + API
POST /api/folders/[id]/members— owner adds a member by user id (instant-add)DELETE /api/folders/[id]/members/[userId]— owner removes someone OR member leaves themselvesPOST /api/folders/[id]/invites— email-token invite, sends email via Resend with the inviter + folder nameDELETE /api/folders/[id]/invites/[inviteId]— cancel pendingGET /api/invites/[token]— public preview (inviter + folder)POST /api/invites/[token]/accept— authed accept, links user to folder_members, fires activity stream event
Activity stream extended with shared_folder_member_added,
shared_folder_member_removed, shared_folder_invite_received.
FeedDropdown renders them. Commits 30d6911 + 0d844e3 + f6bc683.
Web UI
/clientspage renamed in copy to "My Relationships" + role tabs (All / Clients / Peers / Collaborators). Each card has a click-to-cycle role badge (CLIENT green / PEER purple / COLLAB amber). Commitsc305b01+dd073b7+f417fd7.- Dashboard tracks rows +
/listen/<token>+/p/<username>all show a purple BPM·key·Camelot pill ('140 · Am · 8A') when the analyzer has run. Tooltip carries the full info. Commits4e473c8+65e079f+1a78e8f. - Click-to-filter on dashboard pill: GitMerge icon next to the pill
filters the list to harmonically + tempo-compatible tracks
(±1 / mode-swap / +7 Camelot, ±6% BPM). Commit
3011ba5. /filesaudio rows show the same BPM/key pill (3f19196)./filesfolders: purple dot for shared-with-me + member-count chip for folders I own + 'Leave' action on shared folders (f6008f3+5a18d38+f209331).- Share-folder modal (
components/files/ShareFolderModal.tsx) with email invite, copy-link, member list, remove member. Commit0d844e3. /invites/[token]public landing page — preview card with inviter avatar + folder name + Accept / Sign-up / Log-in CTAs. Commit0d844e3.
DJ-DJ b2b prep — SessionFolderPanel (Phase 06)
Inside /room/<roomId> a "Setlist" toggle opens a right-rail panel:
- Pick a folder (eigen or gedeelde)
- Track list with BPM · key · Camelot pill per row
- Drag-drop audio onto the panel → upload + analyzer fires server-side
- Realtime INSERT + UPDATE so DJ B sees DJ A's uploads within ~200ms + the BPM pill appears live ~15s later
- In-session preview cue (no new-tab disruption of LiveKit audio)
- Member-presence avatars in the header (who else is in the same folder right now)
- "Use this" mark per track, shared between members via Realtime broadcast — DJ A taps, DJ B's row highlights instantly
All ephemeral collaboration state via one supabase.channel per
folder — no DB schema for marks/presence. Commits cea0944 +
9040720 + 0a22777 + df6aea8.
Hygiene
- Pre-commit hook now runs
biome check .before typecheck + tests, so lint-fail-on-CI is impossible. Commit2af01ad. - Proxy whitelist for
/invites/+/api/invites/(caught by smoke-test). Commit1e1414d.
Unreleased — Platform maturity + social differentiators (2026-05-20)
Two-day sprint focused on turning a feature-rich MVP into a platform that doesn't fall over. 30+ commits across performance, security, code hygiene, and four out-of-the-box features that make the shared-link experience match the engineering quality of the codebase. Net result: 31 of 45 roadmap items shipped, productive gates green, build pipeline + schema-drift guard + structured logging all wired up.
Public user-facing features
- Posts edit + delete for both posts and comments. Inline
textarea + Save/Cancel, server PATCH author-only. Commit
a61939c. - Track reposts: any signed-in viewer can repost a track from
a group or another profile onto their own profile feed. Pure
share button, optional commentary. Schema column
posts.reposted_track_idenforced via XOR CHECK withreposted_from_post_id. Commit7284098. - Collaborator credits with @-mentions. Track upload forms
for the three credit roles (engineer / producer / label) gained
autocomplete: type
@andrand pick from matching profiles. Both the display text and the resolveduser_idare stored; display layer (CreditLine) renders @handles as clickable links. New "Credited" tab on every profile lists tracks where this user was tagged by anyone. Engineers + labels now have a geverified portfolio that builds itself. Commitf6b3723. - Dynamic Open Graph + Twitter card images for
/listenand/p. Every shared link now renders a proper 1200×630 hero card (artwork + title + artist for tracks; banner + avatar + role for profiles) instead of a grey placeholder. Built via Next.jsopengraph-image.tsx/twitter-image.tsxconventions, served at request time, edge-cached. Commits5f57a60,a45f62c. /trendingdiscovery page. Top 20 tracks of the past 7 days ranked by weighted score (likes + 3×comments + 5×reposts). Hero card for #1, dense list for 2-20. Anonymous-friendly so shared links work as landing pages. Dashboard widget now links to the full ranking. Commitffe41ee.- Embed widget at
/embed/<token>+ oEmbed endpoint at/api/oembed. Tracks pasted into WordPress / Medium / Notion / Discord auto-expand to an inline 180px-tall player. Each embed carries a SoundAssist wordmark + "Open" link as gratis reklame on every paste. Per-route CSP override allows the iframe to render anywhere. Commits75e60a0,a45f62c.
Performance
- Bearer-token cache: 60s in-memory SHA-256 cache on
getUserId(). 30-100ms saved per repeat call. Commit00fa7ce. - Post-feed query bundling: GET /api/posts collapsed three
separate aggregation queries (likes / comments / reposts) into
one
get_posts_with_stats()RPC using LATERAL subqueries. 4 round-trips → 2. Commit4154cfa. - Edge caching with
s-maxage+stale-while-revalidateon/api/profiles/[username],/api/listen/[token],/api/posts,/api/groups/[slug]/tracks. New helpers inlib/cache-headers.tskeep the policy uniform; viewer-aware variant returnsprivate, no-storewhen a Bearer token is present so per-user state never lands in a shared cache. Commitb0f1f84. - Bulk LikeButton fetch: a profile page with N tracks no
longer fires N parallel
GET /api/track-likes/[token]calls. NewTrackLikesProviderwraps the list, fires one bulk call via/api/track-likes/bulk, hands the results to each LikeButton via context. Commitd706b04. - AdaptiveImage via Next/Image with explicit
remotePatternsfor our CDN. AVIF/WebP serving, srcset, lazy-load default,priorityopt-in on hero banners. 4MB banner → ~80KB AVIF on mobile. Commit9f96a25. - Listen page seeded from the listen payload so LikeButton
doesn't fire its own GET. Plus the listen fetch now passes
the Bearer header so
viewer_likedis correct for authed users. 3 network trips → 1. Commit8c64839. - BRIN index on
activities.created_atfor the append-only event stream. Commit6dc66d1. - Bundle audit:
optimizePackageImportsforlucide-react@supabase/*so barrel-named imports get rewritten to deep paths at compile time. Commit4c808d3.
Security
- Rate limiting on every social write endpoint via the
existing
lib/rate-limithelper. Per-user buckets, 60s windows. Posts 10/min, comments 30/min, likes 60/min, reposts 10/min. Commitcad261c. - Magic-byte validation on server-side image uploads. New
sniffImageMime()/verifyImageBytes()inlib/r2.tsreads the first 12 bytes and rejects anything where the declared Content-Type doesn't match. Wired into the 4 multipart upload routes. Commitf08fcaa. - CSP tightening (then reverted): tried an explicit allowlist
for img/media/connect-src; broke audio playback because of a
hostname mismatch. Reverted in
a457830. Re-do viaContent-Security-Policy-Report-Onlyis tracked as a follow-up. - XSS audit: zero
dangerouslySetInnerHTMLin the codebase, zero.innerHTMLassignments in app code. Confirmed via grep- Biome lint config. Commit
2d5213f.
- Biome lint config. Commit
- Secret-leak defense: new
redactForLog()helper inlib/redact.tsscrubs known secrets (SUPABASE_SERVICE_ROLE_KEY, STRIPE_*, R2_*, OPENAI_API_KEY, etc) from anything passed through it. Belt-and-suspenders for the existing console.error audit. Commitcbb5dd6. requireSameOriginaudit: applied uniformly to private user-mutating routes (profile, profiles/[username] PATCH, messages, clients, workspace, stripe portal, send-session-email). Belt-and-suspenders since Bearer auth already prevents CSRF. Commit485779b.- npm audit fix: ws 8.20.0 → 8.20.1 (uninitialized memory
disclosure, GHSA-58qx-3vcg-4xpx). Commit
93e76ed. - Admin audit log for moderation actions. New
admin_audittable +recordAdminAction()helper wired into the group member-mutation routes (kick, ban, role-change, ownership transfer). Internal forensic trail; service-role only, client-invisible. Commit799e833.
Code & architecture
- withAuth() route wrapper in
lib/api-helpers.tsbundlesrequireSameOrigin+requireUserId+ optional rate-limit into one composable. Demonstrated on the post-like route. Future routes default to this. Commitda4f789. - knip dead-code sweep: removed 250 lines of dead code
including 7 legacy
client_fileshelpers, anisUuidShapere-export, and three unused types.npx knipnow exits 0 cleanly. Commitf83f9b9. - Schema-drift guard at pre-commit:
scripts/check-db-types.mjsdiffs the generatedlib/database.types.tsagainst the live Supabase schema; husky runs it whenever a commit touches a migration or the types file. Bypass viaSKIP_DB_TYPES_CHECK=1. Commit7f299ab. - Shared response types in
types/api/posts.ts. Both server routes and client components now import the samePost,PostAuthor,RepostedFrom,RepostedTrack,PostCommentdefinitions. No more silent drift. Commite481115. - Uniform error response helpers in
lib/api-errors.ts:apiError,unauthorized,forbidden,notFound,invalidInput,conflict,unsupportedMedia,rateLimited,serverError. Every route returns the same{ error, message?, code? }shape. Commit76e6465. PostFeed.tsxsplit from 984 lines into 7 focused files undercomponents/posts/. Same behaviour, much shorter per file. Commitbd6b84b.- Structured logger at
lib/log.ts. JSON output to stdout/stderr with secret redaction baked in. Opt-in for new code; oldconsole.errorcalls stay unchanged. Commit807a12b.
Developer experience
npm run checkcombo runs typecheck + tests + lint + knip in one command. Commit1e33f84.- Routes index at
docs/api.md, auto-generated bynpm run docs:routesfrom theapp/api/**/route.tstree. 86 routes catalogued. Commits318c5c6,aff1ff7. - ARCHITECTURE.md brought current with the social pivot:
data model, polymorphic-owner pattern, share_token model,
two-tier group membership, RLS-recursion fix, activity stream,
R2 layout, encoding pipeline, glossary. Commit
3eff2a6. - CLAUDE.md project patterns section: how to write a new
route, error helpers, RLS gating, migration conventions,
test patterns, image uploads, and (post-mortem) Next.js
segment-config files. Commits
f168771,aff1ff7.
Internals
Migrations applied (Supabase production):
20260520_posts.sql- polymorphic posts table20260520_post_interactions.sql- likes, comments, reposts20260520_track_reposts.sql-posts.reposted_track_id20260520_posts_stats_function.sql- aggregation RPC20260520_activities_brin_index.sql- BRIN on created_at20260520_admin_audit.sql- moderation forensic log20260520_credit_user_ids.sql- resolved credit FKs
104 unit tests pass (up from 53). New suites:
api-auth,api-helpers,api-errors,cache-headers,log,r2-sniff,redact.
Known follow-ups (parked, not lost)
- CSP redo via
Content-Security-Policy-Report-Onlyto gather real-world violation telemetry before re-enforcing the strict allowlist - Account lockout (needs Upstash KV or similar shared store)
- RLS test matrix with two test accounts
/tracks/newstate-machine refactor (1000+ line file)- E2E Playwright suite (needs Stripe test mode + seed accounts)
Unreleased — Design polish + mini-player + Groups roadmap (2026-05-19)
15 commits in a single working day, almost all about pulling the public profile + dashboard out of "feels like an MVP" into "feels like a real product". Net result: brand reads as Bandcamp-for- mastering-engineers on every viewport, persistent audio player across the profile page, plan for Groups + Livefeed locked in.
Design / visual identity
- Tall artwork tile on every track row — public profile + dashboard
both get a 96px → 140px (mobile → desktop) square cover-art tile
spanning the full row height instead of a 40x40 thumbnail next to
the play button. Clicking opens a lightbox at up to 90vw/90vh;
empty tiles trigger the upload picker on the dashboard. Spotify /
Apple Music album-art pattern. Commits:
c9f01db,e190048. - Square avatar tile on the public profile banner — same pattern
as the track artwork. Desktop: 160-180px vierkant left edge. Mobile:
16:9 banner across the top (down from 1:1 full-viewport hero which
was eating the whole above-the-fold). Click opens existing
AvatarLightbox. Commits:
ca2bbbb,3605536. - 2-row track headers — title was getting truncated on every
viewport by the inline play-button + 6 badges + actions sharing
one flex row. Restructured to title-on-its-own-row (line-clamp-2)
- actions-on-row-2. Studio Master badge (24-bit / 44.1 kHz) sits
next to title on desktop, hops to row 2 on mobile so title gets
full right-column width. Commit:
d7caa88, mobile fix in3605536.
- actions-on-row-2. Studio Master badge (24-bit / 44.1 kHz) sits
next to title on desktop, hops to row 2 on mobile so title gets
full right-column width. Commit:
- Profile widened to max-w-4xl (896px) from max-w-2xl (672px).
On a 1920px monitor the page was reading as a narrow column
floating in a dark void. Deliberately narrower than SoundCloud /
Spotify (~1200px) — "intimate reading width" reinforces the
lossless / mastering positioning. Commit:
a673f60. - Responsive padding + touch targets —
px-4 sm:px-6 lg:px-8ladder on both profile and dashboard. Play button h-10 on mobile (close to Apple HIG 44px) shrinking to h-8 on sm+ for mouse density. Commit:fc5cc13.
Audio playback
- Persistent bottom mini-player on the public profile — Spotify-
style strip with artwork + title + prev / play-pause / next +
click-to-seek progress bar. Stays visible while scrolling. Reads
state from the active TrackRow's WaveSurfer instance via a ref-map
- a small set of new callbacks; deliberately not a global audio
context refactor. Cross-route persistence (so audio keeps playing
when navigating to /listen or /tracks/new) is a fase-2 item.
Commit:
a630592.
- a small set of new callbacks; deliberately not a global audio
context refactor. Cross-route persistence (so audio keeps playing
when navigating to /listen or /tracks/new) is a fase-2 item.
Commit:
Navigation
- Livefeed dropdown next to the bell in both desktop nav and
profile owner nav, plus mobile dashboard inline bar. Companion to
notifications: ambient stream of uploads from people I follow
(different mental model than direct-mention notifications). The
backend (
/api/activities/feed, activities table,recordActivity()) was already in place from earlier work — this just exposed it as a top-nav surface. Commits:a614dcf,8af1dbe. - NotificationBell mounted on the profile owner nav — bell + dropdown +
polling all worked from the dashboard nav since earlier commits,
was just never wired into the profile page. Added a
variantprop to the component so icon-mode matches the round buttons there. Commit:b0e0ff3. - Followers / following counters clickable at 0 —
FollowListModalalready had an empty-state ("No followers yet"); the trigger button was disabled at 0 so brand-new accounts couldn't even see the modal. Now only disabled while counts are loading. Commit:3e8ace2.
Brand positioning
- Sessions widget gated behind internal-user flag — "Create or
join session" + "Got a room code?" + "Stream audio from your DAW"
blocks now only render for the founding team (hardcoded email
allowlist in
lib/internal-users.ts). Reason: the macOS Connect build doesn't exist yet, and a Windows-only download CTA on a Bandcamp-style platform reads as use-case confusion. Routes (/room/[id],/download) still work; the pairing flow is untouched. Commit:49bd944. - Upload button routes to
/tracks/new— the public profile Upload button was opening a legacy inline file picker (ProfileTrackUpload component), which skipped the title / genre / description / artwork editing step that the dedicated upload page introduces. Tracks ended up in My Tracks with no metadata, silently. One door into upload now:/tracks/new. Deleted the ProfileTrackUpload component (374 lines of dead code). Commit:accf0ac.
Mobile fixes
- Avatar hero on mobile was 1:1 full-viewport (~360x360) → all
content under the fold. Now
aspect-video(16:9 ~200px tall) on mobile,aspect-square160-180px on sm+. Commit:3605536. - Studio Master badge demoted on mobile — was crushing the title
in row 1, now appears in row 2 next to the play button. Title gets
full ~240px right-column width on 360px viewports. Commit:
3605536. - Feed + Bell icons added to mobile inline nav — only Inbox was
there before, no way to reach feed/notifications without opening
the drawer. Commit:
8af1dbe.
Documentation
- GROUPS-ROADMAP.html — standalone visual roadmap doc for the
Groups + Livefeed multi-week project. 10 anchor decisions locked
(no auto-include, multi-admin promotable, separate groups table,
three join policies, etc.), 7 phases sketched with acceptance
criteria, schema preview embedded. Dark theme matched to brand;
status badges (Planned / Doing / Done) updated as fases complete.
Commit:
9b41fc5.
Brand
- New logo with "Your sound. We assist." slogan — Andre redesigned
the mark as a single composition (wordmark + black slogan block).
SVG primary asset (39 KB, scales without quality loss), generated
PNG fallback (24 KB, 1024x768) for emails / OG cards / Apple touch
icon.
filter: invert(1)works identically on both formats so the dark-website inversion stays unchanged. Logo component size map updated from 2.3:1 to 4:3 ratio. Commit:1297dc8.
Cleanup
- Removed dead Opus 510 encode pipeline —
encodeOpus510(),encodeLocalToOpusAndUpload(),EncodeMetainterface,opus510Key()helper. All unused since the FLAC streaming switch (2026-05-18). Read-side fallback for legacy tracks withopus_r2_keystays in place. Knip post-cleanup: 0 unused exports. Commit:d98a800.
Deferred to next sessions
- Groups foundation (fase 1 of the new roadmap) — schema migration, /groups/new form, /g/[slug] page, first join-request flow.
- Container queries on TrackRow so it reflows by container width rather than viewport breakpoints (needed when the row gets embedded in narrower sidebars / modals).
- Mini-player fase 2: global audio context, shuffle / repeat / volume, cross-route audio persistence.
Unreleased — Principal Engineer roadmap, Phase 1-5 (2026-05-18)
Marathon hardening pass through phases 1 to 5 of the master roadmap. 78 of 124 items resolved in a single session, plus a silently broken Vercel-GitHub webhook on the marketing site discovered and fixed (4 commits had been stranded on origin without deploying for ~24h).
Security
- 2FA enforced on all six operational platform accounts: GitHub, Vercel, Supabase, Stripe, Cloudflare, Resend. Recovery codes stored locally. Google Authenticator backed up to Google account cloud sync.
- Rate limit on
/api/transcribe(Whisper costs real money). 20 calls/user/hour + 40 calls/IP/hour. Combined with the OpenAI org-level $25/month budget cap, max realistic exposure for a compromised account is now ~3-4h before the org cap kicks in. /api/public-feedback+/api/upload/[token]whitelisted in proxy.ts. Both routes are documented as anonymous landing-page endpoints, but neither was inPUBLIC_API_PREFIXES. The proxy returned 401 to cross-origin POSTs from soundassist.online, silently breaking the "Get free feedback" CTA in production. Found during the Phase 3.12 functional check.SECURITY.mdwritten. Per-secret rotation playbook for all 9 credential classes. Vulnerability reporting flow. Defence-in- depth notes. Last-reviewed date so reviewers can spot staleness.
Added
- Typed Supabase Database end-to-end.
lib/database.types.tsgenerated viasupabase gen types typescript. All clients now parameterised with<Database>. Surfaced 7 latent bugs on first typecheck (nullable date fallbacks; narrowed update payloads; runtime Array.isArray guard forwaveform_peaks; explicitJsoncast in activities). Newdb:typesnpm script regenerates after every migration. - Sentry error tracking wired via the official wizard with
GDPR/quota-friendly defaults: tracesSampleRate 0.1, sendDefaultPii
false, replays only on error, env-var DSN so the SDK is a no-op
without configuration. Tunnel route at
/monitoringbypasses ad-blockers. Verified end-to-end. - Per-user subscription + profile cache in
proxy.ts. 30-second TTL Map keyed by userId, eliminates 1-2 Supabase round-trips per authenticated page nav. Stripe webhook invalidates on every subscription mutation. 7 new tests inlib/proxy-cache.test.ts. - Mobile hamburger nav on the host dashboard. Desktop layout overflowed on phones — "Sign out" wasn't even visible. Slide-in drawer from the right (80% width, 48px tap targets, body scroll-lock when open). Profile card stacks differently per breakpoint to stop role/country chips colliding with edit button.
- prefers-reduced-motion respected globally via
globals.css. - apple-touch-icon link so iOS "Add to Home Screen" produces a real icon instead of a screenshot.
- viewport-fit cover for edge-to-edge rendering on notched iPhones. Combined with the dvh refactor (63 sites in 30 files), no more 100vh URL-bar jitter.
- Vercel Analytics on the marketing site via
<script defer>in all 14 live HTML pages (the static site can't use the npm package). App side switched to@vercel/analytics/next. - WebP variants on the marketing
/studioand/platformpages via<picture>+<source>with JPG/PNG fallback. 190 KB saved per visitor. - Privacy Policy at /privacy.html — GDPR-aligned, all 10 sub-processors listed, legal basis per purpose, retention table, Art. 15-22 rights enumerated.
- Terms of Service at /terms.html — Dutch consumer law-aligned. Refund policy (Art. 16(c) CRD), IP rights, subscription cancel, liability cap, Dutch jurisdiction, EU ODR pointer.
RUNBOOK.md— 9 incident playbooks (site down, DB down, R2 down, Stripe webhook fail, email fail, leaked secret, data loss, mass spam, plus one-liners). Optimised for 3am readability.CONTRIBUTING.md— first-time setup, command reference, codebase tour, conventions, PR process.docs/R2_BACKUP.md— three-tier backup strategy with the "stay at Tier 1 until €1k MRR or 50 GB stored" trigger + rclone recipe..env.example— all 20 env vars in use, grouped by service.- Lint step in GitHub Actions CI (
npm run lint). CI gate for anyone who bypassed the pre-commit hook.
Fixed
- Landing CTA broken in production (proxy whitelist — see Security above).
- Vercel-GitHub webhook silently disconnected on marketing repo. 3 commits stranded for ~24h. Settings → Git → reconnect restored auto-deploys; backlog drained on first push afterwards.
min-h-screen→min-h-dvhacross 63 occurrences (30 files). iOS Safari's 100vh counted the URL bar; dvh resizes smoothly.
Performance
- TTFB baseline + post-cache measured. Hot path on dashboard saves 30-100 ms by skipping Supabase round-trip.
- Cloudflare Cache Rule "Cache R2 audio aggressively" on cdn.soundassist.online. Edge + browser TTL 1 year.
Monitoring
- BetterStack uptime monitors on
/api/health+ marketing home. 3-min interval, email alerts. - Stripe failed-payment notifications verified on.
- Vercel deployment failure notifications verified on.
- Vercel rollback drill completed. Instant Rollback works in ~10 seconds.
Infrastructure
- OpenAI usage cap at $25/month with 80% + 100% email alerts.
- Vercel Sentry integration sets SENTRY_AUTH_TOKEN/ORG/PROJECT at build time so source maps upload during deploy.
- Supabase 8-day rolling daily backups confirmed. PITR add-on explicitly skipped — not justifiable below €5k MRR.
- Supabase connection pooling verified: Transaction mode, pool size 15, max 200 clients.
.gitignorenoise removed (supabase/.temp/, profile.png, release-concept.html). Knip clean of the supabase ignoreDependencies hint.
Unreleased — community-flywheel hardening pass (2026-05-15)
Cross-model diagnostic + security audit followup. Four parallel review agents (security, code review, DB/RLS, frontend) ran against the Phase 3-6 diff. Cross-confirmed findings fixed. No CRITICAL items. Most-impactful fixes: identity binding on comments, RLS tightening on track_likes, CSRF + rate limits on relationships + likes.
Security
- Comments: server overwrites author_name + author_role from the Bearer-resolved profile. The client-side nameLocked guard was bypassable via curl — anyone with a Bearer could post as anyone else.
- Likes: self-likes no longer record an activity event (confusing "Andre liked Andre's track" rendering). 30/min per-user rate limit.
- Follows:
/api/relationshipsPOST/DELETE now require same-origin (other community endpoints already did) + 20/min per-user rate limit. Explicit JSON parse try/catch (was raising 500 on malformed bodies). - Comment threading:
parent_idstrict validation. Must be a UUID, must reference a comment on the SAME track, must NOT itself be a reply (one-level cap now enforced server-side too). - CSRF whitelist: dropped the wildcard
*.vercel.apptrust path. attacker-csrf.vercel.app was previously accepted as same-origin. Whitelisted onlyprocess.env.VERCEL_URLfor the actual deployment. track_likesRLS: tightened fromUSING (true)to "you OR the track is public/voice-message". The wide-open policy let anon clients dump the like graph for Unlisted tracks. Migration is idempotent — re-run20260515_track_likes.sqlin Supabase to apply./api/profile-activitygates on the target profile'sis_public. Anyone with a UUID could previously enumerate a private user's uploads/likes/comments.- Error sanitisation: notifications + relationships routes no longer echo raw Postgres errors back to the client (leaked column names, constraint names, RLS policy names). Generic strings client-side, full error server-side via console.error.
getUserIdnow matches the Bearer scheme case-insensitively per RFC 7235.
Fixed
- Dashboard encode polling: fresh WAV uploads stayed at "Encoding"
badge forever until hard refresh.
/p/[username]had the 4s poll; the dashboard now has it too. Andre hit this during the Phase 3-6 session. - NotificationBell: each row is a real
<button>, not a<div onClick>. Keyboard Tab + Enter/Space now mark-read. - LikeButton race: synchronous
busyRefblocks rapid double-clicks from racing the optimistic flip.mountedRefskips setState after unmount. - LikeButton anon click: no longer silently no-ops. Surfaces "Sign in to like this track" toast.
- TrendingTracks null host: deleted host profiles no longer render
broken
/p/links. Avatar + name fall through to plain text. - ActivityFeed / NotificationBell empty gaps: rows with null actor are filtered before mapping (rare cascade-delete race window).
- Activity feed truncation: over-fetch + filter so recent events targeting Unlisted tracks don't shrink the visible feed below 30.
Changed
lib/relative-time.tsextracts the relative-time formatter that was inlined in three components with a hard-coded Dutch "u" suffix. Suffix is now "h" (English) so non-Dutch viewers can read it.
Unreleased — community flywheel (2026-05-15)
Peer-to-peer community features land on the dashboard, public profile, and listen pages. Artists can now find each other, follow each other, hear each other's work, and engage. No editorial curation — the whole thing is data-driven so it scales without an Andre-bottleneck.
Added
- Activity feed on the dashboard — events from people you follow
- your own, newest first. Powered by a single
activitiestable (jsonb metadata) with two read queries: feed (actor in followed-set) and notifications (target_user_id = me).
- your own, newest first. Powered by a single
- Notification bell in the top nav with unread badge. Polls every 60s while the tab is visible, pauses while hidden. Mark-all-read and per-item mark-read both supported. Notifications for: new followers, comments on your tracks, likes on your tracks.
- Likes — heart button on
/listen/<token>and inline on every/p/<username>track row. Backed by atrack_likes(user_id, track_id) composite-PK table so a user can't double-like. - Comment threading — Reply button on every comment, one level deep (matches SoundCloud, avoids Reddit-infinity-indent on mobile). Replies inherit the parent's timestamp.
- Signed-in commenter identity — auto-filled author name pulled from the session; comments from logged-in viewers now flow into the activity stream + notify the track owner (were anonymous before).
- Profile "Recent activity" section on
/p/<username>showing uploads, likes, and comments by that artist. Self-hides on empty. - Trending tracks dashboard widget — 7-day window, score = likes + comments, tiebreak newer-first. Ranks by engagement, not editorial.
- Discover artists widget on the dashboard (already shipped in Phase 2, mentioned here for the changelog reader's mental model).
- "Follows you" badge + "Follow back" button on profiles where the viewed user already follows the viewer. Reciprocal-edge nudge.
- Clickable track titles on
/p/<username>→/listen/<token>page. Visitors can engage with a track (comment, like, scrub the waveform) without leaving the profile by hand-typing a URL.
Changed
- Listed / Unlisted replaces Public / Private on track rows. The share_token has always been an unguessable UUID, so the link is the credential — "Unlisted" controls profile-page visibility, not link reachability. An artist sending a fresh mix to one person can now flip the track to Unlisted and the link still works. Matches the YouTube / SoundCloud mental model.
/api/listen/[token]drops theis_publicprivacy gate. Anyone with the share_token gets in; owner-only paths (download forallow_download=falsetracks) are unchanged./api/relationshipsaccepts both{ followed_id }(legacy) and{ targetId }(DiscoverArtists + FollowListModal sent the latter, server only honored the former). Recording a 'follow' activity is gated to genuinely-new follows so re-toggling doesn't spam the feed.- Copy link button on the dashboard now renders for both Listed AND Unlisted tracks.
Fixed
/api/relationshipsPOST/DELETE silently ignoredtargetId— the DiscoverArtists Follow button and the FollowListModal Follow toggle both senttargetIdwhile the server only readfollowed_id. Symptom: clicking Follow in those surfaces appeared to do nothing.
Infrastructure
- Three new Supabase migrations (
20260515_activities.sql,20260515_activity_read_state.sql,20260515_track_likes.sql) with RLS policies + partial indexes for the hot-path queries (unread notifications, likes per track, activity per actor). - New
lib/activities.tshelper centralisesrecordActivity()calls. Errors are logged and swallowed — a broken feed insert never tanks the underlying follow/upload/comment/like action.
Unreleased — beta-readiness sprint (2026-05-06)
Critical fixes (cross-flow audit)
A platform-wide audit surfaced that the wrong-bucket bug we fixed in the track DELETE flow was actually present in 4 more places. Each silently broke a real feature:
/api/f/[token]GET — public folder downloads returned null URLs/api/f/[token]/upload-url— public folder uploads went to a non-existent bucket/api/messages/[userId]— inbox attachments + voice messages had no playable URL/api/profiles/[username]/tracks/[trackId]— public profile track playback returned null signedUrl
All four switched to the correct session-files bucket. Plus
/api/f/[token]/upload-url now uses the canonical tracks/...
prefix so files uploaded into a host's shared folders pass
/api/download's ownership guard.
Reliability
app/room/[roomId]/page.tsxhandleLeave now wrapsupdateSessionin try/catch. If the DB update fails after a successful recording upload, the orphan.webmis removed via the new/api/recordings/cleanupendpoint (same shared-secret model as/api/end-session, refuses paths outsiderecordings/prefix or that don't match the canonical session form)./api/f/[token]/upload-completenow removes the storage object on either failure path (link expired between upload-url and complete, or DB insert fails). Mirrors the cleanup we did for/api/upload/[token]in inbox1./api/folders/[id]/sharere-selects the canonical share_token after the write. Prevents two near-simultaneous share clicks from each returning a different token while only one wins in the DB.
Polish
middleware.ts→proxy.ts(Next 16 deprecated the middleware filename; same handler, new nameproxy). Build no longer emits the deprecation warning.- Feedback link in the host-dashboard nav.
mailto:toinfo@soundassist.onlinewith a pre-filled body (browser / device prompt + signed-in user's email). Hardcoded — one-person team does not need a separate feedback inbox, and a forgotten env var would silently break the link.
Known issues (deferred)
These were surfaced by the cross-flow audit but deliberately not fixed in this sprint:
- C5: Track upload orphan window when the browser closes
between the storage PUT and
createTrack. Slow leak; the storage-audit script catches it. A proper server-side/api/tracks/upload-completeendpoint would close the gap. - M1:
tracks.is_public = falsedoes not invalidate the existingshare_token. The/listen/[token]URL keeps working forever. Probably intentional (share links are permanent-by-design) but the asymmetry will surprise some users. UX decision pending. - M2: Deleting a client cascade-deletes its sessions but
the
recording_pathfiles in storage and anyclient_files.storage_pathblobs are NOT removed. Build a Postgres trigger or a server-sidedeleteClientthat walks both before deleting the row. - M3:
client_files,upload_links,client_file_foldersexist in code but not insupabase/migrations/. Cannot verify their FK + cascade behaviour. Add migration files next time someone touches the schema. - M5: Double-delete in two browser tabs returns 404 on the second one — verify the My Tracks UI swallows that quietly rather than throwing a scary toast.
- M6: When a track is deleted, messages whose
attachment_track_idpointed at it lose their attachment preview text. The thread view should fall back to "Attachment removed" instead of an empty bubble.
Hygiene + tooling
.editorconfig+.gitattributesenforce UTF-8-no-BOM and LF line endings across all source files. Stops the BOM-regression cycle that bitapp/page.tsxrepeatedly during the inbox1 audit.- Husky
pre-commithook runs typecheck + tests before every commit. Bypassable with--no-verifyfor emergencies. app/api/healthendpoint for liveness + dependency checks. Public, returns 200/503 with per-dependency status JSON.- Branded
app/not-found.tsxandapp/error.tsxreplace the default Next.js error pages. app/robots.ts+app/sitemap.ts: indexable for landing, pricing, download, login, public profiles. Disallow API, authenticated, and any token-bearing URLs.
inbox1 — 2026-05-05
The full inbox + network release. From this point forward SoundAssist has DMs with attachments + voice, follow/unfollow, verified credits, and a hardened security surface.
Added
- Inbox attachments (be006e8): drag a track from My Tracks / My Files into a DM. Inline audio player on receive.
- Voice messages (be006e8): in-composer recorder. Voice
msgs are tracks with
is_voice_message=trueso they don't pollute My Tracks. Compact playback widget in the thread. - Verified credits (6ab18da): Mixed by / Mastered by / Produced by, with mutual confirmation. Either party can claim; only the counterparty can confirm/decline.
- Pending-credits badge in nav (0556ce6): polls every 30s.
- Read receipts in inbox: ✓ sent, ✓✓ read.
ARCHITECTURE.md(fbda494): single-source map of the system — data model, auth, real-time pipes, security posture.README.md: quick start, conventions, deploy flow.- GitHub Actions CI: typecheck + vitest on push and PR.
scripts/storage-audit.mjs+scripts/storage-cleanup.mjs: read-only audit and (opt-in) destructive cleanup for orphan storage objects.
Changed
/api/messages: now acceptsattachment_track_id. Conversation preview and thread API hydrate attachments with 1h presigned URLs.- Migrations consolidated under
supabase/migrations/:20260101_initial_rooms.sql,20260102_clients_sessions.sql,20260103_session_files.sql(renamed from root-level files). lib/file-utils.ts(split from inline copies in 4 places):AUDIO_EXTS,fileExt,isAudioPath,fmtBytes,fmtDate.FileTypeIconlives incomponents/so the lib stays JSX-free and testable.lib/rate-limit.ts: in-memory sliding-window limiter shared across public upload routes.
Fixed
- Track delete bucket bug (3267cf1):
/api/tracks/[id]DELETE usedstorage.from("tracks")(a non-existent bucket) so storage files were never removed. The silent.catchmasked the error. One user accumulated 62 orphan WAVs. Fixed + cleanup script run. - Inbox redirect to /login (f149d4c): inbox page constructed
its own raw supabase-js client (localStorage) while the rest of
the app uses
@supabase/ssr/createBrowserClient(cookies). Switched inbox to the sharedlib/supabaseclient. - Rate-limit memory leak (CRITICAL) (9bd5b81): the buckets Map grew unboundedly because expired buckets only got cleaned when their own key was touched. Could be exploited to OOM the Vercel function. Fixed with a periodic sweep + 50_000 hard cap.
Security
/api/tracks/upload-url(648ead1): now requires Bearer auth and validatesuserId === auth.uid(). Rejects executable.html+.svgextensions (9bd5b81).
/api/download(648ead1): requires Bearer auth and verifies the caller owns the storage path viatracks/session_files/client_files. Rejects path-traversal (9bd5b81)./api/tracks/[id]/stats(648ead1): only increments for publicly-accessible tracks. 5/min/IP rate limit./api/upload-file-url(648ead1): validates room exists- isn't expired before signing upload URL. 10/min/IP rate limit.
/api/relationships(2713d88): hides profiles whereis_public = falsefrom follow lists./api/folders/[id]/share(2713d88): Origin / Referer guard against CSRF, anchored onNEXT_PUBLIC_APP_URL(not the attacker-controllableHostheader) (9bd5b81)./api/f/[token]/upload-url(2713d88): allowlist of audio / archive / doc / image / video extensions; blocks executables.safeFilenamecollapses..segments./api/upload/[token](2713d88): atomic cleanup — deletes the orphan storage object when the DB insert fails.
UX
- 7 Dutch error/UI strings translated to English (0556ce6).
- 11 em-dashes (—) replaced with plain ASCII hyphens (-) in user-facing copy (0556ce6).
- Two icon-only buttons (rename confirm/cancel, chat send) given
title+aria-labelfor screen readers (1c7a8cf).
Infrastructure
- All Vercel env vars now set on Production AND Preview tiers:
Stripe (4),
NEXT_PUBLIC_APP_URL,NEXT_PUBLIC_RELAY_URL, LiveKit (3), OpenAI, Service Role. Preview deploys now actually work for PR reviews. npm updateapplied patch + minor versions across 9 packages. Major bumps (Next 14→16, React 18→19, Tailwind 3→4, TS 5→6, lucide-react) deferred to the pre-launch sweep.
Pre-history
For commits before inbox1, see git log --before=2026-05-05.
The repo dates back to early 2026; the early period was rapid
prototyping of the live-session core (LiveKit + WebRTC + Connect
.exe) and the per-client archive flows.