All work

Ecommerce Beat Shop · 2026

Huuman Beats

An online beat shop where producers browse, preview, and buy beats, instrumentals, and sound kits.

Huuman Beats

Huuman Beats

A full-stack e-commerce platform for independent music producers to sell beats, instrumentals, and sound kits directly to artists.

Live: huumanbeats.com

Built with: Next.js 15 | React 19 | TypeScript | Prisma | PostgreSQL | Stripe | Cloudflare R2 | NextAuth.js | Zustand | Tailwind CSS


Overview

Huuman Beats is a storefront a producer actually owns. Buyers preview beats in a player that follows them around the site, pick a license tier, check out as a guest or a registered user, and get their files plus a signed contract the moment payment clears. Behind it sits an admin studio that runs the whole business: products, pricing, orders, content, branding. No code, no redeploys.

The Problem

Most independent producers sell through marketplaces like BeatStars or Airbit. Those are easy to start with, but they take a cut of every sale, box in your branding, and shelve your catalog right next to your competitors. If you want a brand people recognize, you need your own storefront. The catch is that building one means solving audio streaming, license management, digital delivery, payments, and a content system all at once. That is a lot of engineering for one producer to take on.

The Solution

A store built for selling beats, not a generic e-commerce template with beats bolted on. Every feature is shaped around what producers and their buyers actually do: audition a track by ear, pick a license with the usage rights they need, download in the format they want, and walk away with a contract attached to the receipt.

Tech Stack

Layer Technology Why
Framework Next.js 15 (App Router, RSC) Server components for fast initial loads; API routes co-located with the app
Language TypeScript, React 19 End-to-end type safety across client and server
Database PostgreSQL (Neon) + Prisma ORM 25+ models with complex relations; Prisma provides type-safe queries and migrations
Payments Stripe (Checkout, Subscriptions, Webhooks) PCI-compliant checkout, subscription billing, webhook-driven fulfillment
File Storage Cloudflare R2 (S3-compatible) Cost-effective object storage for audio files and deliverables; presigned URLs for secure access
Auth NextAuth.js v5 Credentials + Google OAuth, JWT sessions, role-based access control
State Zustand Lightweight client stores for cart (localStorage-persisted) and audio player (ephemeral)
Email Resend + Mailchimp Transactional emails (receipts, contracts, password resets) + newsletter capture
Styling Tailwind CSS Utility-first CSS with dark mode throughout
Validation Zod Runtime schema validation for all API inputs
PDF Generation pdf-lib Auto-generated license contracts embedded in purchase emails

Key Features

Storefront

  • Audio preview player: One persistent player across the whole site, with play/pause, seek, volume, a queue, and loop mode. It streams tagged previews from R2 through signed URLs, so the untagged masters never leave the vault.
  • Multi-license checkout: Every beat carries several license tiers (MP3 Lease, WAV Lease, Exclusive, and so on) at different prices and usage rights. Buyers pick a tier before anything hits the cart.
  • Digital delivery: A purchase unlocks a download button per format (MP3, WAV, Stems/ZIP). Files come through presigned URLs that expire an hour after they’re issued.
  • PDF license contracts: Every sale generates a real contract as a PDF, stamped with the buyer’s name, the beat title, the license terms, and a unique order reference. It rides along on the confirmation email and lives in the account dashboard.
  • Guest checkout: You can buy without an account. A one-off download token gets emailed to you and opens a dedicated download page. That same email offers a link to set a password and claim the account later if you want it.
  • Subscription memberships: Monthly or yearly plans run on Stripe Subscriptions, with download allowances the producer can configure.
  • Coupons and bulk discounts: Percentage or fixed-amount coupons with usage limits and expiry dates, plus bulk rules like buy 2 get 1 free.
  • Exclusive license clarity: Exclusive licenses say “Full ownership · Beat removed from store after purchase” everywhere they appear: license selectors, beat cards, and the homepage comparison table. Hover the comparison table and a tooltip spells out the removal policy.
  • Catalog that behaves on any screen: A list/grid toggle that defaults to list on mobile and grid on desktop. Compact mobile rows pack title, BPM, key, and tags into two tight lines. Filter tags scroll sideways on mobile instead of wrapping. Re-click the active sort button to flip between ascending and descending, with an arrow showing which way you’re going.
  • Blog: A built-in CMS for SEO content, producer tips, and announcements, with featured images, scheduled publishing, and per-post SEO metadata.

