Premium artisanal skincare and wellness e-commerce site. "Skincare is self-care."
Hand of Yah is a custom-built DTC skincare brand website built on Next.js (App Router) with Sanity for content, Stripe for payments and subscriptions, and Supabase for auth and customer data. It replaces a Squarespace site that failed to meet the brand's premium positioning requirements.
The design concept is "Sacred Apothecary" — warm earth tones, editorial typography (Cormorant Garamond serif + Outfit sans-serif), and a shopping experience modeled after Aesop's intentional, unhurried UX.
The site is live at handofya.com and is fully self-manageable: the owner can create and publish journal posts, update product content, and manage educational content without developer involvement through Sanity Studio at /studio.
| Layer | Technology |
|---|---|
| Framework | Next.js (App Router) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS |
| CMS | Sanity v3 |
| Payments | Stripe (Checkout Sessions + Subscriptions) |
| Auth | Supabase (Magic Link, passwordless) |
| Database | Supabase PostgreSQL |
| Listmonk (self-hosted on Render) | |
| Hosting | Vercel |
| Testing | Playwright (E2E) |
- Node.js 18+
- npm
cd web
npm installCopy .env.local.example to .env.local and fill in your credentials:
cp .env.local.example .env.local| Group | Variable | Description |
|---|---|---|
| Sanity | NEXT_PUBLIC_SANITY_PROJECT_ID |
Your Sanity project ID |
NEXT_PUBLIC_SANITY_DATASET |
Dataset name (default: production) |
|
SANITY_API_TOKEN |
Read token from Sanity dashboard | |
SANITY_REVALIDATE_SECRET |
Random string — validates ISR webhook calls | |
| Supabase | NEXT_PUBLIC_SUPABASE_URL |
Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Public anon key (safe to expose) | |
SUPABASE_SERVICE_ROLE_KEY |
Service role key (server-only, never expose) | |
| Stripe | STRIPE_SECRET_KEY |
Server-side Stripe secret key |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
Client-side Stripe publishable key | |
STRIPE_WEBHOOK_SECRET |
Webhook signing secret from Stripe dashboard | |
| Listmonk | LISTMONK_URL |
Your Listmonk instance URL |
LISTMONK_API_USER |
Listmonk API username | |
LISTMONK_API_PASSWORD |
Listmonk API password | |
LISTMONK_LIST_ID |
List ID for newsletter subscribers | |
| Site | NEXT_PUBLIC_SITE_URL |
Full site URL (default: http://localhost:3000) |
cd web
npm run devOpen http://localhost:3000.
Visit http://localhost:3000/studio to manage content. The Studio is embedded in the Next.js app — no separate deployment needed.
projects/handofya/
├── web/ # Next.js app (deploys to Vercel)
│ ├── src/
│ │ ├── app/ # Pages and API routes (App Router)
│ │ ├── components/ # React components
│ │ ├── lib/ # Utility libraries (Sanity, Supabase, Stripe, cart)
│ │ └── sanity/ # CMS schemas and GROQ queries
│ ├── tests/ # Playwright E2E tests
│ ├── public/ # Static assets
│ ├── .env.local.example # Environment variable template
│ └── playwright.config.ts # Playwright configuration
└── docs/
├── prd.md # Product requirements
├── architecture.md # Architecture decisions
├── roadmap.md # Product roadmap and pre-launch checklist
├── CHANGELOG.md # Change history
├── design/ # Brand identity, design tokens, prototype
├── specs/ # Phase 1.5 spec artifacts (shape, standards, plan)
├── sessions/ # Session notes by date
└── tests.md # Test specifications (91 framework-agnostic tests)
| Route | Description |
|---|---|
/ |
Homepage — hero, featured products, journal preview, brand story |
/shop |
All products grid |
/shop/category/[category] |
Category pages: face, supplements, hair-oils, eye-cremes, face-masques, fragrances |
/shop/[slug] |
Product detail page (SSG with ISR) |
/journal |
Journal listing page |
/journal/[slug] |
Individual journal post |
/learn |
Learn hub — educational articles |
/learn/[slug] |
Individual learn article |
/ingredients |
Ingredient database with search |
/ingredients/[slug] |
Individual ingredient page |
/about |
Brand story and philosophy |
/faq |
Frequently asked questions |
/contact |
Contact form |
/shipping-returns |
Shipping and returns policy |
/terms |
Terms of service |
/privacy |
Privacy policy |
/cart |
Cart page |
/checkout |
Checkout (redirects to Stripe hosted checkout) |
/checkout/success |
Order confirmation page |
/login |
Magic link sign-in |
/signup |
Account creation |
/auth/callback |
Supabase auth callback handler |
/account |
Customer account dashboard |
/account/orders |
Order history |
/account/subscriptions |
Subscription management (pause, cancel, resume) |
/account/wishlist |
Saved products |
/account/settings |
Account settings |
/studio/[[...tool]] |
Embedded Sanity Studio (owner-only) |
| Method | Route | Description |
|---|---|---|
POST |
/api/checkout |
Creates a Stripe Checkout Session; validates prices server-side against Sanity |
POST |
/api/webhooks/stripe |
Handles Stripe events: creates orders, links to customer accounts, triggers confirmation emails |
GET |
/api/orders |
Returns authenticated customer's order history from Supabase |
GET |
/api/subscriptions |
Returns authenticated customer's active subscriptions |
PATCH |
/api/subscriptions/[id] |
Updates a subscription status (pause/resume/cancel) — syncs with Stripe API |
GET |
/api/wishlist |
Returns authenticated customer's wishlist items |
POST |
/api/wishlist |
Adds a product to wishlist |
DELETE |
/api/wishlist/[slug] |
Removes a product from wishlist |
POST |
/api/contact |
Submits contact form |
POST |
/api/newsletter |
Adds an email address to the Listmonk newsletter list |
POST |
/api/revalidate |
ISR revalidation webhook — called by Sanity on content publish |
Concept: Sacred Apothecary
Fonts:
- Display / headings: Cormorant Garamond (Google Fonts, variable)
- Body / UI: Outfit (Google Fonts, variable)
Color Palette:
| Name | Hex | Usage |
|---|---|---|
| Parchment | #F5F0E8 |
Primary background (replaces white everywhere) |
| Umber | #2C2420 |
Primary text (replaces black everywhere) |
| Terracotta | #C4704B |
Primary CTA buttons, active states, link hover |
| Sage | #8B9A7E |
Ingredient tags, subscription badges |
| Stone | #E8E2D8 |
Card backgrounds, section dividers |
| Taupe | #B8AFA6 |
Borders, muted text, placeholders |
| Espresso | #1A1614 |
Footer background, dark sections |
| Amber | #D4A574 |
Hover states on Terracotta |
| Linen | #FAF7F2 |
Lighter background variant, modal overlays |
| Clay | #A85E3A |
Pressed/active button states |
Full documentation: docs/design/brand-identity.md, docs/design/design-tokens.md.
The Sanity schema has 7 document types, accessible through the Studio at /studio:
| Schema | Key Fields | Notes |
|---|---|---|
product |
name, slug, price, description (Portable Text), ingredients (references), usageInstructions (Portable Text), category (reference), images, subscriptionEligible, featured, status, seo | Supports SSG with ISR |
category |
name, slug, description, order | 6 categories: Face, Supplements, Hair Oils, Eye Cremes, Face Masques, Fragrances |
ingredient |
name, slug, description (Portable Text), benefits | Cross-referenced by product; powers ingredient database |
journalPost |
title, slug, featuredImage, body (Portable Text), excerpt, category (reference), publishedAt, seo | Blog-style editorial content |
journalCategory |
name, slug | Tags for journal posts |
learnArticle |
title, slug, featuredImage, body (Portable Text), relatedProducts (references), seo | Educational content hub |
page |
title, slug, body (Portable Text), seo | Static pages: About, FAQ, Shipping, Terms, Privacy |
Cart: Guest cart stored in localStorage. Authenticated user cart persists to Supabase. Subscription items (monthly / every 2 months / every 3 months) are marked at add-to-cart time.
Checkout: Redirects to Stripe hosted checkout. Prices are validated server-side against Sanity before creating the session — client-side price tampering is rejected.
Subscriptions: 10% discount on all subscription-eligible products. Customers can pause, resume, or cancel from /account/subscriptions. Changes sync immediately to Stripe.
Free shipping threshold: $75 USD.
Order flow: Stripe webhook (payment_intent.succeeded) creates the order in Supabase, links it to the customer account by email, and triggers an order confirmation email via Listmonk.
- Connect the
web/directory to a Vercel project - Add all environment variables from
.env.local.exampleto the Vercel dashboard - Set the root directory to
web/in Vercel project settings - Deploy — the build runs
next buildand outputs static/ISR pages
- Create a project at sanity.io (free tier is sufficient)
- Copy the Project ID and Dataset name into your environment variables
- Add CORS origins for
localhost:3000and your production domain in the Sanity dashboard - Set up a webhook pointing to
https://yourdomain.com/api/revalidate?secret=YOUR_SANITY_REVALIDATE_SECRETto trigger ISR on content publish
- Create a project at supabase.com (free tier: 500 MB database, 50K auth users)
- Enable Email auth with magic link in Authentication settings
- Create the following tables (see
docs/specs/2026-03-27-1200-handofya-redesign/shape.mdfor full schemas):customers— account profiles linked to Supabase auth usersorders— order records created by the Stripe webhooksubscriptions— active subscription records linked to Stripe subscription IDswishlists— saved product slugs per customer
- Add your Supabase URL and keys to your environment variables
- Add your Stripe API keys to environment variables
- Enable Stripe Tax for US compliance
- Create products and prices in the Stripe dashboard to match your Sanity catalog
- Create subscription prices (monthly, bi-monthly, quarterly) for eligible products
- Configure the webhook endpoint in the Stripe dashboard pointing to
/api/webhooks/stripe - Copy the webhook signing secret to
STRIPE_WEBHOOK_SECRET
- Deploy Listmonk on Render using the official Docker image (see Listmonk docs for Render setup)
- Configure an SMTP relay — Amazon SES or Postmark recommended for deliverability
- Set up SPF, DKIM, and DMARC DNS records for handofya.com
- Create an order confirmation transactional template and a newsletter subscriber list
- Copy your Listmonk URL, credentials, and list ID to environment variables
The test suite is Playwright E2E, covering all critical flows: product browsing, add to cart, checkout, auth, subscriptions, and wishlists.
cd web
npx playwright testRun a specific test file:
npx playwright test tests/checkout.spec.tsSee docs/tests.md for the full test specification (91 framework-agnostic specs).
| File | Contents |
|---|---|
docs/prd.md |
Product requirements, user stories, functional requirements |
docs/architecture.md |
Architecture decision record — three approaches evaluated, Approach 1 chosen |
docs/design/brand-identity.md |
Color palette, typography, brand voice, logo, spacing |
docs/design/design-tokens.md |
CSS custom properties and Tailwind token reference |
docs/design/design-prototype.md |
Design prototype and visual review notes |
docs/roadmap.md |
Pre-launch checklist and post-launch feature roadmap |
docs/CHANGELOG.md |
Full change history across all phases |
docs/tests.md |
Test specifications |
docs/specs/ |
Phase 1.5 spec artifacts: data models, API surface, standards, references |
TBD