# Build my own furniture retail store on Crystallize, like Furniture Universe

I want a storefront for my business that works like **Furniture Universe** (https://furniture-universe.superfast.shop), a demo store built on Crystallize: A high-end furniture store where the configurable pieces are the product: a modular sofa and a wall system built in 3D and priced part by part, with shoppable inspiration and a store that re-sorts itself to each shopper’s taste.

The content model and the wiring below are proven in that store: keep them for the features I want, and rename things to fit my trade. If a feature doesn't fit my business, leave its parts out rather than forcing it. Everything I see is mine to decide: brand, colours, typography, layout, copy and pages. Don't copy the demo's look; build my brand.

What the demo does: product configurator, shoppable content, vector personalisation, member pricing, multiple markets. It is B2C, in English, Norwegian, Swedish, Dutch; my markets and languages may differ.

## How to work with me

- You need to run commands, edit files and use MCP tools. If you can't (for example in a plain chat), tell me, and suggest I paste this prompt into Claude Code, Cursor or another coding agent instead.
- Ask your questions a few at a time (three to five), in the order of the steps below, and suggest a sensible default for each so I can just say yes. Ask each thing once, even where two lists overlap, and ask the ones that change the build first. Don't start a step before the one before it is done.
- The technical detail below is for you. Talk to me in plain words, and explain a choice only when I need to make it.
- Keep the folder empty until step 5, apart from `.env.local`.
- Never ask me to paste an access token secret into the chat, and tell me not to paste a filled-in command or its output either. Tell me which command to run or which file to put it in, and I'll do it.
- Before you create or change anything in Crystallize, show me the plan (shapes, pieces, topics, price variants, markets) and wait for my yes. Before an import, show me a sample of five items.
- Use the Crystallize skills (the `skills` MCP tool, or the use-crystallize plugin) for how-to: `content-model`, `information-architecture`, `taxonomy`, `data-creation`, `mass-operations`, `pricing`, `query`, `mutation`, `js-api-client`, `responsive-images`, `payments`, and when this store needs them `subscriptions`, `bookable-resources`, `vector-ranking` and `permissions`. Where this prompt and a skill disagree, the skill wins. Check the live schemas with the MCP instead of guessing field names.
- When something I asked for isn't possible in Crystallize the way I pictured it, say so and offer the closest thing that is.
- **The storefront never reads from the Core API on customer-facing pages.** Products, public prices, stock and content come from the Discovery API and the Catalogue API; confidential prices (negotiated B2B price lists) from the Catalogue API on the server.
- **Use the Shop API for everything about customers:** carts, checkout, customers, orders (including orders created directly, such as renewals), order meta, pipeline stages, payments and subscription contracts. Take the customer identifier from the server session, never from the browser. The Core API is for setup scripts, back-office tools, scheduled jobs and what the Shop API can't do, always on the server and never once per page view.

## Step 1: Crystallize tenant, tokens and MCP

Ask me:
1. Do I have a Crystallize account and a tenant for this store? If not, I sign up for free at https://app.crystallize.com/signup and create an empty tenant. Ask for its **tenant identifier**.
2. Do I have access tokens? If not, tell me: in the Crystallize App, go to **Settings → Access Tokens → Generate a new token**, name it, and keep the ID and the secret (the secret is shown once). A token acts with its user's role, so while we build, that user needs to be Tenant Admin (or have a role that can write shapes, pieces, topics, items and prices and run mass operations). Once the store is built, switch the MCP to a token from a read-only user.
3. Is the Crystallize MCP connected, with write access? Check that you have its write tools (`run-mass-operation`, `mutate-core`), not only the read tools (`tenant-overview`, `fetch-content-model`): with only the read tools, it was added without `?exposeWrite=true`. If it's missing, have me run this in my terminal with my own token, then restart you:
   ```bash
   npx add-mcp "https://mcp.crystallize.com/mcp?exposeWrite=true" \
     --header "X-Crystallize-Access-Token-Id: <token id>" \
     --header "X-Crystallize-Access-Token-Secret: <token secret>"
   ```
   If the MCP can't be connected in my tool, say so and fall back to scripts that call the Crystallize APIs with the tokens from `.env.local`.
   In Claude Code, also suggest the Crystallize skills: `/plugin marketplace add crystallizeapi/ai`, then `/plugin install use-crystallize@crystallize-ai`. The plugin brings its own read-only Crystallize MCP entry with placeholder credentials: keep using the one added above (and add `&exposeSkills=false` to its URL once the plugin's skills are installed).
4. Confirm the connection with `tenant-overview`, and tell me what's already in the tenant. If it isn't empty, ask whether to build next to what's there or in a fresh tenant.

The storefront needs the tenant identifier and its own credentials: an access token pair for the Shop API (server only), and a static auth token if Discovery and the Catalogue API are secured. Don't reuse the admin build token in production. Put them in `.env.local` (have me fill in the secrets) and make sure `.env*` is in `.gitignore`.

## Step 2: my business and my brand

Ask me:
- What I sell, who buys it, and what makes us different. My brand name and a one-line pitch.
- My markets, currencies and languages, and my VAT/tax setup.
- Where my product data lives today: a spreadsheet or CSV, an export from Shopify, WooCommerce or another platform, a PIM or ERP, my current website, or nothing yet. Where my product images are.
- My brand: logo (SVG if I have it), colours, typefaces, tone of voice, and two or three sites whose look I like. If I have no brand guide, propose a direction from my answers and show it before building pages.
- Where it should run: my own domain, and hosting (suggest Vercel for Next.js).

Then ask what is particular to this kind of store:

- Which of my products are configurable, and how: modular pieces joined in a line or around corners (like a sofa), parts hung on a grid (like a wall system), or just picking options on one product?
- The rules: which parts connect to which, left and right versions, corner angles, which ends must be closed, what requires or excludes what, and the limits (size, load).
- How the price is built: per part, per fabric or finish group, surcharges, and a discount on ready-made setups?
- **Do I have 3D models?** In which format, units and origin, and how big are the files? If I don't, go through the options below with me and help me pick one:
  - The manufacturer: many supply CAD or configurator exports (STEP, FBX, OBJ). They need converting to GLB in Blender, reducing in detail, and re-scaling and re-origining to the conventions below.
  - A 3D artist, commissioned with the conventions below as the brief. The most reliable route for a range that must snap together.
  - Built from dimensions in code or Blender, as the demo did: simple, parametric shapes, good for modular upholstery and panels.
  - Photogrammetry for a hero piece: realistic, but needs heavy clean-up and doesn't suit modular parts.
  - Marketplaces (Sketchfab, CGTrader, TurboSquid) for props and accessories only: check that the licence allows commercial use in a configurator, then re-scale and re-origin.
- My materials and finishes: how many, and do I have texture scans or swatches with their real-world size? If not, CC0 texture libraries (Poly Haven, ambientCG) or my suppliers' swatch scans work.
- My markets, currencies, VAT and languages; delivery or click & collect; made-to-order lead times.
- Member or trade prices, campaigns, and quotes through a sales desk?
- Do I want AR (seeing the piece in the room on a phone)?
- Ready-made setups to sell as named products with their own photos?

## Step 3: the content model

Create this in my tenant with the MCP, renamed to fit my trade, after I have approved the plan. Keep the structure: it is what the storefront's features depend on.

Build in dependency order, since mass operations run in sequence with no rollback: pieces and shapes (empty first, then their components), topic maps (at most 30 topics per `topic/create`), price variants, markets and VAT types, the folder tree (two to four levels, five to twelve per level), and only then items. Use lowercase-hyphen identifiers. Decide now which fields are translated and which are discoverable. Restrict every item relation with `acceptedShapeIdentifiers`. Never add price or stock components to a shape: variants have them built in.

A new configurable product is a new `specs` choice on these shapes, never a new shape.

- Pieces:
  - `model-3d`: a `glb` files component, `mirrored` boolean, `thumbnail` image, notes.
  - `compatibility`: requires and excludes relations, `accepts-arm`.
  - `dimensions`: width, depth, height (cm).
  - `merchandising`: launch date, sold in 30 days, margin, rating, review count, campaign boost, staff pick, hide from listings.
- **module** (product, one buyable part): description, `options` relation, compatibility, seo, brand, designers, gallery, merchandising, and `specs` as a component choice:
  - `sofa-module`: module type (seat, corner, end, chaise, pouf, arm), run role (seat, corner, terminator, standalone), turn angle, pocket-spring surcharge, seat height.
  - `wall-part`: role (wall panel, floor panel, shelf, cabinet, desk, rail), the system it belongs to, hook offsets, collision height, max load, surface height, depth class.
  - Variant components: `dimensions`, `model-3d`, `colour`.
- **setup** (product, a ready-made configuration): tagline, summary, description, option slots, series, brand, designers, materials and care, product facts, pairs with, gallery, and `specs` (seating, or a wall setup with mount, bays and a `wall-layout` chunk of which module sits in which bay at which height). Variant: the fabric, dimensions, model and colour. The `recipe` (repeatable chunk: setup SKU, position, module, quantity) lives on the product, one row per variant, because variant chunks may not publish.
- **series** (folder, a product family): description, hero, brand, designers, and `specs`: for a sofa, its fabrics and option slots (type, min, max, allowed, default); for a wall system, its grid, panel thickness, bay widths, depth classes, max bays, first and last slots, clearance, load per section, and finishes (code, name, swatch colour, materials).
- **fabric** (product, a colourway family): price group, fabric type (bouclé, velvet, woven, leather), material relation, a spec chunk (Martindale, pilling, colour fastness), care. Variants are colourways: colour code, hex colour, and a `material-3d` piece (tint, colour map, normal map, roughness map, weave scale in mm).
- **material** (document, a PBR texture set): material model (standard, sheen, leather), albedo, normal, ARM (occlusion, roughness, metalness) and roughness maps, tile size in mm, roughness and normal scale, sheen, source and licence.
- **option** (product): comfort, armrest or leg details, description, model, compatibility.
- Retail: **furniture** (product with a `specs` choice for seating, tables, storage, lighting, textiles, accessories; variants with colour, physical size and model; GTINs in a product-level identifiers chunk, one row per SKU), plus **brand**, **designer**, **store** (address, location, hours, services, market), **collection**, **article**, **landing-page** (blocks and targeting), **category** (folder), **care-instruction**.
- Catalogue: `/sofas/<series>/{modules,setups,options}` and the same for wall systems, plus fabrics, materials, care, brands, designers, stores, inspiration, pages, and retail categories (seating, tables, storage, lighting, textiles, accessories).
- Topic maps in three taste groups: style (brand, collection, designer, style), home (room, material, colour), product (product type, price tier, feature). Images are topic-tagged too, with `updateImage(key, language, { topicIds })` once per language.
- Pricing: net prices, one price variant per currency. Fabric price groups are absolute price lists (`group-a`, `group-b`…; `SOME_SKUS` with a modifier per SKU). Campaigns are dated percentage lists aimed at a market or everyone. The member discount is a percentage list with `targetAudience: { type: SOME, customerIdentifiers: [...] }`: customer groups can't be targeted. `updatePriceList` replaces the whole SKU selection.
- Stock: an online warehouse plus one location per store for click & collect; made-to-order pieces are delivery only.
- Order pipelines: Quotes (new, sent, in dialogue, accepted, lost) and Orders (new, in production, shipped, delivered).

## Step 4: my data

Import my products and content with mass operations, mapping my data onto the model: validate each file with `build-mass-operation`, run it with `run-mass-operation` and follow it with `get-mass-operation-status`. Read the operation logs, not just the task status: a task can finish as complete with failed operations, and there is no rollback. Give every upsert a `resourceIdentifier` so a re-run updates instead of duplicating, and chunk large imports. Start with a small batch, check it in the Crystallize App with me, then run the rest.

Publish every imported item in each language: the storefront only sees published versions (in a mass operation, `item/publish` needs real `itemId`s). Import images from URLs with `copyRemoteAsset`, or upload them (presigned upload, then `registerImage`). Renditions are generated in a queue: after a large import, republish once they exist, and have the storefront fall back to the original image URL while `variants` is empty. Before building pages, query Discovery once; if the tenant isn't ignited yet, run `igniteDiscoApi` (with `stacks: opensearch` if we use vector ranking), wait for the task to complete, and allow a few minutes. If I have no data yet, create a small, clearly labelled placeholder set (ten or so products) so we can build the pages, and remind me to replace it.

- The demo's models are all generated in code (Python with trimesh): 26 sofa GLBs of 41–184 KB each, and 22 wall GLBs, 730 KB together. Tests fail if a model's bounding box doesn't match the catalogue's dimensions; do the same.
- **The conventions my models must follow:**
  - GLB (binary glTF 2.0), in metres, Y up.
  - The back of the piece at z = 0, the body toward +z.
  - Sofa modules: origin at back-left-bottom. Wall parts: a panel's origin at its left face and bottom edge; a hung item's at the inner face of the left panel, at its hang slot.
  - Model the right-hand version; the left is a mirror (or its own file).
  - One GLB per geometry. Finishes and fabrics are named materials swapped at runtime (e.g. `Upholstery`; `Panel`, `Board`, `Front`, `Hardware`).
  - Closed meshes, outward normals, and one UV scale in real-world units (the demo uses 15 cm per UV unit) so fabric textures show at their true size.
  - Budget: roughly 50–200 KB per module, 2048 px textures (albedo, normal, packed ARM).
- Fabric and wood textures in the demo come from Poly Haven (CC0), with tile size and author recorded.
- From me: my configurable range with its rules and prices, 3D models (or a plan to get them), materials and finishes with swatches, stores, and photos.

## Step 5: the storefront

- A Next.js storefront (App Router, Vercel), with the configurator as its own Vite + React + three.js app, served on the same domain through rewrites (`/<lang>/configure`). AR through `@google/model-viewer`.
- The configurator's small server is the only holder of the token; the browser never calls Crystallize. It serves one catalogue snapshot (modules, setups, fabrics, options, price lists, wall systems), cached for 5 minutes: from the Discovery API (products with their variants, dimensions and models) and the Catalogue API (the price lists).
- One shared engine package (plain JS: layout, rules, pricing, markets) runs the same in the browser, on the server and in tests.
- **Sofa snapping:** each module has an entry and an exit connector (a point and a heading). A sofa is a run, an ordered chain of SKUs where each exit meets the next entry; the turn angle (0, 45, 90, −90) turns the heading. Terminators (arm, open end, chaise) close the ends; poufs stand alone. `canAttach()` gives a reason when it refuses. Handedness comes from a `Side` attribute; an arm is modelled for the right end and mirrored at the start.
- **Wall snapping:** a 4 cm grid; sections of one to six bays (60 or 90 cm) between shared blades; an item hangs in a bay at a slot whose hook offsets exist on both blades; its depth class must match; no overlaps; desk surface 72–76 cm; warnings for tight clearance and heavy wall loads.
- **Price, part by part:** each module's price comes from the chosen fabric's price-group list in the market's currency, plus surcharges. A run that matches a ready-made setup is priced as that setup. Then member and campaign discounts apply. Walls are the sum of their parts. The server re-prices every cart line and sets it with `changeCartItemPricing`.
- **Shop API** for carts, checkout and orders. A shared configuration is a cart of type `wishlist` with the configuration in its meta. Quotes are Shop `/order` orders of type quote, created with `pipelines: [{ identifier: "quotes", stage: "new" }]` and moved with `addToStage` (never Core `updateOrder`); accepting one creates a normal order.
- **Discovery API** for the storefront's catalogue reads. Per-SKU price lists (fabric groups, member and campaign lists) come from the Catalogue API on the server: a whole list via `priceList(identifier) { productVariants(language, first) { … priceVariant(identifier) { priceList(identifier) { modifier modifierType } } } }`, stopping on an empty page, or a member's price via `priceVariant { priceFor(count, customerIdentifiers: [...]) }` (at most 150 SKUs a call). Discovery resolves market-targeted lists only.
- **AR:** rebuild the configuration off-screen, export a GLB in the browser (textures capped at 1024 px), and open it in model-viewer (WebXR and Quick Look, fixed scale, on the floor). Desktop shows a QR code.
- Uploads: GLB and PDF files through a presigned STATIC upload, video as MEDIA, then attach the key to the component.
- Env: `CRYSTALLIZE_TENANT_IDENTIFIER`, `CRYSTALLIZE_ACCESS_TOKEN_ID`, `CRYSTALLIZE_ACCESS_TOKEN_SECRET`, a shopper session secret, and the configurator's origin and API URL.

Build my design, not a demo's: my typefaces, colours and tone, with pages that suit my products. Show me the home page, a category and a product page early, and adjust the design with me before building the rest.

Every page is built for search engines, answer engines and AI agents from the start, not added at the end:
- **SEO:** server-rendered HTML, one `<h1>`, a unique title and description, canonical URLs, `hreflang` per language, a sitemap and `robots.txt`, and clean, translated paths.
- **Structured data:** JSON-LD on every page that has a subject: `Organization` and `WebSite` (with a `SearchAction`) on the home page, `Product` with `Offer` (price, currency, availability) on product pages, `BreadcrumbList` on categories and products, `Article` or `Recipe` on content. Generate it from the same data as the page.
- **GEO, for answer engines and agents:** `/llms.txt` (an H1, a summary, links to every section), `/llms-full.txt`, and a Markdown twin of every page (its URL plus `.md`, or `Accept: text/markdown`), with prices, stock and facts written out in plain sentences.
- **Agents can act, not only read:** register the store's actions (search, add to cart, check availability) with WebMCP (`navigator.modelContext.registerTool`, or annotated forms), and keep the accessibility tree clean: semantic HTML, labelled controls, real buttons and links.
- **Fast and stable:** responsive images from Crystallize, no layout shift (reserve space for images and late content), little client JavaScript. Aim for 100 in every Lighthouse category (Performance, Accessibility, Best Practices, SEO; heavy 3D or video pages may score lower on performance) and pass every Agentic Browsing check.

## Step 6: run it, then ship it

Run it locally and walk me through each feature above with my own data. Fix what I find. Before you call it done, run Lighthouse on the home page, a category and a product page (`npx lighthouse <url> --only-categories=performance,accessibility,best-practices,seo,agentic-browsing`), validate the JSON-LD (https://validator.schema.org), open `/llms.txt` and a page's `.md` twin, and fix what falls short. Deploy only when I ask: set the same environment variables on the host, point my domain, and give me the live URL.

## Watch out for

- The Catalogue API can come back with no variants for these products: read variants from Discovery.
- The mass-operation runner can say "completed" when operations failed: read every operation's log. Files can't go through mass operations: upload them, then attach the key.
- `updateComponent` replaces a whole piece: read, merge, write (updating a thumbnail once wiped the GLB).
- Handedness is the most common mistake: define right and left seen from the front, in one table, and test the geometry.
- A product can't change shape in place, and SKUs are unique across the tenant.
- Image renditions for swatches are small; use them.
- The Shop API applies price variants and the lists aimed at the cart's customer or markets, but can't know the fabric group or surcharges: set each configured line with `changeCartItemPricing` (the line becomes unmanaged).
- Discovery ranking needs ranking enabled on the tenant (probe `__type(name: "RankByInput")`) and `igniteDiscoApi(stacks: opensearch)`, polled until complete. Boost only on fields in the introspected `TenantRankByField`; numbers you facet on are excluded.
- The 3D scene, AR export and thumbnails stall in background tabs.
- A material's roughness multiplies the ARM map's green channel; the GLB's UV scale and the scene's must match.
- Don't invent facts about my business (prices, stock, certifications, delivery promises). Ask, or leave a clearly marked placeholder.

And for every Crystallize store, from the skills:
- **Orders:** create and change them through the Shop API `/order` (`createFromCart`, `create`, `setMeta` with `merge: true`, `addToStage`, `setPayments`). Editing an order in Core adds a duplicate to the Shop API's list. Read a customer's orders with the identifier from the session, `limit: 100`, grouped by `coreId` (keep the newest). Seed order history through `/order` `create`, not Core `registerOrder`. The order id is the cart id; don't call `fulfill` after `createFromCart`.
- **Payments:** create the order from the payment provider's verified webhook, never from the browser, once per cart.
- **Cart meta** goes in `hydrate(input: { meta })`; `hydrate` is the whole cart, so send every line each time.
- **Images:** serve them straight from Crystallize (not through an image optimizer such as `next/image` in its default setup): a WebP `srcset` from the returned `variants` with an accurate `sizes`, falling back to the original URL while renditions are missing. Replace an image by uploading a new one, not with `registerImageRevision`.
- **Ranking (if the store ranks per shopper):** check that `__type(name: "RankByInput")` exists; pass `context` as a variable, with `userTaste` entries of `{ vocabulary, weights, magnitude }` (magnitude = the square root of the sum of squared weights); `tieBreaker` is required; don't combine `sorting` with ranking, and page with `skip` and `limit` inside the rerank window; introspect `TenantRankByField` for field names; leave `context` out when there's no taste; use `explain: true` only for tuning.
- **Bookings (if the store books):** durations are in seconds; the role needs the `bookingPolicies` permission; put the customer on the cart before `bookSkuItem`; every `hydrate` must resend each booked line with its window and unit, or the booking is cancelled; remove a booking by re-hydrating without it.

## Done when

- My tenant has the content model above with my products, prices and images in it.
- The storefront runs on my domain in my brand, and every feature I chose works with my data: we have clicked through each one together.
- Lighthouse scores close to 100 in every category and every Agentic Browsing check passes on the home, category and product pages.
- I know how to add a product, change a price and publish, in the Crystallize App.
