Animal Garage: building a headless automotive storefront from scratch

Case study of animalgarage.net: SvelteKit sprint, Saleor/Ghost/Supabase hydrators, mock-first flow, and next integration steps.

Animal Garage is a merchandising-forward automotive brand site: culture merch, parts discovery, build threads, video, and loyalty UI in one SvelteKit storefront. Commerce is meant to live on Saleor at a separate domain; editorial content on Ghost; auth and operational data on Supabase.

This post is a case study of how the repo was built from zero, not a product launch announcement. The public site is still a prototype with mock fallbacks in many paths. The interesting part is the development flow: scaffold fast, wire shape before wire API, and use small server modules ("external hydrators") so each route can render with or without backend keys.

Source repo: jjheffernan/animalgaragenet

Starting point: one sprint, many surfaces

Git history on main is compact: 38 commits, essentially one intensive build window around June 2026. The arc is deliberate:

  1. Initial scaffold: SvelteKit 2, Svelte 5, TypeScript, Tailwind v4, ESLint/Prettier, click-through marketing and shop pages.
  2. Phase 2 prototype: mock Saleor-shaped catalog (merch + parts), mega-menus, cart drawer, locale hooks, guides/blog stubs, builds, watch hub, loyalty shell.
  3. Phase 3 parallel workstreams: nav/search/cart fixes, video detail panel, support pages, auth/admin RBAC, Ghost wiring, Saleor swap points, Vitest + Playwright, agent docs and CI.

That ordering matters. The team did not wait for commerce.animalgarage.net or a Ghost instance before shipping UI. Every major route existed with realistic mock data first (src/lib/data/mock/), then gained env-gated server loaders that swap to live APIs when keys are present.

For a brand site with animation, fitment tools, and dual catalogs (merch vs parts), that mock-first flow kept design and IA moving while integrations caught up.

Architecture in one diagram

The planned topology (from docs/infrastructure.md) is a classic headless split:

SvelteKit (animalgarage.net)
    ├── Saleor GraphQL     → products, cart, checkout
    ├── Ghost Content API  → guides + blog
    ├── Supabase           → auth, profiles, builds, YouTube rows
    └── CloudFront + S3    → media CDN (planned)

The frontend owns experience and routing. Saleor owns money and inventory. Ghost owns long-form editorial. Supabase owns sessions and data that is not a SKU.

That separation is the right default for an automotive brand that will outgrow a monolithic CMS, but it multiplies the number of moving parts that must be hydrated on every request.

External hydrators: what they are and how they work

In this codebase, an external hydrator is a server-only module that:

  1. Checks whether integration env vars are set (isGhostEnabled(), isSaleorEnabled(), etc.).
  2. Fetches from the external API when enabled.
  3. Maps the response into internal domain types (Product, Guide, BlogPost, Video).
  4. Falls back to mock data when disabled, on HTTP error, or on empty responses.

Routes stay thin: +page.server.ts calls one function; the hydrator decides live vs mock.

Ghost: guides and blog

ModuleRole
$lib/server/ghost/client.tsContent API fetch, env gating
$lib/server/ghost/posts.tslistGuides, getGuide, listBlogPosts, getBlogPost
$lib/server/ghost/mappers.tsGhost post → app types
RichContent.svelteSanitized HTML via sanitize-html (Netlify SSR–safe)

Posts are filtered by Ghost tag slug: guide for /guides, blog for /blog. The ghost audit scores readiness at 8/10: loaders and mappers are done; OG tags and hard-fail behavior in production are still open.

Alternatives considered (or equivalent in other stacks):

ApproachTradeoff
Ghost (chosen)Editors get a polished writing UI; Content API is simple; another hosted service and tag discipline required
MDX / Keystatic in-repoGit-based, no runtime API; weaker for non-dev editors; what this site (heff.world) uses
Sanity / ContentfulStrong structured content + preview; more schema work and cost
Saleor metadata onlyFewer systems; poor fit for long guides and news cadence

Ghost wins when marketing will publish often and should not touch the storefront repo.

Saleor: catalog, cart scaffold, channels

ModuleRole
$lib/server/saleor/client.tsGraphQL fetch, isSaleorEnabled()
queries.ts / mappers.ts / metadata.tsProducts, collections, fitment tags
catalog/products.ts, parts.ts, collections.tsEnv-gated swap points for loaders
checkout.tsCreate checkout, add line, cookie ag-checkout-id

The Saleor audit scores 7/10: primary catalog loaders swap when PUBLIC_SALEOR_API_URL is set; checkout completion, line mutations, and collection product edges are not.

Alternatives:

ApproachTradeoff
Saleor (chosen)Open-source headless, channels for international, attributes for fitment; ops overhead
Shopify Storefront APIFaster time-to-checkout; less control, platform fees
MedusaJS-native, self-host; smaller ecosystem than Saleor for multi-channel
Embedded Shopify / WooOne admin; fights the "animated custom storefront" goal

Saleor fits a separate commerce.* domain and a parts catalog that will grow past mock JSON.

Supabase: auth, profiles, future CMS rows

ModuleRole
$lib/server/supabase/client.tsSSR cookie client (@supabase/ssr)
auth.tsMagic link, OAuth, mock ag-session cookie when unset
hooks.server.tsPer-request session refresh
supabase/migrations/RLS-first schema

