Build yours with AI

A store like Spirits Universe, with your brand

Copy this prompt and paste it into a coding agent (Claude Code, Cursor or Codex) in an empty folder. It asks for your Crystallize tenant, your products and your brand, then builds the store on your own tenant.

Raw Markdown

I want a storefront for my business that works like Spirits Universe (https://spirits-universe.superfast.shop), a demo store built on Crystallize: A drinks supplier to restaurants, bars, shops and importers, where the range follows the buyer’s licence and country: deposits and kegs out, export documents, cost per serving and margin at the venue’s menu price — and its customer, Wine and Dine Universe, is another Crystallize store.

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: subscriptions, native booking, time-slot bookings, invoice and credit limits, buyer roles, approval flows, standing orders, multiple markets, multi-currency, customer price lists, vector personalisation, explainable ranking, typed specs and filters, shoppable content, campaigns. It is B2B, in English, Swedish, Norwegian, 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:
    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 countries and currencies do I sell in? Do my list prices include alcohol duty, and do I export duty-suspended?
  • Which licence rules decide what a customer may see and buy (alcohol-free for guests, a low-strength threshold for grocery, full range for licensed venues and importers)? The thresholds per country.
  • Which deposit and return schemes apply per pack and country, and the amounts? Do I take back empties and kegs?
  • My pack formats (kegs, crates, cans, returnable and one-way glass, bag-in-box) and case and pallet sizes.
  • Contract pricing: a percentage per chain, tiers, dates it's valid between?
  • Services I sell (tap installation, line cleaning, sommelier, tastings): hosted by people, or sold from a capacity?
  • Subscriptions: campaign content packages, regular line cleaning, standing orders, and at which intervals?
  • Where my product facts and taste data come from: my own PIM, a spreadsheet, a public registry such as Systembolaget's assortment?
  • Do my customers publish their menu prices (for margin per serving), or will they enter them? Do any of them run Crystallize too? (Then the two stores can sync, as Spirits Universe does with its restaurant customer.)

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.

Numerics carry the unit in their identifier (no unit lists); everything shown is discoverable; chunks with translated text are multilingual.

  • Pieces:
    • seo; alcohol: ABV, licence class (free, low-strength, licensed, derived from ABV), licence required, alcohol-free twin (relation).
    • taste: body, bitterness, sweetness, acidity, roast (1–12, the taste-clock scale), aroma, where the taste data came from.
    • serving: min and max temperature, glass, pour (cl), tip. ingredients: ingredients, allergens (EU 14), vegan, organic. sustainability: CO₂e per litre, recyclable packaging.
    • One spec piece per family: beer (IBU, EBC, fermentation, hops, malts), cider, wine (colour, sparkling, grapes, vintage, sweetness), spirit (type, cask, age), soft drink, water.
    • kit for campaign content: headline, intro, social posts (repeatable: text, image, suggested date, channel), menu card lines, pairings, files and images, usage notes.
  • drink (product):
    • tagline, summary, story, highlights; origin chunk (brand, producer, country, region); specs component choice of the family pieces; the alcohol, taste, serving, ingredients and sustainability pieces; pairing (serve with recipes); merchandising chunk (sold in 30 days, margin, rating, campaign boost, pairs with); seo.
    • One variant per pack, SKU = article number. Variant components: pack type, volume (cl), units per case, deposit relation (max 2), deposit scheme, markets (multi-selection of the countries it's sold in), servings, pallet. GTINs (unit and case) go in a product-level chunk, one row per SKU.
  • deposit (product): container (kegs of 20, 30 and 50 litres, bottle, crate, can…), scheme, markets, returnable. Priced per scheme.
  • service (product, bookable): type, duration, seats, price per, hosts, featured drink.
  • campaign-plan (product): what's included, print, events per campaign. Sold as a subscription.
  • Documents: brand, producer, person (hosts and contacts), recipe (cocktail, mocktail or dish; ingredients that relate to drinks), article, campaign (dates, featured drinks, a bundle with a discount, the kit piece). Folder: category.
  • Catalogue: /<category>/<subcategory>/<drink> (beer, cider, alcohol-free, wine, spirits, soft drinks, water, mixers), plus services, campaign content, campaigns, people, inspiration and insights.
  • Topic maps: category, style, occasion, cuisine, dish, origin, seasonal, image type.
  • Vector vocabularies: taste (the five taste dimensions as high or low keys, centred on the catalogue's average), menu (cuisine, dish, occasion), range (category, style, origin).
  • Pricing: excluding VAT; a home-market list price including duty, and export price variants (duty-suspended) per currency, with volume tiers per full pallet. VAT types per country.
  • "Market" lives in the customer's meta and on each variant's markets selection, not as Crystallize markets.
  • Contract prices: dated percentage price lists per SKU, targeted at the group customer; its venues inherit them.
  • Customers: organizations with venues as child customers and people as individuals under them. Meta: business type, country, market, licence type, number and expiry; a person's role.
  • Subscription plans: campaign content (a season or a year), monthly line cleaning, standing orders (weekly, every two weeks, monthly).
  • Order pipelines: Orders (received, picked, out for delivery, delivered, empties returned), Export, Services.

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 itemIds). 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 holds about 400 drinks in 984 packs, 193 brands, 152 producers and 12 deposits. Facts come from the Swedish Systembolaget assortment and the brand owners' sites, collected as research files and built into catalogue JSON. Customers, orders and campaigns are fictional.
  • Pack shots were copied into Crystallize; campaign photos come from Unsplash with credit.
  • From me: products with ABV, packs, volumes and case sizes, GTINs, prices per currency, deposit schemes and amounts, my market's licence rules, customers and contract discounts, services with hosts or capacity, and images I have the rights to.

Step 5: the storefront

  • Next.js (App Router) on Vercel, plain fetch GraphQL clients. Sign-in in the demo is a persona picker; real customers need real authentication.
  • Discovery API: listings, facets, search, products, campaigns, services (with their booking pool), plans, and taste ranking (rankBy with one tasteCosine term per vocabulary and an introspected tieBreaker, plus context.userTaste with a magnitude per vocabulary; explain: true only behind a debug flag).
  • Range by licence and country: Discovery filters on ABV and licence class, plus the variant's market. The cart action refuses what the customer may not buy too. Read the customer's licence type, number, expiry and market once at sign-in and keep them in the signed session: listings and the cart read the session, never Core. Guests see alcohol-free only, without prices on alcohol.
  • Contract prices from the Catalogue API on the server (priceFor(count, customerIdentifiers: [org]), at most 150 SKUs a call, or the whole priceList(identifier)), cached per organization; Discovery doesn't resolve per-customer lists. The cart resolves the same list from its customer.
  • Drinks cart (Shop API): written whole with hydrate: drink lines, one deposit line per unit, and the bundle discount as an external line. Cart meta: venue, licence number, delivery day. Returned empties are a negative external line on the next order. place, then create the order.
  • Services cart (separate, since hydrate replaces a cart): availability (with the item's language), then bookSkuItem with the customer already on the cart: a hosted service books quantity 1 plus the host's unit (also written into the line meta), a capacity service books quantity = seats with no unit. Check __typename. Then place → optional confirmCartBooking → payment → /order createFromCart → poll the cart's order id → confirmCartBooking again. If payment fails after the first confirm, cancel on /booking/admin. Extra seats as an external line.
  • Subscriptions through the Shop API /subscription-contract (experimental; the JWT needs subscription-contract and subscription-contract:admin), on the server for the signed-in customer: create, pause, resume and cancel. A different plan or period is cancel plus a new contract; a different basket is an update to the recurring phase from the next renewal. A standing order is one contract whose recurring.productVariants lists every line. Nothing renews by itself: a scheduled job writes each delivery or invoice with Shop /order create (type recurring, the contract id on the line), then renews. Pausing freezes no dates: on resume, push them forward. Never create contracts for these customers in Core (the two barely sync).
  • Margin at the menu price: servings = volume × units ÷ pour; cost per serving = contract unit price ÷ servings; margin = (menu price excluding VAT − cost) ÷ menu price excluding VAT.
  • Ranking per venue: the venue's taste, its cuisines, dishes and occasions, plus a range vector from its order history.
  • Sync to a customer's store (optional): webhooks on item publish and order create, with a shared secret header, post to the customer store's sync endpoint. The supplier never writes into the other tenant.
  • Env: CRYSTALLIZE_TENANT_IDENTIFIER, CRYSTALLIZE_ACCESS_TOKEN_ID, CRYSTALLIZE_ACCESS_TOKEN_SECRET, and the customer tenant's identifier if they sync.

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

  • Selections filter by exists on each option's field, not by label. Filter, facet and sort fields follow {componentId}_{subfield}_{type}, and a hyphen before a digit becomes _ (sugar-g-per-100ml → sugarGPer_100ml): introspect the generated names.
  • Rich text isn't discoverable by default; numerics with a unit list become required.
  • No content chunks on variants. updateProductVariant without components empties them; updating with components replaces all.
  • Relations resolve once the target is indexed: publish targets first, then republish the items that point to them, per language.
  • Re-index Discovery after shape changes. Discovery caches per query body, errors included.
  • Taste cosine needs centred keys (<dim>:high|low), or everything scores about 1.
  • Price lists target customers, not customer groups.
  • Bookings: customer on the cart before the hold; one booking write at a time per cart (two concurrent bookSkuItem calls lose one). Drop a booked line by re-hydrating without it (not cancelReservation or removeCartItem). Cancel a reservation on a placed or ordered cart on the server through Shop /booking/admin (cancel(id, reason), scopes booking and booking:admin).
  • Negative external lines need variant.product.
  • Subscription contracts: send renewAt and activeUntil together (with only renewAt a contract is born cancelled); dates can't be in the past, so back-date signedAt and the orders instead; updating meta replaces all of it.
  • Webhook concerns are plural (items/publish); a failing webhook query means no call; updateWebhook resets the query target if you leave it out; createWebhook accepts any concern or event string without complaint, so confirm each webhook arrives.
  • Multi-word search can hit OpenSearch's clause limit: query per word.
  • 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.