All notable changes to MusicianHub. Pre-release; not yet versioned.
Newest first. Dates are when the work landed on main.
Unreleased / in progress
- Rebrand: MusicianHub → QuarterNote (quarternote.ai) — renamed across the app: all UI strings, the split-logo markup (
Quarter/Note), transactional email templates +RESEND_FROMdisplay name, page metadata + OG/Twitter (metadataBase→quarternote.ai),sitemap.ts/robots.ts/llms.txt, legal pages, and marketing/developer pages (~115 replacements across 38 files). Still to do (non-code): verify aquarternote.aisending domain in Resend (emails still send fromonboarding@resend.devuntil then), setNEXT_PUBLIC_APP_URL=https://quarternote.aiin Vercel, and note the GitHub repo + Vercel project + Roushi slug remainmusicianhub. - Malformed-path guard (
src/proxy.ts) — 404s requests with backslashes in the path (e.g./faq%5C) that scanners probe with; these previously slipped into the router and threwMODULE_NOT_FOUNDnoise in the runtime logs. Uses Next 16'sproxyfile convention (the renamedmiddleware). - Document vault (school) (
/school/documents) — secure per-teacher storage for tax/HR paperwork (W-9, 1099, contract, certification, other). Because this is PII, it lives in a separate private R2 bucket (R2_PRIVATE_BUCKET, distinct from the public recap bucket) and is served only via short-lived presigned URLs — objects are never public. Upload is a presigned PUT (school-admin only, PDF/images ≤15MB); the recorded object key is validated to live under the school's ownschool-docs/<orgId>/prefix; downloads go through an access-checked route (/school/documents/[id]) allowing only the school's admins or the owning teacher. Newteacher_documenttable +document_categoryenum (migration0015). The vault is disabled with a clear message untilR2_PRIVATE_BUCKETis set (tax PII must not go in the public bucket).- Square → Stripe: runbook added at
docs/SQUARE-TO-STRIPE-MIGRATION.md. Saved cards move via a PCI processor-to-processor migration (Square exports the encrypted vault to Stripe — not a manual export). A school already on Square can adopt QuarterNote today for tools + payroll (path A, no money movement); full migration + school-as-merchant collection is the deferred payment-execution phase (which also unlocks Stripe-generated 1099s).
- Square → Stripe: runbook added at
- Flexible teacher compensation / payroll (
/school/payroll) — schools define how each teacher is paid and QuarterNote computes what's owed per month. Five rule types: base monthly salary, per hour taught, per lesson, per student/month, and tiered per-lesson (e.g. 1–3 → $30, 4–8 → $40, 9+ → $50), plus an optional minimum-hours threshold (paid only if they taught ≥ N hours). Rules are per-teacher with an org-wide default fallback. The payroll page shows each teacher's taught activity (lessons / hours / distinct students), the applied rule, and the computed pay, with month navigation, totals, and CSV export. Newteacher_compensationtable +compensation_typeenum (migration0014); engine insrc/lib/data/payroll.ts.- Scope (decided with Sam): this is the calculation layer only — QuarterNote computes what to pay; the school pays teachers out themselves (their bank/payroll). Actually moving the money (students pay the school → we transfer to teachers) is the heavier follow-up that needs school-as-merchant + KYC + 1099s; deliberately not built. Lessons count once they've happened (not cancelled/no-show); hours derive from lesson length.
- Schools — round 2 (the gap list + master schedule + JWT refresh) — a big follow-up filling out the school workspace:
- Master schedule (
/school/schedule) — every teacher's weekly availability in one grid, plus the next booked lessons across the school. Read-only (teachers still own their hours). - Teacher onboarding nudges (
/school/teachers) — per-teacher setup checklist (Profile → Verified → Payouts → Live), a "N teachers still finishing setup" banner, Nudge (in-app notification to finish Stripe/profile) and Resend (re-email a pending invite). - Earnings / payroll (
/school/earnings) — per-teacher paid-lesson breakdown for the month (lessons, student payments, teacher earned) + totals + CSV export (/school/earnings/export). (True centralized payroll — school-collects-tuition-pays-a-wage — still deferred pending the payout-direction decision.) - Recaps (
/school/recaps) — read-only view of recaps teachers have sent across the school's lessons. Teacher↔student messages stay private (deliberately not surfaced to admins). - Co-admins (
/school/settings) — invite additional administrators (migration0013adds theadminorg-invite kind); owner-only add/remove. Org access is now resolved by membership (getOrgForUser), so co-admins who don't own the org still get in. - School settings (
/school/settings) — rename the school + edit the billing/contact email. - Student "Your school" card (
/student) — enrolled students see their school + assigned teacher with a one-tap "Book a lesson". - JWT session refresh (no sign-out) — accepting a school invite that changes your role (student→teacher, or →school co-admin) now refreshes the session client-side: the
jwtcallback re-reads role ontrigger: "update", and the accept page routes through aSessionRefreshcomponent that callsupdate()then navigates. Replaces the earlier "log out and back in" caveat. - Nav expanded: Overview · Schedule · Teachers · Students · Lessons · Recaps · Earnings · Billing · Settings.
- Master schedule (
- Schools / studios (third customer type) — a school signs up (
role: "school"), names its studio (/school/new), then runs everything from a new/schoolworkspace: Overview (teacher/student/lesson/payment stats + subscription status), Teachers (invite by email → they join keeping their own profile), Students (enroll by email + assign a teacher, reassign inline), Lessons (all lessons across the school's teachers), and Billing. New tenancy tables —organization,organization_member,student_enrollment,organization_invite(one tokenized invite table serving both teacher→member and student→enrollment, mirroring the existingstudent_invitepattern) — plusorg_id+sourceonlesson. Migration0012.- Money model (the settled rule): monthly fee for the tools; a percentage only when we bring the student. Schools pay a Stripe Billing subscription (separate from the Connect payout flow); lessons between a school's enrolled student and its teacher are stamped
source = "school"and carry no platform fee. Attribution is resolved at booking time (resolveAttribution) and wired into every paid path — trial booking,/api/checkout, weekly series, and one-click reserved-lesson pay. Marketplace-sourced students still pay the percentage even when the teacher belongs to a school. - Billing (
/school/billing, feature-gated onSTRIPE_SCHOOL_PRICE_ID): Checkout insubscriptionmode → webhook mirrorscustomer.subscription.*+ subscription-modecheckout.session.completedontoorganization.subscription_status; Stripe billing portal for manage/cancel. Shows "not configured" until a recurring Price id is set. - Org-invite accept (
/org-invite/[token]): threads through register/login like the student invite; a teacher invite seeds a teacher profile (and promotes a plain student account to teacher), a student invite enrolls + assigns.role: "school"added to the enum + role router (/dashboard→/school) + a "Run a school or studio" option on register. - Deferred (needs Alex's call): how a school pays its teachers — per-lesson split vs. true wage/payroll. Today school-student payments route to the teacher's Connect account fee-free; the school→teacher payout direction (school-as-merchant / payroll) is intentionally not built. See
docs/PROJECT-SCOPE.md§4. - Fixed a latent bug found along the way:
/api/checkoutcomputed a fee-exempt split for its response but never passedfeeExemptto Checkout, so it always charged the platform fee regardless. Now honors attribution + the teacher'sfeeExemptflag. - ⚠️ Not yet deployed: run
npm run db:migrate(applies0012) and register the new Stripe webhook events (customer.subscription.created/updated/deleted) on the production endpoint before this goes live.
- Money model (the settled rule): monthly fee for the tools; a percentage only when we bring the student. Schools pay a Stripe Billing subscription (separate from the Connect payout flow); lessons between a school's enrolled student and its teacher are stamped
2026-06-30
- Timezone is now user-settable (from beta feedback — Alex couldn't change it, was stuck on
America/New_Yorkfrom Bangkok). A sharedTimezoneSelect(full IANA list viaIntl.supportedValuesOf, with a "use my current timezone" shortcut) on the teacher Schedule page and student Settings. Changing a teacher's timezone re-stamps their existing availability blocks so open slots recompute in the new zone. Single shared server action (updateTimezone, validates against the runtime tz database). - Booked slot shown on student page (feedback) — the teacher's student detail page now shows a "Next lesson" highlight (day + time) and includes the time on every lesson in the history — all formatted in the teacher's timezone, not the server's UTC.
- Friendlier first-connect Stripe message (feedback) — a brand-new teacher (no students/earnings yet) now sees "Your Stripe is connected — students can now book and pay you" instead of the confusing "You're all paid up. Nothing to chase." (which stays for active teachers).
- Owner/partner fee bypass — a
teacher_profile.fee_exemptflag (migration0011) zeroes the platform fee on that teacher's own lessons across every booking path (marketplace, weekly, embed widget, API). The fee actually charged is stamped into the Checkout metadata and the webhook records that (instead of recomputing), so earnings stay accurate. Enabled for the owner account. - Lessons calendar (
/teacher/calendar) — a new nav item with day / week / month views of booked lessons, navigable (prev/next/today), color-coded by status, click-through to the student. All bucketed and rendered in the teacher's timezone. - Marketplace chrome fix (feedback) — the public
/teacherspages are a student/public surface, so a logged-in teacher now sees the lightweight public header (with a "← Back to dashboard" link) instead of their teacher sidebar wrapped around a student booking flow. Students still get their full app shell there (browsing teachers is core to their nav).
2026-06-29
- Kodokyo feedback widget — embedded the Kodokyo feedback widget app-wide via
next/script(root layout) so beta testers (Alex, Kevin) can submit in-app feedback from any page. - Fix: login 500 after logout — the credentials sign-in server action had no error handling, so a wrong password threw an uncaught Auth.js
AuthErrorand surfaced as a 500 / "Server Components render" crash. Login now uses a client form +useActionState(mirrors register) and shows an inline "Invalid email or password." Re-throws the success redirect; only real auth failures become the inline error. - Payouts copy — clearer empty-state on
/teacher/payoutsdistinguishing "start connecting Stripe" vs "finish the steps you started," after a teacher didn't realize "Continue Stripe setup" was the connect action. - Availability grid (
/teacher/schedule) — replaced the day/from/to dropdown form with a click-and-drag weekly grid (7 days × 48 half-hour cells, full 24h to support cross-timezone teaching). Cells merge into contiguous blocks; one "Save availability" swaps the whole week atomically.getOpenSlotsnow handles a24:00block end (Luxon rejects hour 24) so the midnight slot isn't dropped. - Teacher-initiated student invites — teachers can "+ Add student" by email from
/teacher/students. Newstudent_invitetable (migration0008) + tokenized/invite/[token]resolver: new people land on/register?invite=…(email prefilled, role forced to student) → booking page after signup; existing accounts get an in-app notification + email straight to the booking page. Invite token threaded through credentials + Google sign-in on both login and register. Roster shows a "Pending invites" section with revoke. Reuses the existing Stripe booking/payment path. - Recap composer quick wins — hybrid type-ahead for student name (from the teacher's roster) and instrument; a reference-links section (YouTube/Spotify/etc.) that renders in the "what the family receives" preview.
- Student booking — day/time slot picker — the student-side "Pick a time" on a teacher's profile (trial + weekly) is now a Calendly-style picker (choose a day → choose a time) instead of one long dropdown of every 30-min slot. Submits the same
startAt, so payment/booking logic is unchanged. - Fix: teacher-card credit badge overflow — the "★ Played with …" chip on storefront cards (home +
/teachers) now caps to the card width and truncates long credit lists instead of spilling past the edge. - Recap delivery is now real (Phase A) — "Approve & send" no longer a mock. The composer gained a "Send to lesson" selector; sending publishes the edited recap as the lesson's
note(summary + practice plan + reference links), notifies the student (recap_ready→ in-app + Resend/Twilio), and marks the lesson completed. Re-sending the same lesson updates its note (one recap per lesson). Reference links persist (note.links_json, migration0009) and render on/student/recaps. This is the foundation the deferred audio→text + attachments build on. - Recap audio→text (Phase B) — teachers can record (in-browser
MediaRecorder) or upload an audio clip in the recap composer; it's transcribed via OpenAI Whisper and fills the transcript field, which the AI then drafts from. REST (no SDK), audio transcribed-and-discarded (never stored). NewPOST /api/recaps/transcribe(teacher-only). New envOPENAI_API_KEY/OPENAI_TRANSCRIBE_MODEL(optional — feature returns a clear "not configured" message when unset). - Recap file attachments + large audio (Phase C) — teachers can attach files (sheet music, backing tracks, PDFs/images) to a recap; they persist (
note.attachments_json, migration0010) and render as download links on/student/recaps. Storage is Cloudflare R2 (S3-compatible, free egress — chosen over Vercel Blob since recap files are downloaded/replayed repeatedly): browser uploads direct to R2 via presigned PUT URLs issued by a teacher-gated/api/uploads/signroute, bypassing the ~4.5MB function body limit. The recap audio path routes clips >4MB through R2 too (transcribe-from-URL, then the temp object is deleted) — so full-lesson recordings transcribe up to Whisper's 25MB limit. New envR2_ACCOUNT_ID/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_BUCKET/R2_PUBLIC_URL.
2026-06-21
- Marketing site + SEO/GEO — public, SEO-optimized pages with a shared header/footer (internal-linked): /for-students and /for-teachers feature pages (covering the full AI suite, booking/payments, scheduling, cancellations, recurring lessons), a /developers page documenting the Public Booking API and a new embeddable booking widget (live demo iframe + copy-paste snippet), and a public /faq with
FAQPageJSON-LD. SEO infra:sitemap.ts,robots.ts(explicitly allows AI crawlers — GPTBot/ClaudeBot/PerplexityBot/etc.; disallows app surfaces),llms.txtroute (GEO),Organization+WebSiteJSON-LD on the home page, per-page metadata + canonicals. Home footer swapped to the richer shared footer. - Embeddable booking widget —
/embed/[teacherId], an iframe-friendly, self-contained booking form (slots + name/email → Stripe Checkout, breaking out of the iframe to pay). Teachers drop one<iframe>into their own site; the platform-fee/payout split still runs through us.noindex.
2026-06-20
- AI profile writer (teacher) — a "Draft with AI" button on the profile editor generates a punchy headline + bio from the teacher's instruments, genres, and real verified credits (pulled server-side, not trusted from the client), leading with their strongest credential. Output fills the editable fields; the model is constrained to only the facts provided (no invented credits). Lowers the blank-page barrier in teacher onboarding.
- AI progress insights — on
/student/recaps(once a student has ≥2 recaps), a one-click "Your progress" rollup reads the recap history and returns an encouraging headline, what's improved, what to keep working on, and a concrete next milestone. On-demand; grounded in recaps. (The teacher-side trajectory need is served by the lesson-prep brief.) Also: eslint now ignores_-prefixed unused args (theuseActionStateconvention). - AI practice coach —
/student/practice, a between-lessons chat that answers a student's practice questions grounded in their own recent recaps + goals (reinforces what their teacher actually assigned, not generic advice). Added to the student nav. Auth + Zod-validated; conversation history bounded; context is fetched server-side (never trusted from the client). - AI lesson prep ("pick up where you left off") — on a teacher's student page, a one-click brief reads the student's recent recaps and returns where they are, what to drill next lesson, and what they've struggled with. Grounded strictly in the recaps; on-demand (no AI spend on page load); shows only once the student has recaps.
- AI natural-language teacher search — on
/teachers, students can describe what they want in plain English ("jazz guitar, intermediate, evenings, someone who's toured") and AI ranks the best-fit verified teachers, each with a one-line "why this teacher" reason, plus a restatement of the request. The model is constrained to the real teacher list (no hallucinated teachers/ids) and falls back to normal browse if AI is unavailable. First AI feature that faces the student at the front door (previously AI only touched teacher recaps). - Legal pages — public
/privacy(Privacy Policy) and/terms(Terms of Use), tailored to the marketplace: Stripe payments + platform fee, AI recap processing + subprocessor list, the cancellation / makeup-credit / recurring-lesson policies, independent-contractor relationship, and a parent/guardian clause for minors. Linked from the homepage footer and referenced on the register page; contact set tosupport@kumokodo.ai. Drafts — have counsel review before launch; governing law currently set to Texas. - Launch polish — students can save a mobile number in Settings (validated; stored on the user) so SMS reminders can actually reach them.
.env.exampledocuments the new notification + cron vars. Security hardening from review: the reminder cron fails closed in production whenCRON_SECRETis unset, and notification-email HTML now escapes all interpolated values (names/bodies) and restricts the CTA link to our origin. - Email + SMS notifications & lesson reminders —
notify()now fans out to email (Resend) and SMS (Twilio) for important events (booking confirmed, cancellations, makeup credits, recap ready, reminders), best-effort and a no-op unless the providers are configured. A new reminder cron (/api/cron/reminders, hourly viavercel.json, Bearer-protected byCRON_SECRET) sends a one-time reminder for confirmed lessons starting within 24h (dedup vialesson.reminder_sent_at) to both student and teacher. New env:RESEND_API_KEY/RESEND_FROM,TWILIO_*,CRON_SECRET. Migration0007. - In-app messaging — teacher↔student direct messages.
/messageslists conversations (newest first, unread dot);/messages/[id]is a chat thread with a composer. Start one from a teacher's profile ("Message …") or a student's page ("Message student"). Per-participant read tracking (last_*_read_atonmessage_thread), an unread badge on the Messages nav item (sidebar + mobile chat icon), and a notification + nav badge when a new message arrives. Migration0006. - Recurring weekly lessons — students can set up a standing weekly slot with a teacher from the profile booking card. It holds that time each week (reserving the next 4 instances), charges the first via Checkout now, and the rest show under Reserved — payment due on My lessons for one-click pay. A Weekly plan section lists active series with "End weekly plan" (drops future unpaid instances; paid ones cancel individually under the refund policy). Teachers see a Weekly plan chip on the student's page. Uses the existing
recurring_seriestable +recurring_instancelessons. - In-app notifications — a
notificationtable + best-effortnotify()helper (a failed notification never breaks the underlying action). Events wired in: booking confirmed (student + teacher), makeup lesson booked, lesson cancelled (the other party, plus the student's makeup-credit grant), recap ready, and new review. A bell with an unread badge sits in every app shell (desktop sidebar + mobile header), and/notificationslists them with "Mark all read". Migration0005. - Help section —
/help, a role-aware how-to center (student / teacher / general) with collapsible topics covering booking, payments, lessons, recaps, cancellations + makeup credits, payouts, the public booking API, notifications, and account. New shared(app)route group hosts the cross-role Notifications + Help pages inside the same shell. - Cancellations + makeup credits — students and teachers can cancel a confirmed lesson, with a fair, automatic policy (
src/lib/cancellation.ts): teacher-cancel or student-cancel with ≥24h notice → full refund (Stripe refund reverses the marketplace split); student-cancel inside 24h → no refund but a makeup credit (a prepaid, teacher-specific free lesson, 90-day expiry). Credits surface on the student's lessons page and apply automatically at the top of that teacher's booking widget (free, instantly-confirmed booking — no Stripe). Cancelling reopens the slot. Inline two-step confirm (no native dialogs); refund runs before state flips so a lesson is never cancelled with money stuck. Newmakeup_credittable + lesson cancellation audit columns (migration0004). - Public Booking API — teachers can embed booking on their own websites. CORS-enabled
GET /api/public/teachers/:id/slots(open slots + teacher summary) andPOST /api/public/bookings(guest booking → Stripe Checkout URL with optional return URLs). Payment + platform-fee/teacher-payout split run through us. Docs:docs/PUBLIC-BOOKING-API.md. - Landing redesign — cohesive with the app — homepage now uses the product's own design system (violet/teal on soft pastel surfaces, the shared
Card/Chip/Button/Avatarkit, system type) so it reads as the same product, just polished. Two-column hero with a gradient "legends" accent and a floating teacher card + AI-recap preview, featured-teacher grid (the real storefront cards), three-step "how it works," an AI-notetaker spotlight on a violet→teal gradient panel with a recap mockup, and a teacher CTA. Subtle load + scroll reveals (reduced-motion aware), accessible + OG meta. (Replaced both the earlier dark-glow hero and the off-brand "Liner Notes" print concept.) - Teacher Students roster (
/teacher/students) — every student in one place: per-student lessons, attendance %, recaps sent, next lesson; a detail page per student with "working on now" (from latest recap), goals, full recap history, and lesson list. - Reviews & ratings — students rate a teacher (1–5 + text) after a completed lesson, one review per lesson. Ratings roll up into the teacher's
avgRatingshown on storefront cards, and reviews render on the public profile. - Earnings dashboards — teachers see their net/gross/fee per lesson + Stripe payout balance (
/teacher/earnings); admins see platform-wide revenue, take rate, GMV, payout balance, and recent transactions (/admin/earnings). - Scheduling & availability — teachers set weekly availability windows (
/teacher/schedule); the booking flow now offers real open 30-min slots (timezone-correct via luxon, shown in the student's local time) and validates the chosen slot server-side. Replaces the free-form datetime picker. - Public
/changelogpage rendering this file; portfolio docs added (TECH_STACK.md,CHANGELOG.md, orientedCLAUDE.md); registered in Roushi. - Demo teachers hidden — only verified pros show in the storefront.
- Student area — own app shell with Lessons (upcoming + Join lesson into the live room, plus past), Recaps, Find teachers, and Settings (editable learner profile).
/dashboardis now a role router. - Teacher names shown on storefront cards + profiles (headline becomes the subtitle).
isAdmincapability flag — decoupled fromrole, so one account can be teacher + admin (e.g. kitt).requireAdmin()guard; Admin nav item.- Sign-out + nav escape hatches added to teacher/admin shells; storefront header reflects auth state.
- Fixes: student settings now upserts (was a no-op for OAuth accounts with no profile row); Auth.js
createUserevent seeds a student profile for OAuth sign-ups. - Verified the platform collects its cut — a $45 booking produced a $9.00 application fee (20%).
2026-06-19
- Marketplace storefront —
/teachersdiscovery (search + instrument/modality filters, SEO) and/teachers/[id]public profiles with verified credits ("pedigree"), samples, rate/trial. - Book-a-trial → Stripe Checkout destination charge — the real money path now runs from the UI and establishes the student↔teacher relationship.
- Google sign-in wired on login + register (gated on
AUTH_GOOGLE_*). - In-app lesson room (Daily) + automatic AI notetaker — teacher starts a lesson, both join via a no-login link; live transcription → AI recap → published to the student.
- Security hardening — instant-lesson no longer attaches to arbitrary accounts (confused-deputy); student join link bound to a confirmed lesson + time window.
2026-06-18
- Adopted Alex's design system — Tailwind v4 tokens (violet/teal), primitive UI kit, app shell (sidebar + mobile tab bar), teacher "Today" dashboard.
- M2 teacher supply — registration/roles, teacher onboarding hub, profile editor with credits, Stripe Express payouts, admin verification queue.
- Lesson Recap (AI) — composer that drafts a warm recap + practice plan from quick notes or a pasted transcript (AI SDK v6 via AI Gateway).
2026-06-16
- Initial scaffold — Next.js 16 + Drizzle/Postgres (Neon) + Auth.js v5 + Stripe Connect.
- Full Phase-1 data model; marketplace payment path (destination charges, webhook, reversible refunds).
scripts/demo-booking.tsproves the student → platform-cut → teacher-payout split end-to-end in Stripe test mode.
Reference docs
- Vision:
docs/PROJECT-SCOPE.md· MVP spec:docs/PHASE-1-PRD.md· Stack:TECH_STACK.md