Nordfiord
Full-stack ecommerce platform for pharmaceutical-grade dietary supplements — three production repositories working as one system: a Next.js 15 storefront, a React 19 + Vite admin panel, and a NestJS REST API. Stripe payments, Redis cart, Cloudflare R2 storage, full RBAC, and GDPR-compliant user management.
Full Tech Stack
Overview
Nordfiord is a production-grade ecommerce platform for pharmaceutical-grade dietary supplements. It is built across three tightly integrated repositories — each with a clear responsibility boundary. The storefront handles the customer experience, the admin panel handles internal operations, and the NestJS API is the single source of truth for all business logic, data, and third-party integrations. Every piece of the stack was chosen deliberately: Redis for cart persistence and caching, Stripe for PCI-compliant payments, Cloudflare R2 for cost-effective object storage with presigned uploads that never touch the API server, and better-auth for session management with role-based access across all three apps.
Repositories
Storefront
Next.js 15 · App Router · TypeScriptThe customer-facing storefront built on Next.js 15 App Router with full SEO coverage — dynamic sitemap generated from live product slugs, robots.txt blocking private routes, per-product Open Graph metadata via generateMetadata, and canonical URLs on every page. Customers can browse products with multi-dimensional filtering (effects, ingredients, form, audience), manage their cart, authenticate via better-auth cookie sessions, and complete purchases through Stripe's hosted checkout flow.
- Cart hydration on page load: CartInitializer fetches Redis cart from the API and syncs it into Zustand — client state is always consistent with the server source of truth.
- Stripe Checkout Session flow with dynamic price_data — stock is only decremented after checkout.session.completed webhook fires, preventing overselling on abandoned sessions.
- generateMetadata per product detail page: fetches product name, short description, and primary image from the API to build accurate OG cards for social sharing.
- Sitemap auto-generated at build and runtime from live product slugs via the API — new products appear in search indexing without any manual step.
- Route protection via middleware: /cart, /checkout, /account, and /orders redirect unauthenticated users to /login with the intended destination preserved.
- Role enforcement: staff and admin accounts are blocked from the storefront at the middleware level — they are redirected to the admin panel.
- Axios instance with automatic .data unwrapping and shared error interceptor — API calls across all hooks are consistently handled without boilerplate.
Admin Panel
React 19 · Vite · TanStack RouterInternal dashboard for managing every aspect of the platform — products, variants, images, categories, taxonomy (effects, ingredients, forms, audiences), orders, users, and site settings. Built with React 19 and Vite for fast development iteration, TanStack Router for type-safe file-based routing, and Mantine UI v7 for a consistent design system. React Compiler is enabled, eliminating the need for manual useMemo and useCallback throughout the codebase.
- Full RBAC with two staff tiers: admin has unrestricted access; staff can create and edit products, view and manage orders, and ban/unban users — but cannot delete, change roles, or anonymize.
- Product editor with five dedicated tabs: Basic Info (name, slug, rich description, status), Relations (category, effects, ingredients, audiences), Variants (pricing in cents, SKU, stock, Stripe Price ID), Images (drag-and-drop R2 upload), and Science Facts (sourced research claims with source URLs).
- Image upload flow: frontend requests a presigned uploadUrl + publicUrl from the API, PUTs the file bytes directly to Cloudflare R2, then saves only the publicUrl to the database — the API server never handles file bytes.
- Order management with enforced status transitions: CANCELLED and REFUNDED are terminal states — the UI locks further actions, matching backend validation rules and preventing invalid state changes.
- GDPR user anonymization: admin-only action that irreversibly scrubs PII from the database. Requires typing ANONYMIZE into a confirmation input to proceed — designed to prevent accidental triggering.
- Real-time dashboard: revenue stats (all-time, today, this month, MoM growth %), order status breakdown, low-stock variant alerts, recent orders table, and a 30-day revenue + order count time series chart.
- React Compiler enabled across the entire codebase — automatic memoization of components and hook return values without any manual optimization annotations.
REST API
NestJS · PostgreSQL · RedisThe backend API powering both the storefront and admin panel. All routes are prefixed with /api and organized into domain modules: products, variants, images, categories, effects, ingredients, audiences, forms, science-facts, orders, cart, users, dashboard, and webhooks. Access is tiered into four levels — public (no auth), authenticated (any logged-in user), staff (admin or staff role), and admin (admin only) — enforced via a RolesGuard applied per route.
- Cart stored entirely in Redis with key cart:{userId} and 7-day TTL — cart data never enters the PostgreSQL database, keeping the relational schema clean and cart operations fast.
- Stripe product and price sync on creation: each product maps to a Stripe Product, each variant to a Stripe Price. Price changes archive the old Stripe Price and create a new one — Stripe's immutability constraint is handled transparently.
- Webhook handler at POST /api/webhooks/stripe processes checkout.session.completed (order → PROCESSING, stock decremented), checkout.session.expired (order → CANCELLED), and charge.refunded (order → REFUNDED or PARTIALLY_REFUNDED). Raw body parsing enabled in main.ts for Stripe signature verification.
- Presigned R2 upload endpoint: POST /products/:id/images/presigned-url returns { uploadUrl, publicUrl, key }. The browser uploads directly to R2, the server never receives file bytes. Bucket token uses minimum required permissions (Object Read & Write only).
- Dashboard endpoint returns a full analytics payload cached in Redis for 5 minutes: all-time and period revenue, MoM growth %, order counts by status, product and variant counts with low-stock breakdown, user counts by role, and a 30-day daily time series. Force-refresh available via POST /dashboard/refresh (admin only).
- Soft delete + restore pattern on products, users, and orders — nothing is hard deleted. Admin-only restore endpoints allow recovery from accidental deletions.
- One Stripe Customer per user created lazily on first checkout and stored as stripeCustomerId — subsequent checkouts reuse the existing Stripe Customer for consistent payment history.
Architecture
Three-repository structure: storefront (Next.js, port 5174), admin panel (Vite + React, port 5173), REST API (NestJS, port 3000) — each deployed independently with shared PostgreSQL and Redis.
Single PostgreSQL database accessed exclusively through the NestJS API — neither frontend app queries the database directly, enforcing a clean API boundary.
Redis serves dual purpose: cart session store (keyv with TTL, no persistence required) and API response cache (dashboard endpoint with 5-minute TTL and manual invalidation).
better-auth manages sessions for all three apps with a shared cookie domain. Three roles exist: customer (default, storefront access), staff (content management), admin (full platform access). Role assignment is admin-only.
Image pipeline: browser → presigned URL request to API → direct PUT to Cloudflare R2 → publicUrl saved to DB via API. The API acts as a coordinator only — no file bytes pass through NestJS.
Stripe integration: products and prices synced on creation, checkout via hosted Checkout Sessions, order state driven entirely by webhooks — the frontend never directly modifies order status.
Order status machine: PENDING → PROCESSING → COMPLETED → REFUNDED / PARTIALLY_REFUNDED. Also: PENDING or PROCESSING → CANCELLED. Terminal states (CANCELLED, REFUNDED) reject all further transitions at the API level.