Skip to main content
James Music

Hi! I'm James. I'm blind.
I build things that work for everyone.

Software Engineer. Sound Engineer. Musician. Accessibility Expert.

25 years turning complex problems into production-grade solutions.

About Mr. Music

Born blind, I learned to think in systems before I could see them. That turned out to be a superpower.

I've spent 25 years as a software engineer, shipping everything from startup MVPs to enterprise platforms. I'm also a trained sound engineer, music producer, and multi-instrumentalist with over 30 years playing by ear.

I consult on accessibility because I live it — screen reader, braille display, the lot. When I build or advise, it works for everyone.

Book a 15-minute call

Pick a time that works for you. I'll send a Google Meet link.

Pick a day

Mon
Tue
Wed
Thu
Fri

June 2026

Notes from James

What I'm building, thinking about, and working on.

Check out in .

Building

The Stripe invoice query maps payment intents to a portal type so no component ever imports from Stripe

The getClientInvoices function in lib/portal/stripe-queries.ts takes a stripeCustomerId string and returns an array of PortalInvoice objects — a seven-field interface defined in the same file. The function calls stripe.paymentIntents.list, filters to succeeded and requires_payment_method statuses, then maps each PaymentIntent into the portal shape. The description field falls through three options — metadata.productName first, then pi.description, then the literal string Payment. The date converts from a Unix timestamp by multiplying pi.created by one thousand and formatting with Intl.DateTimeFormat in en-GB locale. The status maps Stripe values to simpler labels: succeeded becomes succeeded, anything else becomes pending, both typed with as const so TypeScript narrows the union. The receiptUrl builds a Stripe dashboard link from the PaymentIntent ID rather than using Stripe's own receipt_url. If the Stripe API call fails, the function catches the error, logs it, and returns an empty array — the invoice section simply does not render rather than crashing the page. The InvoiceTable component that displays this data receives PortalInvoice and never imports anything from the Stripe SDK. The mapping function is the boundary. Every Stripe-specific field name, timestamp format, and status enum stays inside one forty-four-line file, and everything downstream works with a plain TypeScript interface that could be backed by any payment provider.

software
Building

The notification guard checks clerkId not email because a client record can exist before the person behind it

Every admin action in portal/admin/actions.ts that triggers a notification wraps the call in if project client clerkId. The client record always has an email — the createClient form requires it. But the notification only fires when clerkId is not null, meaning the client has actually signed up through Clerk. A client created in the admin panel with email sarah@example.com gets a database record immediately, but her clerkId stays null until she follows the portal link and creates her Clerk account. Sending a session notes notification to sarah@example.com before she has signed up would be confusing — she would receive an email with a portal link she cannot access yet because she has no Clerk credentials. The guard makes the notification system aware of the onboarding gap. I can create a client, add projects, set milestones, write session notes, post comments, add deliverables, and assign action items — all silently. The moment Sarah signs up through Clerk, the webhook at api/webhooks/clerk/route.ts matches her email to the existing record and sets her clerkId. From that point forward, every admin write that would interest a client triggers an email. Five notification functions in lib/portal/notifications.ts — notifySessionNotes, notifyMilestoneUpdate, notifyNewComment, notifyNewDeliverable, notifyNewActionItem — all fire through the same gate. The guard checks relationship status, not record existence, and the entire notification backlog is zero because there is no queue. Anything that happened before signup simply does not notify.

software

If anything I've built or shared has been useful to you...

Buy me a coffee

Stay in the loop

Occasional updates on what I'm building, mixing tips, and accessibility insights. No spam.