My bakery's ordering flow, top to bottom
Bad Batch Bakes is my own small-batch sourdough bakery, based in Fayetteville, AR. Alongside running the business itself, I designed and built its entire site: a marketing homepage that introduces the brand, and a full weekly-drop ordering flow behind it. Customers browse the current menu, pick a pickup time, and pay online — no separate app, no third-party ordering platform.
Unlike the client and enterprise work elsewhere on this site, this is my own business end to end: I own the brand identity, the interface design, and the entire technical build, including the parts that don't show up in a screenshot — payment processing, order validation, and the tooling I rely on to run the ordering flow day to day.
A brand built around one reusable mark
The brand centers on a single custom asterisk mark, used everywhere in
place of bullets, stars, or decorative flourish — in the logo, as a
list marker, as a loading indicator. Rather than exporting a dozen
one-off graphics, I built it once as an SVG and reused it site-wide via
a CSS mask with currentColor, so the same asset recolors
cleanly against pink, cream, or black backgrounds without duplicated
files.
The brand system, as documentation
The module below documents Bad Batch's own design values and components as labeled content — colors, type, shape, and a few live component demos — the same way a case study can show off a client's visual identity without the case study page itself becoming that brand. Nothing here restyles this page; every demo is scoped to its own contained box.
Sourdough
That Slaps
Display face: Bertho Block, custom-licensed, chunky/condensed — headlines only. Shown here in a bold fallback face since Bertho Block isn't embedded in this documentation.
Body face: Inter, weights 400–700 — everything but headlines.
| Role | Face | Size | Weight |
|---|---|---|---|
| Hero title | Bertho Block | 6rem / 96px | 400 |
| Section title | Bertho Block | ~4.2rem / 67px | 700 |
| Menu heading | Bertho Block | ~2.2rem / 35px | 700 |
| Body | Inter | 1.1rem / 17.6px | 400 |
| Eyebrow label | Inter | 0.72rem / 11.5px | 700, uppercase, tracked |
Shape language: fully-rounded pill buttons (999px radius), 16–22px card radius throughout, and the custom asterisk mark standing in for bullets and stars everywhere instead of default list styling.
Order Anytime
24 hours' notice, that's it.
We Bake Fresh
Small batches, baked to order for your pickup day.
Pick Up in Fayetteville
Grab your bakes at the time you picked, still warm.
Every section on the live site carries a data-section
attribute, and a single IntersectionObserver watches
which one is currently in view to repaint the fixed side rail and
nav bar to match, in real time — no manual scroll-position math. The
same observer-driven approach powers the scroll-reveal card
animations and the looping marquee divider between sections, and
all three respect prefers-reduced-motion, falling back
to a static, unanimated layout when a visitor has that preference
set.
A static marketing site in front of a real app
The homepage is static HTML/CSS/JS. The ordering flow behind it is a separate React 18 single-page app (built with Vite), backed by Supabase (Postgres with row-level security), Stripe Checkout, and Vercel serverless functions — all deployed together on Vercel, with Resend handling order confirmation emails.
- The server re-validates every price against the database on checkout — it never trusts prices sent from the client.
- Webhook handling is idempotent, so Stripe's automatic retries can't create duplicate order confirmation emails.
- Pickup-time validation is timezone-aware and correctly handles daylight saving transitions.
- A "vacation mode" toggle lets me pause ordering myself whenever I need to, with no deploy required.
How the money-handling parts actually work
The stack above is the summary; these are the specific decisions underneath it that made the ordering flow reliable rather than just functional.
Product variants without a separate variants table
Products with sizes (e.g. "S'mores Cookie — 6-Pack") are stored as
plain rows in the products table, using an em-dash
naming convention. The client groups them back into a single card
with a size picker at render time — no schema complexity for
what's really just a display grouping, and I can add a new size by
adding one row in Supabase's Table Editor, no code change.
Cart state: localStorage + server as source of truth
The cart persists across page reloads via localStorage,
but every price shown client-side is explicitly "display only" —
the checkout endpoint re-fetches real prices from the database
before creating the Stripe session, so a stale or tampered cart can
never change what's actually charged.
A local API shim that mirrors Vercel's runtime exactly
npm run dev runs a small Node HTTP server that wraps
the same handler functions used in production
(api/create-checkout-session.js,
api/webhook.js) with Vercel-compatible req/res helpers
— including reading the webhook's raw request body instead of
parsed JSON, since Stripe's signature check needs the exact bytes.
The dev and prod code paths are identical; only the server wrapper
differs.
Split build pipeline
vite build only bundles the /order React
pages. The marketing homepage is copied into dist/
byte-for-byte by a small post-build script — it's deliberately
never touched by a bundler, keeping the two halves of the site
independent.
Test coverage on the money-handling path
The checkout endpoint has unit tests (Vitest, with Stripe and
Supabase mocked) covering: unavailable products, missing/invalid
pickup times, sub-24-hour pickup rejection, non-POST requests, and
— notably — a test asserting the store fails open (stays
orderable) if the store_settings row is ever missing,
rather than accidentally locking out every customer.
Keeping a free-tier database alive
A daily Vercel Cron job (api/keepalive.js) pings
Supabase with a trivial read — Supabase's free tier auto-pauses a
project after 7 days with no activity, which would otherwise take
the whole ordering flow down silently.
What it looks like in production
The homepage introduces the brand and the weekly drop; the order page turns the menu into an actual checkout. Visit badbatchbakes.com ↗ to see it live.
Live, and actually used
Processing real customer orders and payments for my own operating bakery.
Brand, design, and full-stack implementation — including payments — built and shipped by me alone.
I can pause ordering myself through a vacation-mode toggle whenever I need to, with no engineering support required.