MOYE
An online record store for the artist MOYE. Custom checkout backend, an audio player that survives page navigation, and a hand-tuned hover language.
Problem
Off-the-shelf options for artist commerce — Shopify, Bandcamp, Big Cartel — are templated and constrained. They limit creative direction, impose a particular checkout shape, and don't preserve playback state across page transitions. For an artist whose work treats the album as a visual and sonic environment, the platform is part of the product.
MOYE's site is a custom Angular SSR build that owns its own checkout backend, runs a persistent audio player outside the router outlet, and treats hover and cursor behavior as part of the visual identity.
Architecture
- Angular 21 (with @angular/ssr)
- TypeScript 5.9
- Firebase 12 (Functions, Firestore, Storage)
- Express 5
- Vitest
The frontend is an Angular SSR app deployed via Firebase App
Hosting. Cart state lives in a root-scoped signal-based service with
localStorage persistence. Checkout posts to a Firebase Function that
re-fetches prices and validates stock against Firestore, builds line
items server-side, writes a pending order, then creates a Stripe
payment session. A webhook flips the order to paid once Stripe
confirms. The audio player is a singleton service plus a single
persistent <audio>
element mounted outside the router outlet — playback continues
across navigation.
Technical decisions
-
Server-side cart validation, not Stripe Checkout
functions/src/index.ts:62–84 re-fetches prices from Firestore on every checkout and validates stock before building Stripe-bound line items — the client is never trusted on price. functions/src/index.ts:114–124 writes a pending order ledger before the Stripe session is created, and functions/src/index.ts:178–204 flips it to paid in the webhook. Stripe handles payment; MOYE owns session orchestration, validation, and the order record. The tradeoff: more backend code in exchange for end-to-end control of pricing, inventory, and the post-payment hook.
-
Audio player outside the router outlet
web/src/app/core/services/audio-player.service.ts:13–22 is a
providedIn: 'root'service holdingcurrentTrack,isPlaying,position, andqueueas signals, backed by a single persistent HTML<audio>element. The player component is mounted at web/src/app/app.html:7 — outside<router-outlet>— so it survives every route change. Without this shape the<audio>element would unmount and reload on navigation, losing position and rebuffering each time. With it, the user keeps listening as they browse. -
Hover language as a first-class concern
web/src/styles.scss:45–53 defines custom SVG cursors with explicit hotspot coordinates — a default cursor and a separate hover cursor for
a, button. web/src/app/features/home/sections/buy.section.ts:100–145 coordinates a scale-up on product art with a cursor-following caption; signal-driven position updates keep the caption in sync with the mouse. The interaction is choreographed, not browser default — which matters when the site is supposed to feel like part of the album, not a checkout page with merch on it.
What's next
-
WAV streaming. The current player uses a bare HTML
<audio>element with browser-native progressive download — no chunked range requests, no hover prefetch, no in-memory buffer. For high-fidelity tracks this gets sluggish. Chunked range-request streaming plus hover prefetch is the next perf win.