Admin Studio

  • Product management: Full create/edit/delete for beats and kits, with multi-file uploads (artwork, tagged previews, untagged masters, WAV deliverables, stems), per-product license setup, tag management, and drag-and-drop ordering.
  • Order dashboard: Every order with its payment status, line items, and customer details.
  • Customer management: Browse all customers, see order history, export to CSV.
  • License templates: Global templates with editable terms stored as structured JSON. Each one defines which file formats are included.
  • Lead capture: A newsletter popup you can trigger on exit intent, scroll depth, or a time delay, wired to Mailchimp.
  • Upload progress: Uploads (previews, masters, stems, artwork) show real-time progress bars tracked over XHR. That matters when someone is pushing a fat stem archive.
  • Store settings: Branding (logo, colors, fonts), the hero section, nav links, announcement bar, analytics IDs (GA4, Meta Pixel, TikTok), and bulk discount rules. All of it editable without a redeploy.

Analytics and Conversion Tracking

  • GA4 ecommerce events: The full funnel runs through gtag: view_item when a beat page loads, add_to_cart on license selection, begin_checkout before the Stripe redirect, and purchase on the verified success page. Every event carries the standard parameters (item ID, name, price, license category, transaction ID, order value). The purchase event only fires once the verify API confirms payment, so a page refresh can’t double-count a sale.
  • Google Ads conversion tracking: GA4 is linked to Google Ads, with purchase imported as a primary conversion and begin_checkout and add_to_cart imported as secondary, observe-only ones. No extra tag code went in. The existing gtag setup feeds both GA4 and Google Ads off the shared measurement ID.
  • Dynamic landing pages for paid search: Genre ad groups point at /beats?tag=trap, /beats?tag=hip-hop, and the like. The beats page rewrites its <h1>, <title>, and meta description from the active tag, so the keyword, the ad, and the page you land on all line up. Google’s Quality Score sees a page that actually matches the search.

SEO

  • Dynamic sitemap: /sitemap.xml builds itself from every published beat and blog post, with lastModified timestamps and priority values.
  • Robots.txt: Set up to welcome public pages and keep crawlers out of admin, API, and account routes.
  • Rich product title tags: Title tags assemble themselves from a beat’s attributes, so “Blackmail - 85 BPM Hip Hop, R&B Beat in F# Minor” writes itself from title, BPM, genre, and key. Genres go in the title; moods, hook availability, and custom copy go in the meta description so nothing is said twice.
  • Keywords meta tag: Populated automatically from a beat’s tag names (genres, moods, instruments).
  • Open Graph and Twitter cards: Product artwork plus the enriched title and description render as OG and Twitter meta, so shared links preview properly.
  • JSON-LD structured data: Organization, Product (pricing, BPM, key, and hook availability as additionalProperty entries, plus keywords), and Article schemas for Google rich results.

Architecture Highlights

Two Layers of Access Control

