--- id: intro slug: / title: Overview --- # Nimara Documentation Welcome to the Nimara documentation! This guide will help you set up and deploy your e-commerce storefront using Nimara, a modern headless e-commerce platform built on top of [Saleor](https://github.com/saleor/saleor). Made with the latest version of Next.js, TypeScript and React Server Components, Nimara is a modern and performant e-commerce storefront. ## Getting Started To get started with Nimara, explore the **Quickstart** section which contains step-by-step guides for: - **[Running locally](/Quickstart/running-locally)** - Set up Nimara on your local development environment - **[Storefront](/Quickstart/storefront)** - Deploy your storefront to production - **[Environment variables](/Quickstart/environment-variables)** - Configure your application settings - **[Saleor CMS](/Quickstart/saleor-cms)** - Manage your content with Saleor's CMS features - **[Add new channel](/Quickstart/add-new-channel)** - Add locales, languages, and markets - **[UCP integration](/Integrations/ucp-integration)** - Universal Commerce Protocol for AI agents and platforms - **[Stripe integration](/Integrations/stripe-integration)** - Set up payment processing - **[Marketplace](/Advanced/marketplace)** - Set up the vendor marketplace app - **[Terraform](/Advanced/using-terraform)** - Infrastructure as code deployment - **[i18n](/Advanced/i18n)** - Internationalization architecture and message bundles - **[Developer Onboarding](/onboarding)** - Architecture tour, repo layout, dev workflow - **[Release Workflow](/release-workflow)** - How releases are cut and versioned Choose a guide from the menu to begin! --- --- id: onboarding title: Developer Onboarding --- # Developer Onboarding Welcome. This guide explains **why Nimara is built the way it is**, how the code is organized, and how you'll work in it day to day. Read this when you want to contribute, not just run a store. :::tip Just want to run a store? Start with [Running Locally](/Quickstart/running-locally) and [Storefront](/Quickstart/storefront). Come back when you want to write code. ::: ## What Nimara is for Nimara is an **open, composable commerce platform** for teams who want SaaS-level speed without SaaS-level lock-in. The goal is simple: - **Get a real storefront running in minutes** — even before you have a backend configured. - **Own every line of code.** No closed cores, no required upgrades. - **Swap any integration** — commerce backend, CMS, search, payments — without rewriting the app. - **Grow from one storefront** into a multi-app monorepo (vendor panel, admin, internal tools) on the same foundation. If a piece of the codebase ever feels over-engineered, ask _"could I swap this for something else next year?"_ That's usually the reason. ## Prerequisites | Tool | Version | Notes | | ----------- | ---------- | ---------------------------------------------------- | | **Node.js** | `24.x` | Pinned in `.nvmrc`. Run `nvm use`. | | **pnpm** | `9.x` | Only supported package manager. | | **Git** | any recent | Fork + upstream workflow (see CONTRIBUTING). | | **Docker** | optional | For a local Saleor or marketplace ledger/LocalStack. | A **Saleor backend** is optional for a first look but needed for real work. A free [Saleor Cloud](https://docs.saleor.io/cloud/overview) developer account is the fastest path. ## Get it running ### Fork and clone ```bash git clone https://github.com/{your_github_username}/nimara-ecommerce.git cd nimara-ecommerce git remote add upstream git@github.com:mirumee/nimara-ecommerce.git ``` ### Install ```bash pnpm install ``` Installs everything across the workspace. ### Optional: connect a backend ```bash cp apps/storefront/.env.example apps/storefront/.env ``` The storefront is **zero-config** — skip this step and it still boots with empty data, useful for a first look. To talk to a real Saleor, edit `apps/storefront/.env` and set: ```properties NEXT_PUBLIC_SALEOR_API_URL=https://{your_domain}.saleor.cloud/graphql/ ``` Each extra variable turns on another capability (payments, marketplace, server-only resources). Run `pnpm preflight --report` at any time to see what's currently on or off. ### Run it ```bash pnpm dev:storefront ``` Opens on [http://localhost:3000](http://localhost:3000). For webhooks and deployment, see the [Storefront guide](/Quickstart/storefront). ## How the codebase is organized Nimara is a **layered monorepo**. Each layer has one job and only depends on layers below it. The rule sounds boring, but it's the reason you can swap backends and run zero-config: each piece only knows what it needs to know. Each layer may use anything below it; nothing reaches up. What each layer is for: - **`domain`** — what your business cares about: types, rules, error shapes. Doesn't know about React, GraphQL, or any backend. - **`infrastructure`** — the only place that talks to the outside world (Saleor, ButterCMS, Algolia, Stripe). Every external service hides behind a domain-typed contract. - **`foundation`**, **`ui`**, **`i18n`** — shared building blocks: hooks, design system, translations. - **`features`** — full vertical slices (checkout, search, account) ready to drop into an app. - **`apps/`** — what users actually see: the storefront, marketplace, payment apps. The full tour of every folder and the deeper conventions live in [`AGENTS.md`](https://github.com/mirumee/nimara-ecommerce/blob/main/AGENTS.md). ## The swap-anything promise Saleor is the default commerce backend, but **nothing in the storefront or features layer knows that.** Every external system — backend, CMS, search, payment gateway — sits behind a domain-typed contract in `infrastructure`. The repo already proves this works: CMS pages have both a Saleor and a ButterCMS provider; search has both Saleor and Algolia. What this buys you: - **Change a CMS, not your codebase.** Adding a new provider means writing one adapter for that one capability. Your UI and feature code don't move. - **Run before you commit.** No env vars? Every service returns empty data, the UI still renders, nothing crashes. You can show the project to a teammate in 60 seconds. - **Errors look the same everywhere.** Every operation returns a `Result` — no special path for "Saleor error" vs "Stripe error". - **Pay for what you use.** Backend SDKs (`buttercms`, `algoliasearch`, …) are optional peers. If you don't use a provider, it's not in your bundle. If you want to see exactly how this is wired in code, the cleanest example is the `cms-page` capability in `packages/infrastructure/src/cms-page/` — two providers, one shared port. The deeper write-up lives in `AGENTS.md`. ## How features turn on The storefront boots **empty by default** and lights up as you add environment variables. One switch turns on most of the commerce surface; payment and marketplace are independent. | When you set… | You get… | | -------------------------------------- | ------------------------------------------------------------------------------------- | | Nothing | Every page renders with empty data — great for design work or showing teammates. | | `NEXT_PUBLIC_SALEOR_API_URL` | The full commerce surface — cart, checkout, products, search, account. | | `NEXT_PUBLIC_MARKETPLACE_ENABLED=true` | Vendor-aware behaviour for marketplaces. | | `NEXT_PUBLIC_PAYMENT_APP_ID` | Payments. | | `SALEOR_APP_TOKEN` | Server-side access to resources hidden from anonymous users (e.g. unpublished pages). | Run `pnpm preflight` to see what's currently on. The full list of variables is in [Environment Variables](/Quickstart/environment-variables). ## Day-to-day workflow ```bash pnpm dev # the storefront pnpm dev # any app under apps/, generated ones included pnpm codegen # regenerate GraphQL types after editing .graphql files pnpm test # unit tests (Vitest) pnpm test:e2e # end-to-end (CodeceptJS) pnpm format # Prettier pnpm format:check # what CI checks ``` ### Branches and commits - **Trunk:** `main` is the only long-lived branch and is always expected to be releasable. - **Change branches:** create one from the latest `main`, keep it focused, and aim to merge it within two working days. External contributors use the same flow from a fork. - **Incomplete work:** split larger changes into releasable slices. Use a short-lived feature flag or branch-by-abstraction seam when incomplete behavior must be merged, and remove the flag after rollout. - **Commits and PRs:** use a [Conventional Commit](https://www.conventionalcommits.org/) PR title because the squash title drives semantic-release. Husky runs `lint-staged` on commit, so formatting is automatic. Run `pnpm exec cz` for a guided prompt. ### Before opening a PR ```bash pnpm format:check pnpm test ``` CI repeats both. Make sure they're green locally first. ## Conventions to know A few minutes here saves a round of review comments. - **`Result` for expected failures.** Operations that can fail return a `Result` from `@nimara/domain/objects/Result` instead of throwing. Throw only for truly unexpected errors. - **Named exports only.** Default exports are off by ESLint (Next.js route/page/layout files are exempt). - **Type-only imports.** Write `import { type X }` when only types are needed. - **Server Components by default.** Add `"use client"` only when you really need it. Mutations go through Server Actions. Parallelize independent fetches with `Promise.all`. - **Forms.** `react-hook-form` + `zod`. Validate env vars with Zod too — never read server secrets from client code. - **i18n.** Translations live in `@nimara/i18n` (`packages/i18n/src/messages/{locale}/`), composed by app via `createRequestConfig`. See `packages/i18n/AGENTS.md` to add a locale. - **Dependencies.** Don't add one silently — propose it (why, alternatives, which package). ## Make your first change ### Pick the right layer - Pure rule or type → `domain` - Hook, helper, utility → `foundation` - External API call → `infrastructure` - Visual primitive (no logic) → `ui` - A full feature (UI + state + logic) → `features` - App-only glue → in the app ### Branch, build, test ```bash git checkout -b contrib/my-first-change pnpm codegen # only if .graphql files changed pnpm test pnpm format ``` ### Open a PR Push, open a PR against `main`, give it a Conventional Commit title, and include testing evidence. `Linters & Tests` and all four Vercel project statuses (`nimara-docs`, `nimara-ecommerce`, `nimara-ecommerce-stripe`, and `nimara-marketplace`) must pass before the PR can be squash-merged. A maintainer takes it from there. ## Where to go next - **[Running Locally](/Quickstart/running-locally)** — backend setup options. - **[Storefront](/Quickstart/storefront)** — full setup, webhooks, deployment. - **[Environment Variables](/Quickstart/environment-variables)** — every configurable variable. - **[Saleor CMS](/Quickstart/saleor-cms)** & **[Add a new channel](/Quickstart/add-new-channel)** — content and markets. - **[Stripe](/Integrations/stripe-integration)**, **[UCP](/Integrations/ucp-integration)**, **[Marketplace](/Advanced/marketplace)** — integrations and advanced apps. - `AGENTS.md` and `CONTRIBUTING.md` in the repo root — deeper conventions and the contribution process. --- --- id: release-workflow title: Daily Workflow & Releasing --- # Daily Workflow & Releasing Nimara uses trunk-based development. `main` is the only long-lived branch, the source of releases, and the Vercel production branch. Every commit on `main` must be safe to release. ## Daily development Start each change from the latest `main`: ```bash git switch main git pull --ff-only origin main git switch -c feat/my-new-feature ``` Keep the branch focused on one releasable change and aim to merge it within two working days. Several pull requests can serve one issue or story. Do not use a shared feature branch as an integration branch. For work that cannot be completed in that window: - Split it into backward-compatible slices. - Hide incomplete behavior behind a short-lived, default-off feature flag. - Use branch by abstraction for a longer replacement or migration. - Give every flag an owner and removal condition, and test each meaningful state. ## Pull requests Open every pull request against `main` and use a Conventional Commit title. Nimara squash-merges pull requests, so this title becomes the commit semantic-release evaluates. Before merging: - Obtain the required approval and resolve review conversations. - Pass `Linters & Tests` and the four Vercel project statuses: `Vercel – nimara-docs`, `Vercel – nimara-ecommerce`, `Vercel – nimara-ecommerce-stripe`, and `Vercel – nimara-marketplace`. - Bring the branch up to date with `main`. - Complete risk-appropriate testing against the preview deployment. GitHub squash-merges the change and deletes the branch. Direct pushes to `main` have no bypass. During an active incident, only a member of the GitHub `Admins` team may use the ruleset's pull-request-only bypass. Keep the change in a PR, use a squash merge, and record the actor, reason, exact SHA, skipped requirements, and recovery owner in the incident record. ## Additional QA Vercel preview deployments replace branch-based development environments. For high-risk changes, use the **QA Deploy** workflow to deploy the exact commit SHA to `qa-1` or `qa-2`, then record that SHA with the test evidence. The workflow defaults to `main` for convenience and always reports the resolved deployed SHA. The QA environment is an additional validation surface, not a release branch or a place to accumulate changes. ## Releasing After a `main` push passes the full CI workflow: - The Release workflow checks out that exact successful commit and runs semantic-release. - Semantic-release determines whether the Conventional Commit history requires a version, tag, and GitHub release. - Vercel independently builds each affected application from `main` and updates its production domain after a successful build. - Documentation is published from the same commit after the Release workflow succeeds. There is no release branch and no later promotion merge. A `main` commit that does not require a new semantic version can still deploy. ## Production recovery For an application regression: 1. Restore or promote the previous known-good Vercel deployment. 2. Open a revert or fix-forward pull request against `main`. 3. Run the normal required checks and release a new immutable version when applicable. Never force-push `main`, move a published tag, or treat a deployment rollback as a rollback of database migrations, Saleor changes, Stripe actions, provider data, or environment changes. Those stateful changes require their own approved compensating procedure. --- --- id: running-locally title: Running Locally --- # Running Locally To run a local development environment, you need to set up the Saleor instance first. You can use a free developer account at [Saleor Cloud](https://docs.saleor.io/cloud/overview) to start quickly with the backend. Alternatively, you can run [Saleor locally](https://docs.saleor.io/quickstart/running-locally) using Docker. --- --- id: storefront title: Storefront --- # Storefront This guide will help you set up a [Nimara storefront](https://www.demo.nimara.store) in your local environment. ### Fork and clone project First, **fork** the Nimara repository to your own GitHub account. This allows you to make changes independently and submit pull requests later. Go to the [Nimara GitHub repo](https://github.com/mirumee/nimara-ecommerce). Click the **Fork** button in the top-right corner. Once the fork is created, clone your fork locally: ```bash git clone https://github.com/{your_github_username}/nimara-ecommerce.git nimara-ecommerce cd nimara-ecommerce ``` ### Install project's dependencies ```bash pnpm install ``` ### Set up environment variables The recommended way is to run the interactive preflight wizard from the repo root. It lets you pick providers (Search, CMS) and optional features (auth, Stripe, marketplace, Sentry), shows exactly which env vars each choice requires, and writes the env file for you (defaults to `apps/storefront/.env.local`): ```bash pnpm preflight ``` Alternatively, copy **.env.example** to **.env** by hand and fill it in yourself (the file lives in `apps/storefront`, so run this from the repo root): ```bash cp apps/storefront/.env.example apps/storefront/.env ``` ### Add backend URL Use a free developer account at Saleor Cloud to start quickly with the backend. Alternatively, you can run Saleor locally using Docker. ```properties # Add backend address NEXT_PUBLIC_SALEOR_API_URL=https://{your_domain}.saleor.cloud/graphql/ # Local example # NEXT_PUBLIC_SALEOR_API_URL=http://localhost:8000/graphql/ ``` For a full list of required and optional variables, see the [Environment Variables Guide](./environment-variables). ### Set up webhooks in Saleor See the [Webhooks](#webhooks) section below. ### Run project Run the development server: ```bash pnpm run dev:storefront ``` ## i18n The storefront uses the shared i18n package and message composition system described in the [i18n architecture docs](/Advanced/i18n). It passes `app: "storefront"` to `@nimara/i18n`'s `createRequestConfig`, so it receives the `common` + `storefront` message bundles (with locale-specific overrides applied where defined). ## Webhooks Webhooks in Saleor let you receive real-time notifications for events like product or page updates, allowing you to instantly reflect changes in your frontend. This keeps your Nimara storefront in sync with Saleor, enabling instant cache updates and smooth integration. ### How to set up webhooks in Saleor dashboard ### Create a new extension for webhooks Go to your Saleor dashboard → **Extensions** → click **Add Extension** → select **Provides details manually** → add name and assign the necessary permissions for your app: **Manage navigation**, **Manage pages**, **Manage products** → click **Save**. ### Create webhooks In the **Webhooks & Events** section click **Create Webhook**. You will need to create four webhooks, one for each of the following. **Product Webhook** - Target URL: `https:///api/webhooks/saleor/products` - Events: select all events related to the `Product` object, except `Export completed`. - Payload's Subscription Query: ```graphql subscription { event { __typename ... on ProductUpdated { product { slug } } ... on ProductDeleted { product { slug } } ... on ProductMetadataUpdated { product { slug } } ... on ProductMediaCreated { productMedia { productId } } ... on ProductMediaUpdated { productMedia { productId } } ... on ProductMediaDeleted { productMedia { productId } } ... on ProductVariantCreated { productVariant { product { slug } } } ... on ProductVariantUpdated { productVariant { product { slug } } } ... on ProductVariantDeleted { productVariant { product { slug } } } ... on ProductVariantBackInStock { productVariant { product { slug } } } ... on ProductVariantOutOfStock { productVariant { product { slug } } } ... on ProductVariantMetadataUpdated { productVariant { product { slug } } } ... on ProductVariantStockUpdated { productVariant { product { slug } } } } } ``` **Menu Webhook** - Target URL: `https:///api/webhooks/saleor/menu` - Events: select all events related to the `Menu` object. - Payload's Subscription Query: ```graphql subscription { event { __typename ... on MenuCreated { menu { slug } } ... on MenuUpdated { menu { slug } } ... on MenuDeleted { menu { slug } } ... on MenuItemCreated { menuItem { menu { slug } } } ... on MenuItemUpdated { menuItem { menu { slug } } } ... on MenuItemDeleted { menuItem { menu { slug } } } } } ``` **Page Webhook** - Target URL: `https:///api/webhooks/saleor/page` - Events: select all events related to the `Page` object. - Payload's Subscription Query: ```graphql subscription { event { __typename ... on PageCreated { page { slug } } ... on PageUpdated { page { slug } } ... on PageDeleted { page { slug } } ... on PageTypeCreated { pageType { slug } } ... on PageTypeUpdated { pageType { slug } } ... on PageTypeDeleted { pageType { slug } } } } ``` **Collection Webhook** - Target URL: `https:///api/webhooks/saleor/collections` - Events: select `Deleted` and `Updated` events related to `Collection` object. - Payload's Subscription Query: ```graphql subscription { event { __typename ... on CollectionUpdated { collection { slug } } ... on CollectionDeleted { collection { slug } } } } ``` ## Deployment ### Connect GitHub Repository Go to your projects on [Vercel](https://vercel.com/) → click **Add New** and select **Project**. Choose your Nimara GitHub repository and click **Import**. ### Set up New Project Select your **Vercel Team**, add **Project Name**, set **Root Directory** to `apps/storefront`: ![New Project on Vercel](../images/storefront/vercel_new_project.png) Set **Build Command** to `turbo run build --filter=storefront` and **Install Command** to `pnpm install`: ![Build and Install commands on Vercel](../images/storefront/vercel_commands.png) Vercel does not use your local **.env** file so you must define all required variables: ![Environment variables on Vercel](../images/storefront/vercel_env_vars.png) :::note See the [Environment Variables section](./environment-variables) for detailed instructions on setting up these variables. ::: ### Deploy Click **Deploy** to deploy your project. ### Verify Deployment & Configure Settings After successful deployment, click **Continue to Dashboard** to manage your deployed Nimara project. :::warning Node.js version 24.x is required. Set the Node.js version in your Vercel project settings. ::: From the **Vercel dashboard**, you can manage and customize your storefront project: - Monitor deployments – view the status of each deployment (production & previews) - Set environment variables – add or update secrets without redeploying locally - Configure custom domains – add or change production and staging domains - Trigger redeployments – manually redeploy if you update envs - Inspect logs – view build and runtime logs for debugging - Manage team access – invite developers with role-based permissions - Enable password protection – secure preview deployments from public access - Enable analytics – track performance and traffic with Vercel Analytics --- --- id: environment-variables title: Environment Variables --- # Environment Variables This page lists and explains all environment variables used by the Nimara storefront. Use this to correctly set up your `.env` file. **Zero-config:** every variable below is optional. With an empty `.env` the storefront boots and renders every page with empty data (no menu, no products, checkout hidden). Set variables only to connect real services. Copy `apps/storefront/.env.example` to `.env` and fill in what you need, then run `pnpm preflight --report` to see which features are currently on/off. --- ## Core #### `NEXT_PUBLIC_ENVIRONMENT` - **Description**: Environment type for error reporting, logging, and demo-data fallbacks. - **Options**: `TEST`, `LOCAL`, `DEVELOPMENT`, `STAGING`, `PRODUCTION` - **Default**: `LOCAL` #### `NEXT_PUBLIC_DEFAULT_CHANNEL` - **Description**: A Saleor channel slug used for catalog and pricing on the storefront. - **How to get it**: Saleor dashboard → **Configuration** → **Channels** → copy the slug of the channel to be used as the default. - **Default**: `default-channel` #### `NEXT_PUBLIC_DEFAULT_EMAIL` - **Description**: Contact email shown in the UI. - **Default**: `contact@mirumee.com` #### `NEXT_PUBLIC_DEFAULT_PAGE_TITLE` - **Description**: Default page title used in metadata. - **Default**: `Nimara Storefront` #### `NEXT_PUBLIC_STOREFRONT_URL` - **Description**: Public URL of your storefront, used for generating absolute URLs in sitemap.xml, OpenGraph, etc. - **Default**: The Vercel branch URL, or `localhost:3000`. - **Example**: `https://my-new-store.com` #### `NEXT_PUBLIC_DEFAULT_IMAGE_FORMAT` - **Description**: Preferred image format served from Saleor. - **Options**: `AVIF`, `WEBP`, `ORIGINAL` - **Default**: `AVIF` --- ## Backend (Saleor) #### `NEXT_PUBLIC_SALEOR_API_URL` - **Description**: URL of the Saleor GraphQL API endpoint. When unset, the storefront runs in zero-config empty mode (no products, no menu). - **How to get it**: Saleor Cloud → **Projects** → select the relevant project → **Environment Details**. - **Note**: Must end with a trailing slash `/graphql/`. - **Example**: `https://your-domain.saleor.cloud/graphql/` #### `SALEOR_APP_TOKEN` - **Description**: Saleor app token (server-only). Needed for authenticated reads, e.g. loading unpublished CMS/vendor pages. Leave empty for anonymous, published content only. - **How to get it**: Saleor dashboard → **Extensions** → click **Add extension** → select **Provide details manually** → add name and assign the necessary permissions for your app: **Handle checkouts** and **Manage Customers** → click **Save**. --- ## Integrations (provider selection) Each capability (search, CMS) is served by one swappable provider, chosen at build time and read server-side only. When Saleor is not configured, search and CMS fall back to the built-in `dummy` provider (sample data) so a fresh clone is populated out of the box — except when `NEXT_PUBLIC_ENVIRONMENT=PRODUCTION`, where they stay empty so demo data never leaks to a live deployment. Per-provider config is namespaced (`SEARCH__*` / `CMS__*`) and validated only when that provider is selected — set only the variables for the provider you picked. ### Search #### `SEARCH_SERVICE` - **Description**: Search provider used by the storefront (server-side only). - **Options**: `saleor`, `algolia`, `dummy` - **Default**: `saleor` #### `SEARCH_ALGOLIA_APP_ID` - **Description**: Algolia application ID. Required if `SEARCH_SERVICE` is set to `algolia`. - **How to get it**: Algolia dashboard → **API Keys** → copy the `Application ID`. - **Example**: `your-algolia-app-id` #### `SEARCH_ALGOLIA_API_KEY` - **Description**: Algolia search API key. Required if `SEARCH_SERVICE` is set to `algolia`. - **How to get it**: Algolia dashboard → **API Keys** → copy the `Search API key`. - **Example**: `your-algolia-search-api-key` #### `SEARCH_ALGOLIA_INDICES` - **Description**: JSON array of index settings (channel, currency, `indexName`, available facets, virtual replicas). Required if `SEARCH_SERVICE` is set to `algolia`; at least one index must be configured. - **Example**: `[{"channel":"default-channel","currency":"USD","indexName":"products","availableFacets":{},"virtualReplicas":[]}]` ### CMS #### `CMS_SERVICE` - **Description**: CMS provider for pages and menus (server-side only). - **Options**: `saleor`, `butter-cms`, `dummy` - **Default**: `saleor` #### `CMS_BUTTER_TOKEN` - **Description**: ButterCMS read token. Required if `CMS_SERVICE` is set to `butter-cms`. - **How to get it**: ButterCMS dashboard → **Settings** → **API Tokens**. --- ## Auth #### `AUTH_SECRET` - **Description**: Secret for session encryption (used by Auth.js). Required for customer login. - **How to get it**: Generate a random secure string, e.g. `openssl rand -base64 32`. #### `AUTH_URL` - **Description**: Auth.js base URL for callback handling. - **Notes**: Required when **not** deploying on Vercel. - **Example**: `https://my-new-store.com` --- ## Payments (Stripe) See the [Nimara Stripe integration section](../Integrations/stripe-integration) for detailed instructions on setting it up. #### `NEXT_PUBLIC_PAYMENT_APP_ID` - **Description**: Identifier of the Saleor payment app used at checkout. The app reports its own publishable key per channel, so the storefront holds no Stripe key. --- ## Marketplace #### `NEXT_PUBLIC_MARKETPLACE_ENABLED` - **Description**: Enable multi-vendor behavior in the storefront (e.g. cart limited to a single vendor, marketplace-specific UI). - **Options**: `true`, `false` - **Default**: `false` #### `NEXT_PUBLIC_MARKETPLACE_VENDOR_URL` - **Description**: Public URL of the marketplace vendor portal, used for footer links. When empty, the marketplace footer column is hidden. - **Example**: `http://localhost:3001` --- ## Monitoring (Sentry) Sentry is only enabled when `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, and `SENTRY_PROJECT` are set. #### `SENTRY_DSN` - **Description**: Sentry project DSN. `NEXT_PUBLIC_SENTRY_DSN` is also supported and takes precedence when both are set. #### `SENTRY_AUTH_TOKEN` - **Description**: Auth token for uploading source maps. #### `SENTRY_ORG` - **Description**: Your Sentry organization name. #### `SENTRY_PROJECT` - **Description**: Your Sentry project name. #### `SENTRY_DEBUG` - **Description**: Enable Sentry debug logging (set to `true` or `false`). --- ## Analytics (Google Tag Manager) #### `NEXT_PUBLIC_GTM_ID` - **Description**: Google Tag Manager container ID. GTM loads only when this is set. Google Consent Mode v2 gates `analytics_storage` on the cookie-banner choice; ad signals stay denied. - **Example**: `GTM-XXXXXXX` #### `NEXT_PUBLIC_GTM_AUTH`, `NEXT_PUBLIC_GTM_PREVIEW` - **Description**: Optional GTM environment parameters for preview/staging containers. Set both together. --- ## Logging #### `LOG_LEVEL` - **Description**: Logging level for application output. Read directly (not part of the validated env schema) and shared across apps. - **Options**: `debug`, `info`, `warn`, `error`, `critical` - **Default**: `debug` --- ## E2E Testing (CodeceptJS) These variables configure the `apps/automated-tests` CodeceptJS suite, not the storefront itself. #### `TEST_ENV_URL` - **Description**: URL used for end-to-end tests. The suite fails to start without it. - **Example**: `https://localhost:3000` #### `LOCALE` - **Description**: Selects the URL prefix and address fixture the tests use. `us` adds no prefix and `gb` adds `/gb`. Defaults to `us`, and any other value fails the run at startup. - **Example**: `us` #### `USER_EMAIL`, `USER_PASSWORD` - **Description**: Credentials for testing login during E2E tests. --- --- id: saleor-cms title: Saleor CMS --- # Add content with Saleor CMS This guide will help you set up Saleor CMS, which manages your storefront's **homepage**, **footer**, and **navigation**. ## Configure homepage Saleor CMS relies on attributes to organize content and specific attributes are required for setting up the homepage. Refer to the screenshots below to identify the necessary attributes. ### Create required attributes Navigate to Saleor dashboard → **Configuration** → and select **Model Attributes**. Create the following attributes: **Carousel products** - display up to 8 products in a carousel on the homepage: ![Carousel products attribute](../images/cms/homepage/carousel-products.png) **Homepage banner button text** - display button text on banner section: ![Banner button text attribute](../images/cms/homepage/banner-button-text.png) **Homepage banner header** - display header on banner section: ![Banner header attribute](../images/cms/homepage/banner-header.png) **Homepage banner image** - display the background image of the banner section: ![Banner image attribute](../images/cms/homepage/banner-image.png) **Homepage button text** - display button text on page bottom: ![Button text attribute](../images/cms/homepage/button-text.png) **Homepage grid item header** - display header on featured carousel's element: ![Grid item header attribute](../images/cms/homepage/grid-item-header.png) **Homepage grid item header font color** - set the font color of the header on featured carousel's element: ![Grid item header font color attribute](../images/cms/homepage/grid-item-header-font-color.png) **Homepage grid item image** - display the background image of the featured carousel's element: ![Grid item image attribute](../images/cms/homepage/grid-item-image.png) **Homepage grid item subheader** - display subheader on featured carousel's element: ![Grid item subheader attribute](../images/cms/homepage/grid-item-subheader.png) **Homepage grid item subheader font color** - set the font color of subheader on featured carousel's element: ![Grid item subheader font color attribute](../images/cms/homepage/grid-item-subheader-font-color.png) ### Create Homepage Model Type To set up the homepage, you need to create a **Model Type** and assign appropriate attributes. #### Navigate to Model Types Go to Saleor dashboard → **Configuration** → **Model Types**. #### Create new Model Type Click on **Create model type**. Enter the name **Homepage** for Content Type Name and click **Save**. #### Assign attributes Assign all required attributes to **Homepage model Type**: ![Homepage model type attribute](../images/cms/homepage/homepage-page-type.png) ### Create an actual homepage Once **Homepage Model Type** is set up, you can create the actual homepage content by following these steps: #### Navigate to Content management Go to Saleor dashboard → **Modeling** and click **Models**. #### Select Model Type In the dropdown, select **Homepage**. This ensures that attributes assigned to **Homepage Model Type** are available for editing. #### Fill in attributes Fill in all the required attribute values for your homepage content. ## Configure navigation & footer ### Navigate to Navigation management Go to Saleor dashboard → **Configuration** → **Miscellaneous** and select **Navigation**. ### Create Menu for navigation and footer Click **Create menu** to open the menu creation form. For the main navigation, name the menu **navbar**. This menu will manage links displayed in your website's header navigation. For the footer, create another menu and name it **footer**. This menu will handle links displayed in the footer section of your website. ![Navigation menus](../images/cms/navigation/navigation.png) ### Create Menu items Select the menu you want to edit and click **Create new item** to open the menu item creation form. Menu items can link to various resources: - **Category** - **Collection** - **Static Page** - link to a page created in the **Content** section - **URL** - URL to an external website or link to an internal page within your storefront ## Configure Static Page A static page in Saleor is used for standalone content, such as "About Us" or "Contact" pages. Unlike the **Homepage** page type, **Static Page** does not require attributes. ### Navigate to Content Management Go to Saleor dashboard → **Configuration** → **Content Management** and select **Page Types**. ### Create new Page Type Click on **Create page type**. Enter the name **Static page** for Content Type Name and click **Save**. ### Enter page details - **Title** - provide a title for the page (e.g., "About Us"). - **Slug** - specify the slug for the page, this will determine its URL (e.g., `/about-us`). - **Content** - use the rich text editor to add your content (e.g., text paragraphs, headings, bullet points or numbered lists, images, hyperlinks). ### Create an actual static page Creating an actual static page follows the same steps as creating a homepage: Go to Saleor dashboard → **Content** → click **Create content** → select **Static page** as the page type → fill in the details → click **Save**. --- --- id: add-new-channel title: Add New Channel --- # Adding a new locale, language, and market in Nimara Nimara storefront currently supports two locales: **en-US** (United States) and **en-GB** (Great Britain). This guide walks you through adding a new locale, language, and market (for example, Spanish/Spain) to your Nimara storefront. ## Architecture Overview Nimara's i18n system is organized across two main layers: - **`@nimara/i18n` package:** Shared i18n configuration, routing, and message loading (locales, locale prefixes, message paths) - **`apps/storefront/src/foundation/regions`:** Storefront-specific market/region configuration (channels, languages, currencies, markets) This separation allows flexibility: you can have multiple locales mapped to the same market, or create region-specific configurations independent of i18n. ### Create a new sales channel in Saleor Go to your Saleor dashboard → **Configuration** → **Channels** → click **Create Channel**. Enter the channel details: - **Name:** e.g., "Spain Channel" - **Slug:** e.g., "channel-es" (used in storefront configuration) - **Default country:** Spain - **Shipping zones:** Assign appropriate zones (e.g., "Europe" for Spain) After creating the channel, assign the products you want to offer in this channel. ### Add the locale to the shared i18n config The i18n package (`packages/i18n/src/config.ts`) centralizes locale configuration that applies across the storefront. Edit `packages/i18n/src/config.ts`: ```ts title="packages/i18n/src/config.ts" /** * List of supported locales. * Format: BCP 47 / IETF language tag (e.g. "en-US", "es-ES") * Combines ISO 639-1 language code with ISO 3166-1 region code. */ export const LOCALES = ["en-US", "en-GB", "es-ES"] as const; /** * Map of locales to their display names. */ export const LOCALE_LABELS = { "en-US": "English (United States)", "en-GB": "English (United Kingdom)", "es-ES": "Español (España)", } as const satisfies Record; /** * Map of locales to their message files. * Each locale must have a corresponding JSON file in src/messages/ */ export const MESSAGES_PATH_MAP = { "en-US": "@nimara/i18n/messages/en-US.json", "en-GB": "@nimara/i18n/messages/en-GB.json", "es-ES": "@nimara/i18n/messages/es-ES.json", } as const satisfies Record; /** * Map of non-default locales to their URL prefixes. * Default locale ("en-US") does not need a prefix. */ export const LOCALE_PREFIXES = { "en-GB": "/gb", "es-ES": "/es", } as const satisfies Record< Exclude, `/${string}` >; ``` :::note `LOCALE_LABELS` and `MESSAGES_PATH_MAP` exist to satisfy the `Record` type — adding your locale to them keeps the type checker happy, but they are **not** what loads translations at runtime. The actual message loading happens in `packages/i18n/src/loadMessages.ts` (see [Register the locale's messages in the loader](#register-the-locales-messages-in-the-loader) below). `LOCALES`, `LOCALE_PREFIXES`, and `DEFAULT_LOCALE` are the config values that routing and middleware read. ::: ### Add translations for the new locale Translations live in **per-locale directories** under `packages/i18n/src/messages/`, each split into three files: - `common.json` — messages shared across all apps - `storefront.json` — storefront-only messages - `marketplace.json` — marketplace-only messages Create a directory for the new locale and add the three files (seed them from the base locale, then translate): ```bash mkdir -p packages/i18n/src/messages/es-ES cp packages/i18n/src/messages/en/common.json packages/i18n/src/messages/es-ES/common.json cp packages/i18n/src/messages/en/storefront.json packages/i18n/src/messages/es-ES/storefront.json cp packages/i18n/src/messages/en/marketplace.json packages/i18n/src/messages/es-ES/marketplace.json ``` Each file mirrors the base structure — top-level namespaces with translation keys. For example, `common.json`: ```json title="packages/i18n/src/messages/es-ES/common.json" { "checkout-errors": { "INSUFFICIENT_STOCK": "El producto no tiene stock suficiente." }, "filters": { "color": "Color" } } ``` ### Register the locale's messages in the loader `packages/i18n/src/loadMessages.ts` wires the message files up with **static imports** — this is what actually loads translations at runtime. Add imports for the new locale and register them in `LOCALE_SHARED` (the `common.json`) and `LOCALE_APP` (the per-app files): ```ts title="packages/i18n/src/loadMessages.ts" // ...existing imports import esSharedCommon from "./messages/es-ES/common.json"; import esMarketplace from "./messages/es-ES/marketplace.json"; import esStorefront from "./messages/es-ES/storefront.json"; const LOCALE_SHARED: Partial> = { "en-GB": enGbSharedCommon as AnyMessages, "es-ES": esSharedCommon as AnyMessages, }; const LOCALE_APP: Partial< Record>> > = { "en-GB": { storefront: enGbStorefront as AnyMessages, marketplace: enGbMarketplace as AnyMessages, }, "es-ES": { storefront: esStorefront as AnyMessages, marketplace: esMarketplace as AnyMessages, }, }; ``` ### Configure the market/region in the storefront Storefront-specific market configuration lives in `apps/storefront/src/foundation/regions/config.ts`. This maps the locale to a Saleor channel, defines language metadata, and market-specific details. Edit `apps/storefront/src/foundation/regions/config.ts`: ```ts title="apps/storefront/src/foundation/regions/config.ts" import { LOCALES } from "@nimara/i18n/config"; import { clientEnvs } from "@/envs/client"; export const CHANNEL = clientEnvs.NEXT_PUBLIC_DEFAULT_CHANNEL; export const LOCALE_CHANNEL_MAP = { "en-GB": "gb", "en-US": "us", "es-ES": "es", // Map the locale to the market ID } as const; export const SUPPORTED_CURRENCIES = ["USD", "GBP", "EUR"] as const; export const SUPPORTED_CHANNELS = [ "default-channel", "channel-us", "channel-uk", "channel-es", ] as const; // Keep this list in sync when a market uses a continent not already listed. // Spain is in Europe, which is already present, so no change is needed here. export const SUPPORTED_CONTINENTS = [ "Asia Pacific", "Europe", "North America", ] as const; export const LANGUAGES = { GB: { id: "gb", name: "English (United Kingdom)", code: "EN_GB", locale: "en-GB", }, US: { id: "us", name: "English (United States)", code: "EN_US", locale: "en-US", }, ES: { id: "es", name: "Español (España)", code: "ES_ES", locale: "es-ES", }, } as const; export const MARKETS = { GB: { id: "gb", name: "United Kingdom", channel: "channel-uk", currency: "GBP", continent: "Europe", countryCode: "GB", defaultLanguage: LANGUAGES.GB, supportedLanguages: [LANGUAGES.GB], }, US: { id: "us", name: "United States of America", channel: "channel-us", currency: "USD", continent: "North America", countryCode: "US", defaultLanguage: LANGUAGES.US, supportedLanguages: [LANGUAGES.US], }, ES: { id: "es", name: "España", channel: "channel-es", currency: "EUR", continent: "Europe", countryCode: "ES", defaultLanguage: LANGUAGES.ES, supportedLanguages: [LANGUAGES.ES], }, } as const; export const REGIONS_CONFIG = { channel: CHANNEL, supportedLocales: LOCALES, languages: LANGUAGES, markets: MARKETS, localeToMarket: LOCALE_CHANNEL_MAP, supportedCurrencies: SUPPORTED_CURRENCIES, } as const; ``` When the storefront loads, it uses the current locale's channel map to fetch products and configurations for that market. ### Test the new locale Start the dev server and test the new locale: ```bash pnpm run dev:storefront ``` Visit: - `http://localhost:3000` - Default locale (en-US) - `http://localhost:3000/gb` - United Kingdom (en-GB) - `http://localhost:3000/es` - Spain (es-ES) Verify: - Correct locale is displayed in headers/footers - Products are from the correct Saleor channel - Prices are in the correct currency - Checkout works with the correct market configuration ## Key Concepts **Locale:** A BCP 47 language tag combining language and region (e.g., "es-ES" = Spanish in Spain). **Market:** Storefront-specific configuration including channel, currency, language metadata, and region details. **Channel:** A Saleor sales channel that groups products, pricing, and regional configuration. **Locale Prefix:** URL path segment for non-default locales (e.g., `/es` for "es-ES"). **LOCALE_CHANNEL_MAP:** Maps each locale to a storefront market ID for routing and configuration. ## Summary Adding a new locale involves: 1. Create channel in Saleor 2. Add locale to `@nimara/i18n/config.ts` (`LOCALES`, `LOCALE_LABELS`, `MESSAGES_PATH_MAP`, `LOCALE_PREFIXES`) 3. Create translation files in the locale directory: `packages/i18n/src/messages/es-ES/{common,storefront,marketplace}.json` 4. Register the messages in `packages/i18n/src/loadMessages.ts` (`LOCALE_SHARED`, `LOCALE_APP`) 5. Map locale to market in storefront: `apps/storefront/src/foundation/regions/config.ts` 6. Test across all locales This modular approach keeps Nimara scalable and international-ready. --- --- id: ucp-integration title: UCP Integration --- # Universal Commerce Protocol (UCP) Nimara includes a built-in implementation of the [Universal Commerce Protocol (UCP)](https://ucp.dev/2026-04-08/specification/overview/), an open standard for agentic commerce. UCP enables AI agents, platforms, and businesses to interact through a shared API for product discovery, cart building, checkout, and order management. Current version: **`2026-04-08`** ## Overview The UCP integration exposes a set of REST API endpoints that allow external platforms and AI agents to: - **Discover** capabilities and payment handlers via a well-known profile - **Search and browse** the product catalog - **Build carts** before purchase intent is established - **Create and manage checkout sessions** with full payment lifecycle - **Retrieve orders** after checkout completion All UCP endpoints are served from the storefront under `/api/ucp/{channelSlug}/`. ## Architecture UCP is implemented across three layers of the Nimara monorepo: | Layer | Location | Responsibility | | -------------- | ------------------------------------------ | ------------------------------- | | Discovery | `apps/storefront/src/app/.well-known/ucp/` | Business profile endpoint | | API Routes | `apps/storefront/src/app/api/ucp/` | REST route handlers | | Infrastructure | `packages/infrastructure/src/ucp/` | Saleor integration, serializers | The infrastructure layer handles all communication with the Saleor backend, translating between Saleor's GraphQL API and UCP's REST/JSON contract. ## Supported Capabilities | Capability | Description | | --------------------------------- | ------------------------------------------------------------- | | `dev.ucp.shopping.checkout` | Create, update, complete, and cancel checkout sessions | | `dev.ucp.shopping.order` | Retrieve order details post-checkout | | `dev.ucp.shopping.discount` | Apply/remove discount codes (extends checkout) | | `dev.ucp.shopping.buyer_consent` | Buyer consent collection (extends checkout) | | `dev.ucp.shopping.fulfillment` | Shipping methods and destination selection (extends checkout) | | `dev.ucp.shopping.cart` | Pre-purchase cart building | | `dev.ucp.shopping.catalog.search` | Free-text product search with filters and pagination | | `dev.ucp.shopping.catalog.lookup` | Batch product/variant lookup by identifier | ## Discovery The discovery profile is available at: ``` GET /.well-known/ucp ``` This endpoint returns the business profile describing supported services, capabilities, and payment handlers. Platforms use this profile for capability negotiation before making API calls. The profile follows the UCP 2026-04-08 format with capabilities as a registry (map of arrays) rather than a flat array. Each capability entry includes `version`, `spec`, and `schema` fields. Extension capabilities (discount, buyer_consent, fulfillment) declare which root capability they extend. ## Capability Negotiation UCP uses profile-based capability negotiation. Platforms include a `UCP-Agent` header pointing to their own discovery profile: ``` UCP-Agent: profile="https://platform.example/.well-known/ucp" ``` When a request arrives, the storefront: 1. Extracts the profile URL from the `UCP-Agent` header 2. Fetches the platform's profile (HTTPS only, no redirects, 3-second timeout) 3. Validates the platform's UCP version matches `2026-04-08` 4. Computes the intersection of business and platform capabilities 5. Selects the highest compatible version for each shared capability 6. Prunes orphaned extension capabilities (e.g., discount without checkout) The resulting negotiated capability set is included in every response under `ucp.capabilities`. ## API Endpoints ### Catalog | Method | Endpoint | Description | | ------ | ------------------ | ------------------------------------ | | POST | `/catalog/search` | Search products by query and filters | | POST | `/catalog/lookup` | Batch lookup products by ID | | POST | `/catalog/product` | Get single product detail | ### Cart | Method | Endpoint | Description | | ------ | -------------------- | -------------------------- | | POST | `/carts` | Create a new cart | | GET | `/carts/{id}` | Get cart by ID | | PUT | `/carts/{id}` | Update cart (full replace) | | POST | `/carts/{id}/cancel` | Cancel a cart | ### Checkout | Method | Endpoint | Description | | ------ | ---------------------------------- | ------------------------- | | POST | `/checkout-sessions` | Create checkout session | | GET | `/checkout-sessions/{id}` | Get checkout session | | PUT | `/checkout-sessions/{id}` | Update checkout session | | POST | `/checkout-sessions/{id}/cancel` | Cancel checkout session | | POST | `/checkout-sessions/{id}/complete` | Complete checkout session | ### Order | Method | Endpoint | Description | | ------ | -------------- | ----------- | | GET | `/orders/{id}` | Get order | ## Response Envelope All UCP responses include a standard metadata envelope: ```json { "ucp": { "version": "2026-04-08", "status": "success", "capabilities": { "dev.ucp.shopping.checkout": [{ "version": "2026-04-08" }] }, "payment_handlers": {} }, "id": "...", "status": "incomplete", "..." } ``` - `ucp.status` is the primary success/error discriminator (`"success"` or `"error"`) - `ucp.capabilities` contains only the capabilities relevant to the current operation - `ucp.payment_handlers` is present on checkout responses, absent on cart and catalog responses ## Error Handling Error responses follow the UCP message format: ```json { "ucp": { "version": "2026-04-08", "status": "error", "capabilities": { "..." } }, "messages": [ { "type": "error", "code": "out_of_stock", "content": "Item is no longer available", "severity": "recoverable" } ], "status": "incomplete", "continue_url": "https://storefront.example.com/checkout" } ``` ### Error Severities | Severity | Meaning | | ----------------------- | ------------------------------------------------------------- | | `recoverable` | Platform can fix by modifying inputs and retrying | | `requires_buyer_input` | Merchant needs info their API cannot collect programmatically | | `requires_buyer_review` | Buyer must authorize before order placement | | `unrecoverable` | No valid resource exists; retry with new resource or inputs | When errors have `requires_buyer_input` or `requires_buyer_review` severity, the checkout status becomes `requires_escalation` and a `continue_url` is provided for handoff to the storefront UI. ## Checkout Status Lifecycle | Status | Meaning | | ---------------------- | -------------------------------------------- | | `incomplete` | Data still being collected | | `ready_for_complete` | All required data present, ready for payment | | `complete_in_progress` | Payment/completion in progress | | `completed` | Order created | | `canceled` | Session cancelled | | `requires_escalation` | Buyer action needed via `continue_url` | ## Configuration The UCP integration uses these environment variables: | Variable | Description | | ----------------------------- | ------------------------------------------------ | | `NEXT_PUBLIC_STOREFRONT_URL` | Base URL for generating `continue_url` and links | | `NEXT_PUBLIC_DEFAULT_CHANNEL` | Default Saleor channel slug for the API endpoint | | `NEXT_PUBLIC_SALEOR_API_URL` | Saleor GraphQL API URL | No additional environment variables are required. The UCP API endpoint is automatically constructed from the storefront URL and default channel. ## Prices and Currency All monetary amounts in UCP are expressed in **ISO 4217 minor units**. For example, `12900` in USD represents $129.00. The `currency` field always accompanies amounts to indicate the denomination. ## Cart-to-Checkout Conversion When the cart capability is negotiated, platforms can convert a cart to a checkout by including `cart_id` in the create-checkout request: ```json { "cart_id": "cart_abc123", "line_items": [] } ``` The business uses the cart contents to initialize the checkout session. If an incomplete checkout already exists for the given `cart_id`, the existing session is returned (idempotent conversion). ## Required Headers All UCP endpoints expect the following headers: | Header | Required | Description | | ----------------- | -------- | ---------------------------------------------------------------------------------------------------- | | `Content-Type` | Yes | Must be `application/json` for all POST/PUT requests | | `UCP-Agent` | Yes | Platform profile URL, e.g. `profile="https://platform.example/.well-known/ucp"` | | `Idempotency-Key` | No | Unique key for safe retries on checkout-session create. Same key + same body returns cached response | | `Request-Id` | No | Correlation ID echoed back in the response for request tracing | | `Authorization` | No | Reserved for future use. Not currently validated (see Known Limitations) | | `Signature` | No | Reserved for future use. Not currently validated (see Known Limitations) | | `Api-Version` | No | Reserved for future use. Version is negotiated via the `UCP-Agent` profile | ## Known Limitations The following limitations apply to the current UCP implementation. These represent intentional trade-offs or items planned for future work. ### Authentication and Authorization UCP endpoints do not currently verify `Authorization` or `Signature` headers. The `UCP-Agent` header validates the platform's _discovery profile_ (capabilities and version), but does not authenticate the caller's identity. Any client with a valid profile URL can operate on any checkout, cart, or order by ID. The AP2 (Agent Payment Protocol 2) mandate verification functions -- `verifyMerchantAuthorizationDummy`, `verifyCheckoutMandateDummy`, and `validateCheckoutTermsDummy` -- are placeholder stubs. They return success unconditionally and do not perform cryptographic verification. ### Idempotency Idempotency is supported on checkout-session creation via the `Idempotency-Key` header. However, the current storage is **in-memory only** with a 24-hour TTL. This is not safe for multi-instance or serverless deployments where requests may be routed to different instances. Cart endpoints do not support idempotency keys. **Recommendation:** Replace the in-memory idempotency storage with Redis or another distributed cache for production deployments. ### Rate Limiting No rate limiting is applied to UCP or discovery endpoints. The profile-fetch step during version negotiation amplifies this concern, as each inbound request triggers an outbound HTTP fetch to the platform's profile URL. ### Input Validation Request bodies are not validated with a schema validation library. Invalid JSON on most endpoints results in an unhandled 500 error rather than a structured 400 response. Exceptions: - `catalog/lookup` validates array length (max 50 items) - `catalog/search` checks for the presence of `query` or `filters` but not their internal structure ### Single-Channel Discovery The discovery profile at `/.well-known/ucp` is statically generated and uses `NEXT_PUBLIC_DEFAULT_CHANNEL` for the endpoint URL. Multi-channel setups require the platform to know channel slugs independently; the discovery profile only advertises one channel. ### Version Negotiation - Version matching is **strict** -- the platform must support exactly `2026-04-08`. Semver-range matching is not implemented. - Profile fetching uses HTTPS only in production. HTTP is allowed only when `NODE_ENV` is `development`. - Profile fetch timeout is 3 seconds. Slow profile hosts may cause request failures. ### Checkout Complete Fallback When `completeCheckoutSession` succeeds but the subsequent checkout re-fetch fails, a fallback session is built with hardcoded `currency: "USD"`, empty line items, and zero totals. The order data is still attached, but the checkout portion of the response is degraded. ### Error Shape Inconsistencies - `checkout-sessions/{id}` PUT with an empty body may return a raw JSON array rather than the standard UCP error envelope. - `catalog/product` returns HTTP 200 with an error payload when the product is not found (per UCP spec convention), rather than HTTP 404. ## Saleor-Specific Behaviors The UCP implementation is backed by Saleor's GraphQL API, which introduces some Saleor-specific behaviors that integrators should be aware of. ### Cart and Checkout Equivalence In Saleor, there is no separate "cart" object. UCP carts are implemented as Saleor checkouts under the hood. This means: - Cart IDs and checkout session IDs follow the same format (Saleor checkout global IDs) - Creating a cart creates a Saleor checkout - Converting a cart to a checkout session reuses the same underlying Saleor checkout ### Checkout Cancellation Saleor does not have a native checkout cancellation API. Cancellation is implemented by: 1. Setting a `ucp.cancelled` metadata key to `"true"` on the checkout 2. Zeroing out all line item quantities to trigger Saleor's built-in checkout expiration The `canceled` status is determined by reading the `ucp.cancelled` metafield. Updates and completions are blocked on cancelled checkouts, returning a `410 Gone` HTTP status. ### Buyer Data Persistence Buyer data (email, name, consent flags) is stored in a `ucp.buyer.json` metadata field on the Saleor checkout. This data is **replaced in full** when the `buyer` field is present in an update request -- it is not merged with existing data. If the `buyer` field is omitted from an update, the existing buyer metadata is preserved unchanged. ### Checkout Handoff (continue_url) When a checkout requires buyer escalation (e.g., for address input or payment), the response includes a `continue_url`. This URL routes through a proxy middleware that: 1. Sets a checkout session cookie with the Saleor checkout ID 2. Redirects the buyer to the storefront checkout page The proxy is activated on requests to `/checkout` that include `checkoutID` and `redirectPath` query parameters. ## AI Agent Skill For AI agent developers, a detailed agent skill file is available at `.claude/skills/ucp-agent/SKILL.md`. This file contains step-by-step conversation flows, request/response examples, and decision logic for building a commerce agent on top of the UCP API. ## Further Reading - [UCP Specification (2026-04-08)](https://ucp.dev/2026-04-08/specification/overview/) - [Checkout Capability](https://ucp.dev/2026-04-08/specification/checkout/) - [Cart Capability](https://ucp.dev/2026-04-08/specification/cart/) - [Catalog Capability](https://ucp.dev/2026-04-08/specification/catalog/) - [Order Capability](https://ucp.dev/2026-04-08/specification/order/) --- --- id: stripe-integration title: Stripe Integration --- # Stripe Integration in Nimara Nimara supports Stripe integration via a Saleor App. The Nimara Stripe App is a serverless **TypeScript**-based integration built with Next.js and intended for deployment on Vercel. It supports: - Stripe Payment Intents - Multi-channel Stripe configuration - Stripe Tax calculation - Webhook setup via Stripe keys ### Prerequisites Before proceeding, ensure you have: - Access to your **Stripe account** (with API keys) - Access to your **Saleor Dashboard** - Access to your **Vercel account** — needed to _deploy_ the app. Running it locally does not require one; see [App Settings Storage](#app-settings-storage). ### Set Environment Variables Add the following variables to your **.env** file in the `apps/stripe` directory. Copy `apps/stripe/.env.example` to `apps/stripe/.env` as a starting point (run this from the repo root): ```bash cp apps/stripe/.env.example apps/stripe/.env ``` :::note Your **Stripe keys are not set here.** They are entered per channel in the app's dashboard UI _after_ installation (see [Install the App in Saleor](#install-the-app-in-saleor)) and persisted through the configured store (see [App Settings Storage](#app-settings-storage)). ::: For local development against your own Saleor Cloud, two variables are enough: ```properties CONFIG_PROVIDER=file ALLOWED_DOMAINS=* ``` **`NEXT_PUBLIC_ENVIRONMENT`** - **Description**: Environment type for error reporting and logging. - **Example**: `LOCAL`, `STAGING`, `PRODUCTION`, etc. **`NEXT_PUBLIC_SALEOR_API_URL`** - **Description**: URL of the Saleor GraphQL API endpoint. - **How to get it**: Saleor Cloud → **Projects** → select the relevant project → **Environment Details**. - **Note**: Must end with a trailing slash `/graphql/`. - **Example**: `https://your-domain.saleor.cloud/graphql/` **`ALLOWED_DOMAINS`** - **Description**: Comma separated list of Saleor domains allowed to install the app. `*` acts as a wildcard, so a whole Saleor Cloud space can be allowed with one entry. While unset, **every** installation is refused. - **Example**: `nimara-*.eu.saleor.cloud,demo.nimara.store`. A bare `*` allows any Saleor and belongs in local development only. **`CONFIG_PROVIDER`** - **Description**: Where the app persists its settings. `file` writes them to a local JSON file; `edge` uses Vercel Edge Config. Defaults to `edge`. - **Example**: `file` **`CONFIG_FILE_PATH`** - **Description**: `file` provider only. Path of the config file, relative to `apps/stripe`. Gitignored, and created owner-readable because it holds Stripe secret keys. - **Default**: `.saleor-app-config.json` **`VERCEL_TEAM_ID`** - **Description**: The unique identifier of your Vercel team account. Required only when `CONFIG_PROVIDER=edge`. - **How to get it**: Vercel Dashboard → **Settings** → **General** → copy the **Team ID**. **`VERCEL_ACCESS_TOKEN`** - **Description**: Personal Vercel API token used to read and write the Edge Config. Required only when `CONFIG_PROVIDER=edge`. - **How to get it**: Vercel Dashboard → click your avatar and select **Account Settings** → select **Tokens** → **Create Token**. **`VERCEL_EDGE_CONFIG_ID`** - **Description**: ID of the Edge Config database where dynamic settings are stored. Required only when `CONFIG_PROVIDER=edge`. - **How to get it**: Vercel Dashboard → open your project → **Storage** → click **Create Database** → select an **Edge Config** → copy its ID. **`CONFIG_KEY`** - **Description**: Key used to identify and store app-specific configuration in Edge Config. `edge` provider only. - **How to get it**: Set this manually. It should be unique and descriptive. - **Example**: `nimara-config` ### App Settings Storage The app stores, per installed Saleor, the Saleor auth token and the per-channel Stripe keys. `CONFIG_PROVIDER` selects where: | Provider | Backing store | Use for | | -------- | ------------------ | -------------------------------------------------------- | | `file` | Local JSON file | Local development. No Vercel account required. | | `edge` | Vercel Edge Config | Any deployment. Default when `CONFIG_PROVIDER` is unset. | :::warning Do not deploy with `CONFIG_PROVIDER=file`. A serverless filesystem is ephemeral and is not shared between instances, so installations would be lost between requests. ::: ### Running the App Locally Saleor pushes the payment webhooks to the app, so it has to be reachable over the public internet — your Saleor Cloud cannot call `localhost:4000`. Start the dev server and expose it with a tunnel: ```bash pnpm dev:stripe # then, in another shell, point any tunnel of your choice # (cloudflared, ngrok, untun, …) at port 4000 ``` Install the app from the tunnel's URL, and keep the tunnel running while you test: Saleor stores the app's URL at installation time, so a new tunnel URL means reinstalling the app. ### Configure the Nimara Storefront To enable checkout, point the storefront at the installed app (see `apps/storefront/.env.example`). In your Nimara storefront, update the **.env** with: ```properties # Must match the app id from the Stripe app's manifest: `.stripe`. NEXT_PUBLIC_PAYMENT_APP_ID=LOCAL.stripe ``` :::note The storefront holds no Stripe key. The app reports the publishable key for the channel being paid in with every payment or card-saving session it opens, so per-channel Stripe accounts work without rebuilding the storefront. ::: :::note The app id is built as `.stripe` (the `stripe` part is the Stripe app's package name). So the `LOCAL` prefix changes with your `NEXT_PUBLIC_ENVIRONMENT` — e.g. `STAGING.stripe`, `PRODUCTION.stripe`. ::: ### Deploy the App Deploy the app to Vercel using the Vercel CLI or dashboard. Once deployed, your app's manifest will be available at: ``` https:///api/saleor/manifest ``` ### Install the App in Saleor You can install the app via the manifest URL: - From the Saleor Dashboard → **Extensions** → click **Add Extension** → select **Install From Manifest**. The app requests `HANDLE_PAYMENTS` and `MANAGE_USERS`. It needs `MANAGE_USERS` for [stored payment methods](https://docs.saleor.io/developer/payments/stored-payments): it reads the customer a payment event belongs to and keeps the Saleor user ↔ Stripe customer mapping in the user's private metadata. ### Provide Stripe Keys Once the app is installed, open it from the Saleor Dashboard and, for each channel, provide your Stripe **secret** (`sk_...`) and **publishable** (`pk_...`) keys. :::note The Stripe webhooks are installed automatically when the keys are saved — you do not need to configure them manually in Stripe. ::: After the keys are saved, make sure the storefront's `NEXT_PUBLIC_PAYMENT_APP_ID` is set (see [Configure the Nimara Storefront](#configure-the-nimara-storefront)) and restart it. :::note For detailed setup instructions, see the official docs: [Nimara TypeScript Stripe App documentation](https://github.com/mirumee/nimara-ecommerce/blob/main/apps/stripe/README.md) ::: --- --- id: marketplace title: Marketplace --- # Marketplace This guide explains how to set up the Nimara marketplace app in a local environment. The marketplace is a vendor portal for products, orders, collections, and vendor pages in a multi-vendor setup powered by Saleor. It runs on port 3001. The marketplace is also a Saleor App. You must install it in Saleor before it works. The installation stores the app auth token that every server-side Saleor call uses. A running dev server alone is not enough. ## Local setup ### Fork and clone project First, **fork** the Nimara repository to your own GitHub account. Go to the [Nimara GitHub repo](https://github.com/mirumee/nimara-ecommerce). Click the **Fork** button in the top-right corner. Once the fork is created, clone your fork locally: ```bash git clone https://github.com/{your_github_username}/nimara-ecommerce.git nimara-ecommerce cd nimara-ecommerce ``` ### Install project's dependencies ```bash pnpm install ``` ### Copy variables The marketplace app has its own environment file. Copy it from **apps/marketplace/.env.example** to **apps/marketplace/.env**: ```bash cp apps/marketplace/.env.example apps/marketplace/.env ``` Three values need your input: `NEXT_PUBLIC_SALEOR_URL`, `ALLOWED_SALEOR_DOMAINS`, and `STRIPE_SECRET_KEY`. The first section of the file holds them. Every other value is a working local default or is optional. ### Add the backend URL Use a free developer account at Saleor Cloud, or run Saleor locally with Docker. The marketplace app reads `NEXT_PUBLIC_SALEOR_URL`. The GraphQL server throws an error at startup if the variable is missing. Set the instance origin, without a path: ```properties NEXT_PUBLIC_SALEOR_URL=https://{your_domain}.saleor.cloud # Local example # NEXT_PUBLIC_SALEOR_URL=http://localhost:8000 ``` `NEXT_PUBLIC_SALEOR_API_URL` is a different variable. The marketplace app does not read it. Code generation reads it from **apps/storefront/.env**. For a full list of variables, see [Environment Variables](#environment-variables) below and the [Environment Variables Guide](../Quickstart/environment-variables). ### Restrict the domains that can install the app `ALLOWED_SALEOR_DOMAINS` holds a comma-separated list of Saleor hosts that can install the app. Set the host of your instance, without a scheme and without a path: ```properties ALLOWED_SALEOR_DOMAINS=nimara-stage.eu.saleor.cloud ``` An unset list accepts an installation request from any Saleor instance. Every deployed environment must set this variable. ### Start the local services The marketplace uses three containers, defined in **apps/marketplace/docker-compose.yml**. Run these commands from the repository root: ```bash pnpm localstack:up # app config store, port 4566 pnpm db-ledger:up # Postgres ledger, port 5434 pnpm mailpit:up # SMTP sink, ports 1025 and 8025 ``` LocalStack holds the app auth token. With the default `MARKETPLACE_APP_CONFIG_PROVIDER=aws`, the token is read from and written to AWS Secrets Manager, which LocalStack emulates. Mailpit receives the emails that the marketplace sends. Open the Mailpit inbox at [http://localhost:8025](http://localhost:8025). ### Apply the ledger schema The Postgres ledger is optional. See [Payouts and ledger](#payouts-and-ledger). If `DATABASE_URL` is set, apply the schema from the repository root: ```bash pnpm migrate:ledger ``` The command creates the ledger, payout, and Stripe settlement tables. ### Run project Run the development server: ```bash pnpm dev:marketplace ``` The marketplace app runs on **port 3001** by default. ### Expose the app with a tunnel Saleor Cloud, and any remote Saleor, must reach the app during installation. It calls the `tokenTargetUrl` from the manifest, so `http://localhost:3001` does not work. Open a tunnel to port `3001`, for example with ngrok. Then set the public origin, so the manifest URLs are reachable: ```properties NEXT_PUBLIC_MARKETPLACE_VENDOR_URL=https://{your-tunnel}.ngrok.app ``` Restart the dev server after the change. Next.js reads `NEXT_PUBLIC_*` variables at startup, and it also derives its `allowedDevOrigins` entry from this value. Without that entry, Next.js blocks its own development resources for the tunnel host and the app stays on a loading spinner. Verify that the tunnel serves the manifest: ```bash curl -o /dev/null -w "%{http_code}\n" https://{your-tunnel}.ngrok.app/api/saleor/manifest ``` A `200` response means that you can install the app. For a Saleor instance on the same host, you can skip the tunnel. ### Install the app in Saleor In the Saleor Dashboard go to **Apps → Install external app** and use the manifest URL: ``` https://{your-tunnel}.ngrok.app/api/saleor/manifest ``` Use `http://localhost:3001/api/saleor/manifest` only if Saleor runs on the same host. The manifest requests nine permissions: `IMPERSONATE_USER`, `MANAGE_PAGES`, `MANAGE_PAGE_TYPES_AND_ATTRIBUTES`, `MANAGE_PRODUCTS`, `MANAGE_USERS`, `MANAGE_ORDERS`, `MANAGE_SHIPPING`, `MANAGE_CHANNELS`, and `HANDLE_PAYMENTS`. Install with an account that holds all of them. You can only grant a permission that you hold yourself. On success the app appears in the Saleor app list as **Marketplace Vendor Panel**, and the auth token is persisted for your Saleor domain. The installation also creates the **Vendor Profile** model, which is a Saleor page type with the slug `vendor-profile` and its attributes. The **Options** tab in the super-admin area repeats this step by hand when a model is missing. ### Set up Stripe Connect The vendor panel requires a connected Stripe Connect account. The authenticated layout redirects every vendor without one to an onboarding screen, so the dashboard stays out of reach until the onboarding completes. Set at least the secret key, then restart the dev server: ```properties STRIPE_SECRET_KEY=sk_test_{your_key} ``` The onboarding screen also offers a manual status sync, so you can complete a local onboarding without the Stripe webhook secrets. Stripe returns the vendor to the value of `NEXT_PUBLIC_MARKETPLACE_VENDOR_URL`, which is the tunnel host, not localhost. ## Features The marketplace app provides the following vendor-facing features: - **Dashboard** - Overview of the vendor's store - **Product Management** - Create, update, and manage products and variants - **Order Management** - View and fulfill customer orders - **Collections** - Organize products into collections - **Drafts** - Manage draft products and pages - **Customer Management** - View and manage customers - **Configuration** - Manage channels, addresses, branding, and general vendor settings The first six features have an entry in the top navigation. Configuration opens from the user menu. A super-admin area at `/app` lets platform operators approve or reject vendors, review the payout overview, and bootstrap the vendor profile model. The manifest mounts it in the Saleor Dashboard catalog navigation as **Marketplace**. Payout data is visible in this area only. The vendor panel has no payouts page. ## Vendor onboarding flow During public vendor sign-up, the marketplace app creates the Saleor vendor profile page and the default vendor collection before it registers the customer account. The vendor page is created as a draft with `isPublished: false`. The app then links the customer account to that page through the `vendor.id` customer metadata key. Saleor sends the account confirmation email, because the sign-up mutation passes a `redirectUrl` that points at the app's `/account-confirm` route. That email does not reach Mailpit. The vendor page is published only after the vendor confirms the Saleor account. When confirmation succeeds and Saleor returns an active user, the marketplace app reads the customer's `vendor.id` metadata and updates the linked vendor page with `isPublished: true`. The marketplace app sends three emails over SMTP: - a new vendor registration notice to `MARKETPLACE_SUPERADMIN_EMAIL`, - a vendor accepted notice, - a vendor rejected notice. A super-admin sets the vendor decision in the `/app` area, which writes the `vendor-status` attribute on the vendor profile page and sends the matching email. The status is informational. It does not block sign-in to the vendor panel. ## Payouts and ledger The marketplace settles vendor earnings through **Stripe Connect**, backed by a **Postgres ledger**. Vendors onboard a Stripe Connect account, Saleor webhooks such as order paid ingest ledger entries, and platform operators close and execute payout batches. A batch creates Stripe **Transfers** to each vendor's connected account. Settlement stops at Transfers. Stripe **Payout** objects, which are bank withdrawals from the Connect account, are not persisted. This subsystem is optional. It activates when `DATABASE_URL` is set. Apply the schema with `pnpm migrate:ledger` from the repository root. Without `DATABASE_URL` the app keeps running and the ledger paths degrade in a defined way: - Stripe Connect onboarding skips the vendor account row, so the vendor panel still works. - The order paid webhook returns a `skipped` status, and it names `DATABASE_URL` as the reason. - The payouts overview endpoint returns `configured: false` with empty lists. - `pnpm migrate:ledger` prints a warning and applies nothing. Payouts therefore need `DATABASE_URL` and the Stripe Connect variables. The rest of the vendor panel does not. ## i18n The marketplace app also uses the shared i18n package described in the [i18n architecture docs](/Advanced/i18n). It passes `app: "marketplace"` to `@nimara/i18n`'s `createRequestConfig`, so it receives the `common` + `marketplace` message bundles, with locale-specific overrides applied where defined. ## Environment Variables Key environment variables for the marketplace. See [apps/marketplace/.env.example](https://github.com/mirumee/nimara-ecommerce/blob/main/apps/marketplace/.env.example) for the full, authoritative list. **Saleor** | Variable | Description | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NEXT_PUBLIC_SALEOR_URL` | Saleor instance URL. Required. The GraphQL server throws an error without it | | `NEXT_PUBLIC_SALEOR_MARKETPLACE_CHANNEL_SLUG` | Channel slug used by the marketplace, for example `default-channel` | | `NEXT_PUBLIC_GRAPHQL_URL` | Marketplace GraphQL endpoint (default: `http://localhost:3001/api/graphql`) | | `NEXT_PUBLIC_MARKETPLACE_STOREFRONT_URL` | Storefront URL the marketplace links to (default: `http://localhost:3000`) | | `NEXT_PUBLIC_MARKETPLACE_VENDOR_URL` | Public origin of this app. Set it to your tunnel URL when you install from a remote Saleor, so the manifest URLs and the Next.js dev-origin allowance are reachable. Leave it unset for pure localhost dev | | `ALLOWED_SALEOR_DOMAINS` | Comma-separated Saleor hosts allowed to install the app. An unset list accepts any domain | **App config** | Variable | Description | | --------------------------------- | --------------------------------------------------------------- | | `MARKETPLACE_NODE_ENV` | `development`, `production`, or `test` (default: `development`) | | `MARKETPLACE_PORT` | Port the app runs on (default: `3001`) | | `MARKETPLACE_BASE_PATH` | Base path the app is served under (default: empty) | | `MARKETPLACE_CORS_ORIGINS` | Comma-separated allowed CORS origins | | `LOG_LEVEL` | `debug`, `info`, `warn`, or `error` (default: `info`) | | `MARKETPLACE_APP_CONFIG_PROVIDER` | App config provider: `aws` or `edge` (default: `aws`) | | `SECRET_MANAGER_APP_CONFIG_PATH` | Secrets Manager path for app config (AWS provider) | | `MARKETPLACE_APP_CONFIG_EDGE_KEY` | Edge Config key for app config (edge provider) | **Ledger (Postgres)** | Variable | Description | | -------------- | -------------------------------------------------------------------------------------- | | `DATABASE_URL` | Postgres connection string for the ledger and payout tables. Required for payouts only | **Stripe Connect (payouts)** | Variable | Description | | -------------------------------------------- | --------------------------------------------------------- | | `STRIPE_SECRET_KEY` | Stripe secret key. Required for vendor onboarding | | `STRIPE_PUBLIC_KEY` | Stripe publishable key | | `STRIPE_WEBHOOK_SIGNING_SECRET` | Signing secret for the marketplace Stripe payment webhook | | `MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET` | Signing secret for Stripe Connect webhooks | | `MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY` | Default country for new Connect accounts (default: `US`) | **Email (SMTP)** | Variable | Description | | ------------------------------ | ---------------------------------------------------------------- | | `MARKETPLACE_SMTP_HOST` | SMTP host used for sending marketplace emails | | `MARKETPLACE_SMTP_PORT` | SMTP port (default: `587`) | | `MARKETPLACE_SMTP_USER` | SMTP username, if your SMTP server requires auth | | `MARKETPLACE_SMTP_PASSWORD` | SMTP password, if your SMTP server requires auth | | `MARKETPLACE_SMTP_SECURE` | Use TLS from the start (default: `false`) | | `MARKETPLACE_EMAIL_FROM` | From address, e.g. `"Nimara Marketplace "` | | `MARKETPLACE_SUPERADMIN_EMAIL` | Email that receives new vendor registration notifications | **AWS (LocalStack for local development)** | Variable | Description | | ----------------------- | ------------------------------------------------------------- | | `AWS_ACCESS_KEY_ID` | AWS access key id (default: `test`, which LocalStack accepts) | | `AWS_SECRET_ACCESS_KEY` | AWS secret access key (default: `test`) | | `AWS_REGION` | AWS region | | `AWS_ENDPOINT_URL` | AWS endpoint, for local development with LocalStack | ## Deployment ### Connect GitHub Repository Go to your projects on [Vercel](https://vercel.com/) → click **Add New** and select **Project**. Choose your Nimara GitHub repository and click **Import**. ### Set up New Project Select your **Vercel Team**, add **Project Name**, set **Root Directory** to `apps/marketplace`. Set **Build Command** to `turbo run build --filter=marketplace` and **Install Command** to `pnpm install`. ### Configure Environment Variables Add the required environment variables in Vercel: - `NEXT_PUBLIC_SALEOR_URL` - `NEXT_PUBLIC_SALEOR_MARKETPLACE_CHANNEL_SLUG` - `NEXT_PUBLIC_MARKETPLACE_STOREFRONT_URL` - `ALLOWED_SALEOR_DOMAINS` (an unset list accepts an installation from any Saleor instance) - `STRIPE_SECRET_KEY` (required for vendor onboarding) - `DATABASE_URL` (required for payouts and ledger) - `STRIPE_WEBHOOK_SIGNING_SECRET` (required for payouts) - `MARKETPLACE_STRIPE_CONNECT_WEBHOOK_SECRET` (required for payouts) - `MARKETPLACE_STRIPE_CONNECT_DEFAULT_COUNTRY` (optional, default `US`) - `MARKETPLACE_SMTP_HOST` (optional, required only to send emails) - `MARKETPLACE_SMTP_PORT` (optional) - `MARKETPLACE_SMTP_USER` (optional) - `MARKETPLACE_SMTP_PASSWORD` (optional) - `MARKETPLACE_SMTP_SECURE` (optional) - `MARKETPLACE_EMAIL_FROM` (optional, required only to send emails) - `MARKETPLACE_SUPERADMIN_EMAIL` (optional, required only to notify admin on registration) See [apps/marketplace/.env.example](https://github.com/mirumee/nimara-ecommerce/blob/main/apps/marketplace/.env.example) for the full list, including app-config and AWS variables. ### Deploy Click **Deploy** to deploy your marketplace app. --- --- id: using-terraform title: Using Terraform --- # Using Terraform Nimara Storefront can be deployed to Vercel using Terraform. This allows you to manage your infrastructure as code which is particularly useful for automating deployments and managing configurations across different environments. ### Prerequisites Before proceeding, ensure you have: - Installed [Terraform](https://developer.hashicorp.com/terraform/install) - Access to the [Vercel account](https://vercel.com/account/settings) - Forked the [Nimara Ecommerce](https://github.com/mirumee/nimara-ecommerce) repository to your GitHub account - Configured [Saleor](https://saleor.io/) instance ### Clone the forked Repository First, clone your forked repository to your local machine: ```bash git clone https://github.com//nimara-ecommerce.git ``` ### Configure Terraform Navigate to the directory where the Terraform configuration files are located: ```bash cd nimara-ecommerce/terraform/storefront ``` Initialize Terraform (this downloads the necessary provider plugins): ```bash terraform init ``` ### Set Up Environment Variables Now it's time to set up the environment variables (both private and public) required for the storefront deployment. Copy the **private** variable file template to a new file named `private.auto.tfvars`: ```bash cp example.private.auto.tfvars private.auto.tfvars ``` :::warning This file will contain sensitive information and should not be committed to version control. Make sure to add it to your `.gitignore` file. ::: Edit the new `private.auto.tfvars` file and add your environment variables, replacing the `CHANGE_ME` placeholders with your actual values. These variables are essential for the storefront to function correctly, including Saleor app token, authentication secret, and Stripe secret key and must be kept private. What they are and how to get them is described in the [Environment Variables](../Quickstart/environment-variables) section. Vercel variables can be obtained from your Vercel account settings. ```hcl title="terraform/storefront/private.auto.tfvars" # Rename this file to `private.auto.tfvars` and fill in the values. # This file cannot be committed to version control as it contains sensitive information. # Can be found in your Vercel team settings - https://vercel.com/YOUR_TEAM_NAME/~/settings. # Look for the `Team ID` section and copy the ID. Example: `team_1234567890abcdef`. # Keep it secure and do not share it publicly. vercel_team_id = "CHANGE_ME" # Can be generated in your Vercel account settings - https://vercel.com/account/settings/tokens # Give it a descriptive name, assign a proper scope and expiration date. # Keep it secure and do not share it publicly. vercel_api_token = "CHANGE_ME" # Map of private environment variables to be set in the Vercel project. Put sensitive information here. private_environment_variables = { "SALEOR_APP_TOKEN": { comment = "Required. Saleor app token for the storefront." sensitive = true envs_values = [ { value = "CHANGE_ME" target = ["production", "preview"] } ] }, "AUTH_SECRET": { comment = "Required. Secret key for authentication." sensitive = true envs_values = [ { value = "CHANGE_ME" target = ["production", "preview"] } ] } } ``` Next, configure **public** environment variables in the same way. Copy the **public** variable file template to a new file named `public.auto.tfvars`: ```bash cp example.public.auto.tfvars public.auto.tfvars ``` Edit the new `public.auto.tfvars` file and fill in the values for the public environment variables: ```hcl title="terraform/storefront/public.auto.tfvars" # Rename this file to `public.auto.tfvars` and fill in the values. # This file can be committed to version control as it should contain only public information. # GitHub repository for the Vercel project. # Format: "owner/repo". Example: "mirumee/nimara-ecommerce". github_repository = "" # Repository branch to deploy to production. If not set, it will default to "main". github_production_branch = "main" # Vercel project name. If not set, it will default to "nimara-ecommerce". vercel_project_name = "nimara-ecommerce" # Optional. List of additional environments with their configurations. additional_environments = [] # Map of public environment variables to be set in the Vercel project. public_environment_variables = { "NEXT_PUBLIC_SALEOR_API_URL" = { comment = "Public Saleor API URL for the storefront" envs_values = [ { value = "https://{YOUR_SALEOR_DOMAIN}/graphql/" target = ["production", "preview", "development"] } ] }, "NEXT_PUBLIC_DEFAULT_CHANNEL" = { comment = "Default channel slug for the storefront" envs_values = [ { value = "CHANGE_ME" target = ["production", "preview", "development"] }] }, "NEXT_PUBLIC_STOREFRONT_URL" = { comment = "Public URL of the storefront. If not set, it will default to the Vercel project domain." envs_values = [ { value = "https://{YOUR_STOREFRONT_DOMAIN}" target = ["production", "preview", "development"] } ] }, "NEXT_PUBLIC_PAYMENT_APP_ID": { comment = "Public ID of the payment app for the storefront" envs_values = [ { value = "CHANGE_ME" target = ["production", "preview", "development"] } ] } } ``` ### Deploy the Storefront After configuring the environment variables, you can deploy the storefront to Vercel using Terraform. First, run the following command to see what changes will be made: ```bash terraform plan ``` If everything looks good, apply the changes to deploy the storefront. This command will prompt you to confirm the changes. Type `yes` to proceed. ```bash terraform apply ``` If you have configured everything correctly, Terraform will create the necessary resources in Vercel and deploy your storefront. Now you can push your changes to the GitHub repository to trigger the deployment on Vercel: ```bash git add . git commit -m "Configure Terraform for storefront deployment" git push origin main ``` --- --- id: i18n slug: /Advanced/i18n title: i18n --- # i18n architecture Nimara uses a shared i18n package with **feature-based message files** and **locale overrides** that are consumed by all apps (storefront, marketplace, stripe, etc.). - Base English messages live under `packages/i18n/src/messages/en/`: - `common.json` – shared namespaces (`common`, `errors`, `form-validation`, `stock-errors`, etc.) - `storefront.json` – storefront-specific namespaces (`home`, `cart`, `checkout`, `auth`, `account`, `order`, etc.) - `marketplace.json` – marketplace-specific namespaces (`marketplace.*`, vendor products, orders, collections, etc.) - Regional overrides live under `packages/i18n/src/messages//` (for example `en-GB`) and only contain keys that differ from base English. This keeps **keys stable** across locales (for example `home.title`, `marketplace.configuration.general.heading`) while allowing regions to override wording where needed. ## Files and namespaces ```mermaid flowchart TD messagesRoot["packages/i18n/src/messages"] --> enDir["en/ (base English)"] messagesRoot --> enGbDir["en-GB/ (overrides)"] enDir --> enCommon["common.json"] enDir --> enStorefront["storefront.json"] enDir --> enMarketplace["marketplace.json"] enCommon --> enCommonKeys["common, errors, form-validation, stock-errors, ..."] enStorefront --> enStorefrontKeys["home, cart, checkout, auth, account, ..."] enMarketplace --> enMarketplaceKeys["marketplace.* (vendor panel)"] enGbDir --> enGbCommon["common.json (overrides)"] enGbDir --> enGbStorefront["storefront.json (overrides)"] enGbDir --> enGbMarketplace["marketplace.json (overrides)"] ``` ## How a messages object is composed Each app passes an `app` identifier into `@nimara/i18n`'s `createRequestConfig`, which calls `loadMessages(locale, app)` to build the final messages object. For example, the storefront uses `app: "storefront"`: ```mermaid flowchart TD baseCommon["en/common.json"] --> mergeBase baseApp["en/storefront.json"] --> mergeBase mergeBase["merge base common + app"] --> mergeLocaleShared localeShared["/common.json (optional overrides)"] --> mergeLocaleShared mergeLocaleShared["apply shared overrides"] --> mergeLocaleApp localeApp["/storefront.json (optional overrides)"] --> mergeLocaleApp mergeLocaleApp["apply storefront overrides"] --> finalMessages["final storefront messages for locale"] ``` The marketplace app works the same way, but uses `app: "marketplace"` and `en/marketplace.json` instead of `en/storefront.json`. Other apps can plug into the same mechanism by: 1. Adding an `.json` base file under `packages/i18n/src/messages/en/`. 2. Optionally adding `/.json` override files where wording differs from base English. 3. Passing `app: ""` into `createRequestConfig` in the app's i18n bootstrap code. --- --- status: superseded review-by: 2026-08-13 superseded-by: "llm-wiki/tech/ADR/ADR-0003 CodeceptJS Is The End-To-End Test Engine.md" --- # Trial CodeceptJS alongside Playwright with a homepage smoke test `apps/automated-tests` carries 5 Playwright specs served by 9 class-based page objects, and the selector/fixture boilerplate is a meaningful share of that code. To find out whether CodeceptJS is worth adopting, we added a working CodeceptJS setup with a single homepage smoke test and left all 5 Playwright specs untouched. This is a time-boxed trial, not a migration: by **2026-08-13** we either migrate the Playwright specs or delete `codecept/`, `codecept.conf.ts`, `steps.d.ts`, and the three devDependencies. Leaving it undecided is the one outcome we explicitly rejected, because maintaining two runners permanently costs more than either runner alone. ## Considered Options - **Minimal working trial, then decide** (chosen). Ships a reversible evaluation and keeps `main` releasable. - **Migrate all 5 specs at once.** Rejected: commits to rewriting 9 page objects before the ergonomics have been felt, and a regression means reverting the whole suite. - **Permanent coexistence**, CodeceptJS for new areas only. Rejected: two runners, two configs, two selector idioms, and two flake surfaces, forever. ## What the trial demonstrates `codecept/homepage_test.ts` covers two scenarios against the storefront homepage — the page loads with its product carousel, and the hero CTA opens the product listing — through the page object in `codecept/pages/homepage.ts`. It **passes against a deployed environment** (2 passed, verified twice for stability), which is the point: the setup is proven end to end, not just proven to load. **There is no like-for-like authoring-cost measurement in this trial, and no such claim should be read into it.** An earlier iteration ported the `logIn` journey scenario-for-scenario and measured 132 → 91 lines (−31%), but that port was removed as unnecessary, so the number is not reproducible from this repository and is recorded here only as a discarded observation. The committed homepage test covers 2 scenarios against Playwright's 6 homepage tests, so comparing their line counts would be measuring different amounts of coverage. Anyone wanting a defensible authoring-cost number must port a journey scenario-for-scenario first. What the trial does establish qualitatively: CodeceptJS needs no `fixtures.ts` registration (one entry in the config's `include` map replaces it), no `Page` constructor plumbing or `readonly Locator` declarations, and no explicit `expect(...).toBeVisible()` calls, since `I.seeElement` auto-waits. Against that, it adds a generated `steps.d.ts` that must be committed and regenerated whenever a page object changes (`pnpm test:codecept:def`). ## Consequences - **It is deliberately not in CI.** `e2e.yaml` is untouched, so the trial never gates a workflow and never adds flake surface to a required check. The files are still covered by the required `Linters & Tests` check via ESLint and Prettier. The cost is that nobody finds out from CI if the trial breaks; it is run manually with `pnpm test:codecept`. - **`playwright` is pinned to an exact `1.59.1`** to match `@playwright/test`. A caret range resolves to 1.62.0, which would install a second `playwright-core` and download a second set of browser binaries in CI. - **The dependency surface is much larger than three packages, and this was accepted knowingly.** The lockfile diff is +1393/−167: 84 genuinely new packages, plus ~53 existing entries re-keyed across the `next` peer graph (storefront, marketplace, stripe, docs). No existing package version regressed, with one exception — `express` gains a second major, `5.2.1`, alongside `4.22.1` in the dev tree. The cause is that `codeceptjs@4.1.0` declares as **hard, non-optional** dependencies: `ai@^6.0.43` with `@ai-sdk/gateway`/`provider`/`provider-utils` (the Vercel AI SDK, for its AI and self-healing features), `@modelcontextprotocol/sdk@^1.26.0` (which pulls `hono`, `@hono/node-server`, `express@5`), `@codeceptjs/configure@^4.0.0-beta.4` (a beta as a hard dependency), the `@cucumber/*` Gherkin stack whether or not BDD is used, plus `axios`, `multer`, `mocha`, `monocart-coverage-reports`, `zod`, and `cheerio`. All of it is dev-only — none reaches a production bundle or the Vercel runtime — and the trial is time-boxed, so the exposure is temporary by construction. Whoever makes the migrate-or-delete call on 2026-08-13 must weigh this cost against a benefit that is, as of now, only qualitative: adopting CodeceptJS permanently means accepting an AI SDK, an MCP server SDK, and a beta package in the repo's dev tree indefinitely. - **`noGlobals: true` is set**, which is the v4 default for new projects and silences a deprecation warning on every run. `Feature`/`Scenario`/`Before` and `inject()` still work unimported, but `within`, `session`, `secret`, `locate`, `actor`, `dataTable`, and the `Helper` base class must be imported if a future test needs them. - **Playwright's `testMatch` is now pinned** to `**/*.spec.ts` so it can never collect the CodeceptJS `*_test.ts` files. - **`.claude/rules/testing.md` says "use Playwright for end-to-end tests", which this trial contradicts.** The rule is intentionally left unchanged — Playwright remains the standard until this ADR is resolved. Whichever way the decision goes on 2026-08-13, that rule must be updated or the trial removed. ## Incidental findings, not addressed here - **`storeHeaders.heroBanner` does not match the deployed demo storefront.** `utils/constants.ts` expects "Power your store with Nimara"; the environment renders "Welcome to Nimara Demo Store". Consequently the existing Playwright test `Homepage › Hero banner elements are present @CI` fails there (verified: 5 passed, 1 failed on `homepage.spec.ts`). The CodeceptJS smoke test deliberately asserts `storeHeaders.productsCarousel` instead, which does match. Whether the constant or the environment content is wrong is a separate question. - **`apps/automated-tests/turbo.json`** declares `test:e2e` inputs of `./tests/e2e/**/*.test.ts` and `playwright.config.json`, neither of which matches reality (`*.spec.ts`, `playwright.config.ts`), so that task's cache keys do not track its inputs. A pre-existing bug, worth its own change.