Without env vars, sign-in still works locally via mock session, the same pattern as Ghost and Saleor. OAuth (Google, Discord, Microsoft) is wired for PKCE; provider-specific polish is documented in docs/auth-oauth.md.

Alternatives:

ApproachTradeoff
Supabase (chosen)Auth + Postgres + RLS in one; fast for builds, newsletter, YouTube tables
Auth.js + managed PostgresMore portable; more assembly
Clerk / Auth0Less backend code; vendor lock-in, cost at scale

Supabase matches "garage squad" loyalty, build submissions, and admin RBAC without pushing that into Saleor.

YouTube: watch hub hydration

src/lib/server/youtube/sync.ts defines fetchChannelVideos and syncToDatabase stubs; /admin/youtube triggers manual sync. The plan is cron → Supabase videos + youtube_channels, with shoppable linkedProductIds on the watch detail panel.

Alternatives: manual embeds only (simplest), Mux for owned video, or Immich-style self-host. YouTube sync fits a brand that already publishes on platform.

Development flow that actually shipped the UI

1. Mock data as the contract

Mocks under src/lib/data/mock/ are not throwaway: they define routes, mega-menu categories, YMM vehicles, build threads, and deal banners before any GraphQL exists. TypeScript types in $lib/types/ mirror what Saleor and Ghost will return later.

2. Swap points, not big-bang integration

Each catalog function follows the same template:

// Pseudocode: pattern used across catalog/products.ts, ghost/posts.ts, etc.
if (!isExternalEnabled()) return mockData;
try {
  const live = await fetchFromApi();
  return live?.length ? live : mockData;
} catch {
  return mockData;
}

Frontend components never import Saleor or Ghost directly: only server loads and form actions do. That kept the Svelte 5 component tree free of GraphQL clients in the browser.

3. Parallel workstreams (Phase 3)

docs/phase3-plan.md split work across nav/UX, watch/YouTube, content pages, and auth/admin. That maps cleanly to how agent-assisted development ran: bounded file ownership, merge at dev, CI smoke tests before main.

4. Test and audit gates

Vitest covers mappers, channels, checkout scaffold, Ghost mappers, cookie helpers. Playwright smoke tests guard critical paths. Integration audit docs (ghost-audit.md, saleor-audit.md) record readiness scores so "wired" is not confused with "production-ready."

5. Org sync and deploy hygiene

Late commits add CI-gated sync to the heff-industries org mirror, pre-push secret checks, and sanitize-html instead of isomorphic-dompurify for Netlify SSR. Small choices that matter when the storefront is public.

What the prototype already demonstrates

Despite mock fallbacks, the IA is ambitious and intentional (docs/decisions.md):

  • Dual catalog (merch vs parts) with shared cart UX but distinct routes
  • Mega-menus, promo bar, Pit Lane Deals (auth-gated), Garage Squad loyalty
  • Build threads with "shop this build," YMM selector on the hero, tire/fitment tools
  • Watch hub with detail panel and admin YouTube shell
  • Policy, wholesale, military program pages: real copy, not lorem

The site reads like a culture + commerce destination, not a generic theme with a logo swap.

Suggested improvements (conclusion)

The repo is honest about gaps. Prioritized from audits and docs/inspiration.md:

Commerce (highest business impact)

  1. Finish Saleor checkout: CHECKOUT_COMPLETE, shipping methods, Stripe via Saleor; /checkout is still a placeholder.
  2. Cart line mutations when Saleor is enabled: remove and quantity today no-op in live mode.
  3. Variant-aware add-to-cart from listing cards: detail pages pass variantId; grid cards still rely on mock lookup.
  4. Collection product edges: homepage collection tiles need real product members, not metadata-only nodes.
  5. YMM → fitment filter: URL params should query Saleor fitment metadata, not just mock parts.

Content and SEO

  1. Ghost production mode: when GHOST_* is set, fail visibly or alert on API errors instead of silently showing mocks.
  2. OG / Twitter meta from Ghost meta_title / meta_description; structured data on PDPs.
  3. S3 + CloudFront: replace picsum placeholders; wire PUBLIC_CDN_BASE_URL for product and build photography.

Auth and community

  1. Complete OAuth providers (Discord, Microsoft) beyond Google stub paths.
  2. Newsletter + notify-me restocks → Supabase tables with RLS (forms exist in UI).
  3. Build moderation workflow: admin shell exists; connect submissions to Supabase and email.

UX and performance

  1. Motion pass: CSS + AnimatedReveal today; evaluate @motionone/svelte only where scroll-driven animation justifies the dependency.
  2. LCP / CLS audit on media-heavy routes before launch; lazy-load video embeds below the fold.
  3. Search v2: Saleor search API instead of fetch-100-then-filter for catalog scale.

Process

  1. Integration tests against staging Saleor + Ghost: env-gated in CI, not only unit tests on mappers.
  2. Single "integration health" admin page: green/red per hydrator (Saleor, Ghost, Supabase, YouTube quota).

Animal Garage is a useful reference for headless automotive retail: mock-first SvelteKit, env-gated hydrators, and clear audit docs so the next phase is integration depth, not re-architecture. The hard product work left is checkout, CDN media, and turning off silent mock fallbacks in production. Everything else is already sketched in routes and types.

If you are building something similar, fork the pattern before the stack: one hydrator per external system, one domain type per surface, mocks that tell the truth about shape. The animations and mega-menus are fun; the hydrator discipline is what keeps the site shippable.

Articles liés