Auth is enforced twice, on purpose. Next.js middleware catches requests to /admin/* and /account/* and bounces anyone unauthenticated before the page renders. Then the API routes check again through a requireAdmin() helper that verifies the JWT session and role, so a direct API call can’t sneak past the middleware. Three roles (BUYER, STAFF, PRODUCER_ADMIN) decide who can touch what.

Presigned URLs for Every File

Uploads and downloads never proxy through the app server. They go straight to R2 over presigned URLs. To upload, the admin client asks the API for a presigned PUT URL and sends the file directly. To download, the API mints a GET URL good for one hour and hands it back. That keeps big binary files off the serverless function and steers clear of Vercel’s 4.5 MB response body limit.

Client State with Zustand

Two small Zustand stores handle client state without Redux ceremony. The cart store persists to localStorage, so items survive reloads and closed tabs. The player store is throwaway: current track, queue, playback state, volume, all reset on refresh. Both expose plain hooks that components read straight from.

Webhook-Driven Fulfillment

Stripe webhooks run everything after payment. When a checkout.session.completed event lands, the handler writes the order and payment to the database and returns a 200 to Stripe right away. The slow work (generating the PDF contract, uploading to R2, sending the confirmation email) runs in a Next.js after() callback that fires once the response is already out the door. So Stripe gets a fast answer and the buyer still gets everything they paid for. A fallback verify endpoint covers the rare race where the webhook and the buyer’s success-page redirect arrive at once.

Challenges and Solutions

Audio Streaming on Vercel

Problem: The first version proxied audio through a Next.js API route so the stream URL wouldn’t leak the S3 key. Trouble is, Vercel serverless functions cap the response body at 4.5 MB, which silently chopped audio files off mid-stream.

Solution: I swapped the proxy for a two-step handoff. The API route mints a time-limited presigned R2 URL and returns it as JSON. The player fetches that URL and sets it as the <audio> source, so the browser pulls audio straight from R2 and the serverless function is out of the loop. One more wrinkle: the AWS SDK config had to suppress the x-amz-checksum-mode=ENABLED query parameter, which Cloudflare R2 rejects on presigned URLs.

Prisma Decimals Breaking Cart Math

Problem: License prices live as Decimal in PostgreSQL, and React Server Components serialized them as strings on the way to the client. JavaScript’s + then glued them together instead of adding them, so cart subtotals came out NaN.

Solution: An explicit Number() at every point where a price enters client-side math: the Zustand cart store’s addItem, updateItemLicense, and getTotal, the bulk discount calculator, and the formatCurrency utility. One line per call site, but the work was tracing every path a Prisma Decimal could take to reach an arithmetic operator.

www vs. Non-www in Production

Problem: The audio stream route checks the request origin to stop hotlinking. In production Vercel served the site at www.huumanbeats.com, but the env vars said huumanbeats.com with no www. The startsWith check failed and every audio request came back 403. The debug logs pointed at the mismatch right away.

Solution: I taught the origin checker to accept both the www and non-www form of every allowed origin, so it passes no matter which subdomain the browser uses. That’s sturdier than hoping the env var is set to exactly the right variant.

Stripe Webhooks Failing in Production

Problem: Once the site went live, Stripe logged webhook failures with “other errors” on all 11 attempts across several days. Three problems were stacked on top of each other. First, the endpoint was registered in Stripe as https://huumanbeats.com/api/webhooks/stripe (no www), but the production domain redirects non-www to www.huumanbeats.com, and Stripe doesn’t follow redirects. A 307 counts as a failure. Second, STRIPE_WEBHOOK_SECRET had only ever been added to the local .env.local, never to Vercel, so signature verification threw on every request. Third, even after fixing those, the handler was generating PDFs, uploading to R2, and sending email before it answered Stripe. All that time added up until the Prisma connection dropped with Error { kind: Closed } and the route returned a 500.

Solution: Three fixes, one per cause. Point the Stripe endpoint at the canonical www domain. Add STRIPE_WEBHOOK_SECRET as a Production env var in Vercel. Refactor the handler to return 200 right after the essential database writes and push the PDF, R2 upload, and email into a Next.js after() callback that runs in the background. Then a fourth issue surfaced while replaying the backlog: the verify endpoint had already created those orders as a fallback while the webhook was down, so the retries hit a unique constraint on stripeSessionId. An idempotency check at the top of the guest checkout handler soaks that up cleanly.

Results

Huuman Beats is live at huumanbeats.com, a sales channel an independent producer fully owns. It carries a buyer the whole way from discovery to delivery: browse and preview, pick a license, check out through Stripe, get the files and a PDF contract by email, download in the format you need. And the producer runs the catalog, pricing, content, and branding from the admin studio, with no code changes and no redeploys.