← Back to all projects

Personal

Dr. Moonie Milk

Strawberry milk grew up.

Role
Solo developer and designer
Timeframe
Sep 2026 — Sep 2026
Status
In Production
Team Size
1
Domain
Consumer brand marketing and pre-launch demand capture
Live site
Dr. Moonie Milk cover

Dr. Moonie Milk is the public launch site for a strawberry milk syrup brand that hasn't shipped yet. Its job is to turn early interest into something measurable: visitors can join the waitlist for the first batch, and they can vote on which flavor should come next without handing over an email address. I built and shipped it solo, owning everything from the brand's visual system through to the server it runs on.

The site began as a quick prototype that had real problems — flat visual hierarchy, sections that felt lifeless, and a hosting setup tied to Cloudflare's platform. I rebuilt it from the ground up on TanStack Start, replacing ad-hoc styling with a proper design system so that type sizes, spacing and colour stay consistent everywhere and scale smoothly from a 320px phone to a wide desktop. Animation was added deliberately rather than decoratively: the page reveals itself as you scroll and the hero composition assembles on arrival, but every bit of it switches off for visitors who've asked their device to reduce motion, and the page still works with JavaScript disabled.

Behind the scenes it is intentionally simple and cheap to run. Signups and votes go into a single SQLite database file, and the whole application ships as one Docker image that applies its own database migrations when it starts, deployed to a self-managed VPS. That choice traded horizontal scale for very low operating cost and no third-party dependencies — a deliberate fit for a pre-launch brand, and one I documented carefully, including the single-writer constraint that means it must run as a single instance.

Solo developer and designer

  • Owned the project end to end: design system, frontend, backend, infrastructure and deployment
  • Migrated the site off a vinext prototype on Cloudflare Workers and D1 onto TanStack Start with a self-hosted Node and SQLite stack
  • Built a Tailwind CSS v4 design token layer covering the brand palette, a fluid clamp() type ladder and vertical rhythm
  • Rebuilt all nine page sections as composable React components with corrected visual hierarchy
  • Implemented the waitlist and anonymous flavor-voting server functions, the Drizzle schema and the migration flow
  • Authored the multi-stage Dockerfile, migrate-on-boot entrypoint and Dokploy deployment runbook
  • Layered in scroll reveals, hero entrance choreography, parallax and micro-interactions, all gated behind prefers-reduced-motion
  • Generated the concept vial mockups for the three upcoming flavors and optimized every image asset to WebP
  • Verified layout, forms and accessibility across 320px to 1672px viewports with browser-driven screenshot testing
  • Email waitlist capture with boundary validation, normalization and a silent honeypot bot trap
  • Anonymous flavor voting tied to an httpOnly cookie, so visitors can vote and change their pick without an email
  • Future-flavors carousel where each slide re-themes the whole section to that flavor's palette
  • Nine-section server-rendered landing page with SEO and Open Graph metadata
  • Tailwind CSS v4 design token system: brand palette, fluid clamp() type ladder and shared vertical rhythm
  • Scroll reveals, hero entrance choreography, parallax and micro-interactions, all reduced-motion aware
  • Accessible FAQ accordion and focus-visible states built on Radix primitives
  • Responsive hero artwork with distinct narrow and wide compositions selected by media query
  • Concept vial mockups for three upcoming flavors, generated and served as optimized WebP
  • Brand favicon set (multi-size ICO, PNGs, Apple touch icon) and a PWA manifest
  • Custom 404 route written in the brand's voice
  • Single-command Docker deployment that migrates the database on boot

Problem

The hero artwork appeared split in half by a straight horizontal line. The composition reuses one splash image twice at different scales, once cropped for the large arc and once for the glass, and the glass copy was revealed with clip-path: inset(40% 0 0 0).

Solution

Replaced the hard clip with paired CSS gradient masks so the splash layer fades out across its lower band exactly as the glass layer fades in across its upper band. The stops were chosen so each layer still fully covers the other's hard boundary, eliminating the seam without changing the composition.

Problem

The flavor carousel behaved oddly on the last slide: the right side went empty while a sliver of the previous card bled in from the left. All component state (index, counter, pills, inert flags) was verified correct, so it was purely visual.

Solution

The Carousel had a utility forcing Embla's own viewport to overflow: visible. Embla only repositions a slide to the other side once it believes that slide is off-screen, so visible overflow exposed the gap where the wrap-around slide had not yet moved. Letting the viewport clip made every slide frame identically.

