Evoloop began as a student hackathon project and has grown into an independent footwear
project. This repository is its consumer-facing storefront front-end — landing page, /shop
browsing, product pages, a Markdown blog and a restrained, consumer-worded shopping assistant —
and the code is open source (MIT) with the long-term goal of evolving into a reusable
open-source shopping landing-site framework. All supplier-derived demo assets (photos,
catalog data, care poster, demo artwork) stay All Rights Reserved — fine to keep and run
in-repo as a demo, but not for redistribution without written authorization (see LICENSE +
LICENSE-ASSETS). Evoloop claims no trademark on its name.
Built with Next.js (App Router), Tailwind and shadcn/ui on the Node runtime (Bun is the package manager only) — with a server-side shopping assistant that stays understated: consumer wording only, never "AI"-branded.
The storefront has no real checkout: PDPs run a custom demo flow — demo price (placeholder
$59–79 band), size/color pickers and a Buy now CTA that opens a payment-QR checkout
dialog (a placeholder demo QR; no real payment is initiated). A Shopify Buy Button channel
(SHOPIFY_BUY_* env + handle map) is retained in the codebase but deliberately dormant — the
demo never configures it (a switchable catalog adapter also remains for a future direct Storefront
read). The catalog is a local seed of 29 real supplier styles (imported from the supplier's
workbook) with real product photos (WebP in public/products/); image-less entries fall back
to programmatic SVG visuals. /shop also carries the "spend $50, get a free gift" offer with a
gallery of leftover-offcut trinkets.
- Next.js 16.3.4 (App Router, Turbopack) + React 19 + TypeScript 5
- Bun ≥ 1.3 as the package manager only (the
nextCLI and every gate script run on Node) - Tailwind CSS v4 + shadcn/ui (Base UI preset)
- Drizzle ORM, dual-driver: SQLite (
better-sqlite3, default, zero-setup) or Postgres (postgres.js, Cloud SQL-ready) — chosen byDB_DRIVERin the environment - Vitest (unit + React Testing Library), ESLint,
tsc --noEmit - AI: OpenAI-compatible streaming client with a deterministic Mock mode when no key is set
- Bun ≥ 1.3 (project ships
packageManager: bun@1.3.14). Verify withbun --version. - No API keys are required to run the demo — the AI assistant works in Mock mode.
bun install
cp .env.example .env.local # defaults are fine — empty AI_API_KEY = Mock mode
bun run dev # http://localhost:3000Node runtime for Next. The
nextCLI runs on Node; the SQLite driver isbetter-sqlite3(Node native, works on Bun too) — no Bun runtime needed for dev/build/start. Bun is the package manager only.DB_DRIVER=postgres(with aDATABASE_URL=postgres://…) switches topostgres.js— both drivers run on Node, Vercel-ready.
First run auto-creates the schema (three tables: products + product_embeddings,
ai_usage) via idempotent CREATE TABLE IF NOT EXISTS on either driver — SQLite file at
./data/local.db, or the Postgres database behind DATABASE_URL. No migration step.
Catalog lives in the products table. The catalog adapter defaults to reading the table;
when it is empty the first request auto-seeds it from the import layer (supplier JSON +
curation). CATALOG_SOURCE=seed switches back to the pure in-memory import layer (used by unit
tests); SHOPIFY_* still takes priority over both.
- Landing page,
/shop(URL-driven filters), product detail pages (SSG, 29 products) - Wishlist (localStorage), size picker with market conversion (US system by default)
- Floating assistant (right-bottom "Need a hand?" FAB on non-landing pages):
chips
Find my size/Style it with/Help me pick/Everyday sneakers, streamed answers, product result cards, deterministic size recommendation — all in Mock mode. - PDP → "Find my size" opens the assistant pre-seeded with the current shoe.
| Command | Meaning |
|---|---|
bun run dev |
Next dev server (Turbopack) on the Node runtime. |
bun run build |
Production build (Node runtime). |
bun run start |
Serve the production build (Node runtime). |
bun run typecheck |
tsc --noEmit |
bun run lint |
ESLint over the repo |
bun run test |
Vitest (56 files, 294 tests) on Node via the better-sqlite3 driver. |
bun run verify |
One-shot acceptance gate: format:check + typecheck + lint + test. |
bun run test:watch |
Vitest watch mode |
The acceptance gate is format:check + typecheck + lint + test + build, all green on main (HEAD).
See .env.example for the annotated template. Summary:
| Variable | Default | Meaning |
|---|---|---|
SITE_MARKET |
US |
Market (US|EU|UK|JP|CN); drives the size-display system + mm-anchored conversions |
AI_API_KEY |
(empty) | Empty → Mock mode (zero cost, demoable). Set to enable the real OpenAI-compatible provider. |
AI_BASE_URL |
(empty) | OpenAI-compatible endpoint base URL (empty = official OpenAI) |
AI_MODEL |
gpt-5.6-luna |
Chat model for the real provider (2026-09: GPT-5.6 budget tier; quality-upgrade: gpt-5.6-terra) |
AI_EMBEDDING_MODEL |
text-embedding-3-small |
Embedding model for semantic search (cached locally) |
AI_MAX_TURNS |
20 |
Per-session turn cap (soft message when exceeded) |
AI_MAX_OUTPUT_TOKENS |
500 |
Max output tokens per provider response |
AI_REQUEST_TIMEOUT_MS |
20000 |
Provider request timeout |
AI_MAX_MESSAGE_CHARS |
800 |
Max characters per incoming user message |
AI_DAILY_TOKEN_CAP |
1000000 |
Daily token budget (SUM over ai_usage per UTC day) |
AI_DISABLE_REAL |
0 |
1 forces Mock mode even with a key (abuse kill switch) |
DB_DRIVER |
sqlite |
sqlite (default) or postgres — selects the app DB driver |
CATALOG_SOURCE |
db |
Runtime catalog source: db = products table (default, auto-seeded when empty); seed = in-memory import layer (tests); SHOPIFY_* still wins |
DATABASE_URL |
./data/local.db |
sqlite: local file; postgres: postgres://… connection string |
SHOPIFY_DOMAIN, SHOPIFY_STOREFRONT_TOKEN |
(empty) | Reserved. Catalog adapter switches seed → Shopify only when both are set (not yet active). |
SHOPIFY_BUY_DOMAIN, SHOPIFY_BUY_TOKEN |
(empty) | PDP Shopify Buy Button channel (2026-09): when both are set, every PDP mapped in src/server/catalog/shopify-buy.ts (29/29 store products, handle-keyed) renders a real Buy Button that takes over variant selection + checkout; unset keeps the demo pickers/price. Independent of the two vars above on purpose (setting those would trip the catalog stub). |
- Put a real key in
AI_API_KEY(optionallyAI_BASE_URLfor a gateway / custom endpoint). - Set
AI_EMBEDDING_MODEL+AI_BASE_URLto activate semantic retrieval; without them the assistant transparently uses keyword search over the catalog. - Restart. Guardrails (rate limit, turn cap, daily budget) apply to real and Mock alike.
AI_DISABLE_REAL=1is the one-switch rollback to Mock.
SITE_MARKET is a deployment-level, single-market setting (no runtime market switching):
sizes are stored once in a canonical EU system and converted to the market's system
(US/EU/UK/JP/CN, foot-length-mm anchored) for display, filtering and "Find my size". Currency and
copy stay English/USD.
src/
app/ # App Router pages & routes
page.tsx # Landing (zero AI presence by design)
shop/page.tsx # /shop — SSR list, URL-state filters (collection/size/price/sort/q)
product/[handle]/page.tsx # PDP — SSG (generateStaticParams), buy CTA placeholder
api/ai/chat/route.ts # POST SSE endpoint (delta|productCards|sizeFit|done|error frames)
og/route.tsx # Local OpenGraph image (ImageResponse, no network)
components/
marketing/ # AppBar, Footer, Hero, CollectionCards, Story, ...
shop/ # ProductCard/Grid, ProductVisual (SVG), size selector, wishlist, PDP cluster
assistant/ # FAB + Sheet chat panel, SSE hook, chips, message list
ui/ # shadcn/ui primitives
server/ # Server-only layers (never imported by client code except `type`)
catalog/ # Seed/DB adapter + market-aware service + Shopify Buy map (handle → store id)
search/ # embedder, keyword search, retrieval (embedding cache + cosine), vector
ai/ # providers (Mock/OpenAI-compatible), chat orchestration, SSE events, prompts
guardrails/ # rate limit, session state (turns/TTL/trim), token budget, soft copy
db/ # Drizzle dual-driver schemas (3 tables each: sqlite-core + pg-core), clients, product-row codec
lib/ # shared pure helpers (site, market, wishlist, size charts, formats, SEO)
test/ # test scaffolding (vitest setup + a11y axe gate)
- Catalog: runtime source is the
productsDB table — auto-seeded from the import layer (29 supplier styles across 4 collections, curation inseed.tsoverdata/supplier.json) when empty;/shop, PDP and search all read the table, so edits (title, price, collection…) apply on the next dynamic request.CATALOG_SOURCE=seedkeeps the pure in-memory layer for tests. Product cards and PDP galleries use real photos (Product.images, WebP underpublic/products/<handle>/) and fall back to SVG visuals only when image-less. A separategifts.tsmodule feeds the/shopfree-gift gallery (gifts are display-only, never in the sellable catalog). - Store buy channel (2026-09): the linked Shopify store holds the same 29 products under
matching handles.
src/server/catalog/shopify-buy.tsis a low-coupling static map (localhandle→ store numeric id, no Product/DB/schema changes) read by the PDP page; whenSHOPIFY_BUY_DOMAIN+SHOPIFY_BUY_TOKENare set, the store-mapped PDP hides the demo price/pickers and mounts one parameterizedShopifyBuyButton(SDKcreateComponentloading the admin-generated options verbatim), letting the store own variants, price and checkout. Unset → all PDPs run the custom demo flow (Buy now → demo payment-QR dialog, placeholderpublic/payments/checkout-demo-qr.png; swap in a live QR image for a real payment demo). - AI: RAG-lite, zero tool-calling — every real/gateway model only needs chat completions.
Retrieved product cards are injected into the system prompt; the model must answer from that
injected content only. Modes:
shopping,size-fit(deterministic),outfit,find-shoes, andsupport(store-policy Q&A from the sharedsrc/lib/store-policyfacts). - Guardrails (all anonymous, no PII): in-memory token bucket rate limit (IP + session),
per-session turn cap + history trim + idle TTL, output-token cap + timeout, and a persisted
daily token budget on
ai_usage. Soft copy everywhere ("taking a short break"), never "rate limited". Runs entirely in-process (single-instance assumption — adequate for this demo deployment; revisit before multi-instance hosting). - Search: embedding vectors cached in
product_embeddings(contentHash-validated), keyed byProduct.id(unchanged by the DB move), cosine in app code (fine below ~2k products — beyond that, move to a native vector backend).
- This site has no cart or checkout. The detail CTA is a demo Buy now that opens a
payment-QR dialog (
public/payments/checkout-demo-qr.png, a placeholder that never charges) to demonstrate the order flow; a dormantgetBuyUrladapter contract returnsnulland the dormant Shopify Buy Button only activates ifSHOPIFY_BUY_*env is ever configured. - Unknown product handles return the not-found UI with HTTP 200 +
noindexunder the currentdynamicParamsSSG setting (a deliberate, documented tradeoff; revisit if SEO on 404s matters). - Compliance: no cookies, no tracking, no personal data sent to AI providers. Anonymous
ai_usagetoken counts are stored locally for budget enforcement only.
Independent project (born at a hackathon); the order flow, pricing and copy are demo. Supplier product codes, photos, colors and size segments are real (from the brand supply-chain workbook); English marketing names, prices and copy are demo placeholders, as is the size conversion table (foot-length-mm anchored, canonical EU 35–48). The free-gift offer is a demo promotion. No real purchase flow is connected.
Source code is released under the MIT License (see LICENSE). All resource / demo assets
(supplier photos under public/products/, the supplier catalog import under
src/server/catalog/data/, hero image & care poster under src/assets/, blog posts under
content/blog/, demo artwork under public/) are All Rights Reserved and may not be
redistributed without written authorization (see LICENSE-ASSETS).