Problem

Section headings rendered far smaller than specified despite using the correct utility classes.

Solution

tailwind-merge was conflating the custom font-size utilities with text colour utilities and stripping them. Extended the cn helper's class-group configuration to register the custom font-size and text-wrap groups.

Problem

The flavor vial product images had no usable transparency; their alpha had been flattened into a baked-in checkerboard pattern, so they could not sit on the brand backgrounds.

Solution

Wrote a programmatic flood-fill tool to detect and strip the fake checkerboard and restore a real alpha channel, then converted the full asset set to WebP for a substantial size reduction.

Problem

Docker builds began failing with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION whenever a dependency had published within the previous day.

Solution

The package.json had no packageManager field, so Corepack resolved a newer pnpm that enforces a 24-hour minimumReleaseAge policy. Pinned the pnpm version and switched the Dockerfile to corepack enable && corepack install in both the deps and runtime stages for deterministic builds.

Problem

SQLite permits only one writer, which conflicts with container orchestration defaults that would briefly run two instances against the same database file.

Solution

Documented and configured a single-replica deployment with rolling deploys disabled, and mounted a persistent volume at /app/data so the database survives redeploys rather than being lost with the container.

Problem

Scroll-reveal animations server-render at opacity 0, so the page would have been effectively blank for anyone without JavaScript, and motion-sensitive visitors needed a genuine opt-out.

Solution

Gated every animation behind useReducedMotion, forcing final visible states rather than skipped transitions, and added a noscript style block that overrides the opacity-0 inline styles so the full page renders without JavaScript.

Problem

The original prototype sized type and layout in vw units, so text scaled erratically and the visual hierarchy collapsed at some viewport widths.

Solution

Replaced vw sizing with a clamp()-based fluid type scale and spacing tokens defined once in the Tailwind @theme layer, then verified the result across seven breakpoints from 320px to 1672px.

Languages

TypeScriptJavaScriptCSSHTMLSQLShell

Frameworks

TanStack StartReact 19TanStack RouterTailwind CSS v4shadcn/uiRadix UIMotionEmbla CarouselNitro

Databases

SQLiteDrizzle ORMbetter-sqlite3Drizzle Kit

Tools

VitepnpmDockerVitestESLintPrettierPlaywrightFontsourceLucideclass-variance-authoritySharp-based image pipeline

Infrastructure

Deployment
Dokploy, OVHcloud VPS, Nitro node-server output, Docker image built from web/ subdirectory context
Cloud
OVHcloud
Containers
Docker (multi-stage: deps, build, runtime)
Other
Persistent volume mounted at /app/data so the SQLite file survives redeploys, Database migrations applied on container boot via docker-entrypoint.sh, Pinned to a single replica with rolling deploys disabled because SQLite allows one writer, pnpm version pinned via packageManager so Corepack resolves deterministically in Docker, Predecessor prototype ran on Cloudflare Workers with D1 (preserved on the original-site branch)
  • Rebuilt a pre-launch brand landing page from a Cloudflare Workers prototype onto a self-hosted TanStack Start, Node and SQLite stack, removing the platform dependency and cutting hosting to a single VPS container
  • Designed a Tailwind CSS v4 design token layer with a fluid clamp() type ladder that fixed inconsistent visual hierarchy across all nine page sections and holds up from 320px to 1672px
  • Built two hardened TanStack Start server functions: a waitlist with network-boundary validation and a silent honeypot, and an anonymous cookie-based voting flow that lets visitors revise a pick without ever supplying an email
  • Shipped a motion layer of scroll reveals, hero choreography and parallax with every animation gated behind prefers-reduced-motion, plus a noscript fallback so opacity-0 reveals never leave the page blank without JavaScript
  • Diagnosed a visible seam in the hero artwork as a hard clip-path edge between two differently scaled copies of the same image, and replaced it with paired gradient masks that cross-fade the layers
  • Traced a carousel glitch on the final slide to a CSS override forcing Embla's viewport to overflow: visible, which broke the library's loop repositioning logic, and documented the incompatibility for future work
  • Authored a multi-stage Dockerfile that rebuilds the better-sqlite3 native addon against the runtime Node ABI and applies migrations on boot, then pinned pnpm via packageManager to stop Corepack resolving a release whose minimumReleaseAge policy broke reproducible builds
  • Recovered real alpha channels from product images whose transparency had been flattened into a baked-in checkerboard, using a programmatic flood fill, then converted all assets to WebP