Development environment
This page is for changing the engine. If you want a running store on a laptop, run the installer under its local profile instead: Install locally. The two coexist; the installer keeps its own environment files under deploy/local/ and never touches the .env a checkout uses.
Prerequisites
Section titled “Prerequisites”- Node 22 (
.nvmrc) and npm.npm installat the root installs every workspace dependency; there is no install step per app. - Docker with Compose, for Postgres and Redis.
- OpenSSL on the path, to generate the session secret.
- Playwright’s browsers, installed on first use of an e2e suite.
After npm install, run npm run prisma:generate once. Nothing runs it for you: package.json has no postinstall hook.
The docker services
Section titled “The docker services”npm run docker:up # docker-compose up -dnpm run docker:downdocker-compose.yml starts four containers: postgres-dev on 55432 (database merchants_engine_dev), postgres-test on 55433 (merchants_engine_test), redis-dev on 56379 and redis-test on 56380. Each has a health check and a named volume, so data survives a restart. docker-compose down -v wipes them.
The environment file
Section titled “The environment file”cp .env.example .env.env.example is written for exactly this setup: the dev database and Redis, CORS_ORIGIN listing the four local origins (53200, 53211, 53300, 53311), the three public base URLs on localhost, MAIL_TRANSPORT=console and STORAGE_DRIVER=local. Three edits:
BETTER_AUTH_SECRET: replace the placeholder withopenssl rand -hex 32. The API refuses to boot in production without one; in development it runs but every session is signed with a known string.API_GLOBAL_PREFIX=api: add this line..env.exampledoes not set it andapps/api/src/main.tsdefaults it to empty, butapps/admin/proxy.conf.jsonforwards/api/*to the API without rewriting the path, and the storefront’s Vite proxy inapps/storefront/vite.config.tsprefixes/apitoo. Without the prefix every admin call answers 404.- Leave
NODE_ENV=development. Onlydevelopmentandtestunlock development behaviour; any other value, or none, reads as production (libs/shared/common/src/env/runtime-environment.ts).
Database commands
Section titled “Database commands”All of these are package.json scripts and run against DATABASE_URL from .env, which prisma.config.ts loads.
npm run prisma:generate: regenerate the Prisma client. Needed after every change toprisma/schema.prisma.npm run db:push:prisma db push, thennpm run db:fts, then the order-status-history backfill. Use this, notprisma db pushdirectly (see the traps below).npm run db:setup: the first-time shape of a database.prisma db push --accept-data-loss, the search setup, the notification-log indexes, the order-status backfill andprisma generate.npm run db:fts: appliesprisma/search_fts_setup.sqlthroughprisma/apply-fts-setup.ts: thesearchVectorcolumn onProduct, its trigger, the trigram indexes and a backfill. Idempotent, and it raises if any active product is left without a vector.npm run prisma:migrate(prisma migrate dev) andnpm run db:reset(prisma migrate reset) exist inpackage.jsonbut cannot run on this repository: the folders underprisma/migrations/do not start from an empty database, so the shadow replay fails withP3006before writing anything. The schema is applied withdb:pusheverywhere and a migration folder is written by hand as a record; see Add a model. To start over, drop and recreate the database and runnpm run db:setup.npm run db:seed:prisma db seed, whichprisma.config.tsmaps tonpx ts-node prisma/seed.ts.npm run db:seed:fixture-catalogue: the storefront fixture catalogue on its own,prisma/seeds/fixture-catalogue/cli.ts.npm run db:flush-cache: drops the Redis money-format cache after a currency or precision change.
Seeds and store fixtures
Section titled “Seeds and store fixtures”prisma/seed.ts is one idempotent script that upserts roles and their permissions from the catalogue, four staff accounts (superadmin@merchants.test with the password Admin123! is the one the admin e2e harness signs in with), a demo catalogue with orders and reviews, the notification templates, the payment methods, and shipping and tax from the store’s configuration. It is destructive on purpose: it hard-deletes every order, review, user and gift card it did not seed, so the database returns to a known baseline. It therefore refuses to run on any database other than merchants_engine_test unless SEED_DEV_OK=1 is set:
SEED_DEV_OK=1 npm run db:seedWhich store the seed shapes comes from STORE_CONFIG_FIXTURE, one of the three fictional configurations under test/fixtures/store-configs/ (test/fixtures/store-configs/README.md describes them):
demo:endefault plusfr, both left-to-right, a two-decimal currency with the symbol after the number, prices shown tax-included. The baseline every visual-regression snapshot was recorded under.gulf-rtl:ardefault plusen,arright-to-left, the Arabic-capable font kit, five percent VAT, a Gulf address shape with no postcode.maghreb-ltr:frdefault plusen, the Latin font kit, a three-decimal currency with a narrow no-break space as thousands separator, nineteen percent VAT, prices shown tax-excluded.
STORE_CONFIG_FIXTURE=gulf-rtl SEED_DEV_OK=1 npm run db:seed seeds a right-to-left store; with the variable unset the seed keeps whatever configuration the database already holds, or the neutral one on an empty database. FIXTURE_CATALOGUE_SEED=1 adds the storefront e2e catalogue (fx- slugs, about thirty-six products in five categories, prisma/seeds/fixture-catalogue/README.md) at the end of the same run. The fixtures carry no real store, and npm run verify:no-client-refs scans the tree to keep it that way.
prisma/seed-roles-only.ts creates the four roles with their grants and nothing else: no users, no catalogue, no demo data. It is what a new production database gets.
Running the applications
Section titled “Running the applications”Three terminals:
npx nx serve api # http://localhost:53000, routes under /api/v1, Swagger at /api/docsnpx nx serve admin # http://localhost:53200, /api proxied to 53000npx nx serve storefront # http://localhost:53300, /v1 proxied to 53000/api/v1npm run start:dev is the same as npx nx serve api. The API logs JSON; pipe it through npx pino-pretty to read it. The storefront reads the store’s identity, brand, locales and money from GET /api/v1/store/config at request time, so the API must be up before the storefront renders anything.
The test stack
Section titled “The test stack”Tests never touch the dev pair. The backend e2e suite reads DATABASE_TEST_URL and REDIS_TEST_PORT (test/e2e/env.ts); both Playwright suites fall back to the same 55433 and 56380 values when the variables are unset. Every harness spawns its own API on 53001 with NODE_ENV=test and reseeds merchants_engine_test in its global setup, so a run starts from the same baseline every time.
Before a single spec runs, each harness calls assertTestTarget from scripts/e2e-identity-guard.ts, which asks the API on the test port GET /v1/health/identity and aborts unless it answers env: "test" with a database name ending in _test. /health alone cannot tell a dev API from a test one, which is why the identity route exists (apps/api/src/health/health.controller.ts).
The three e2e harnesses
Section titled “The three e2e harnesses”- Backend, Supertest:
npm run test:e2e, which isnx e2e api --forceExitwithtest/jest.e2e.config.ts.test/e2e-global-setup.tspushes the schema to the test database, reapplies the search setup, seeds, flushes the test Redis, then runs the built bundledist/apps/api/main.json 53001 with an emptyAPI_GLOBAL_PREFIX;npx nx build apifirst. Specs are HTTP clients undertest/e2e/; Jest never imports the Nest app. - Admin, Playwright:
npx nx e2e admin-e2e,apps/admin-e2e/playwright.config.ts.apps/admin-e2e/global-setup.tsstarts the two test containers, drops and recreates the schema, seeds, builds the API, runs it on 53001 under theapiprefix, signs in as the superadmin and saves the session cookie for every spec. The admin dev server runs on 53211 throughapps/admin-e2e/serve-admin-e2e.mjs, which writes a proxy config pointing at 53001. Specs live inapps/admin-e2e/src/modules/as*.journey.spec.tsand hit the real API; no route mocking. - Storefront, Playwright:
npx nx e2e storefront-e2e,apps/storefront-e2e/playwright.config.ts. Playwright itself spawns two servers:nx serve apion 53001 with theapiprefix against the test database, and the storefront dev server on 53311 pointed at it.apps/storefront-e2e/src/global-setup.tsseeds throughscripts/seed-storefront-test.ts(the baseline seed plus the fixture catalogue), flushes the test Redis, runs the identity guard and checks that the sitemap the API emits names this run’s host.
All three read STORE_CONFIG_FIXTURE and default to demo. Set it to gulf-rtl or maghreb-ltr to run a suite under another store.
The one hard rule
Section titled “The one hard rule”Never run two harnesses at the same time. All three reseed the same merchants_engine_test on 55433 and all three want port 53001 for an API they configure differently (the backend suite runs it unprefixed, the other two under /api). A second harness starting mid-run reseeds the database the first is asserting on, and adopts or collides with the first one’s API. Every harness’s port guard names “a stopped storefront-e2e run” or “another harness” as the usual squatter, because that is what it was.
Known traps
Section titled “Known traps”- A stale listener on 53001 fails every request.
/healthsits outside the global prefix, so a leftover API answers it whatever prefix and database it was started with.test/e2e-global-setup.tstherefore probes a real route (isOurApi) and refuses a server that 404s it;apps/admin-e2e/global-setup.tsrefuses to spawn onto an occupied port (assertPortFree) after killing its own recorded pid once;apps/storefront-e2e/src/global-setup.tscompares the host in the API’s sitemap with this run’s base URL. When one of them stops you, stop the process it names. On Windows, kill the process tree, not the port. - A bare
prisma db pushbreaks search and every product write. ThesearchVectorcolumn and its trigger live inprisma/search_fts_setup.sql, not inprisma/schema.prisma, so a push drops the column and leaves the triggerproduct_search_vector_trigattached. From then on search returns nothing and every product create or edit fails with a 500.apps/api/src/bootstrap/fts-integrity.tsrefuses to boot a dev or test API in that state and names the repair:npm run db:fts.npm run db:pushchains it for you, and every harness reapplies it after its own push. - The storefront harness must see the test Redis.
apps/storefront-e2e/playwright.config.tspassesREDIS_PORTfrom your shell to the API it spawns and only falls back to 56380 when the shell has none. A shell that exported the dev value (56379) sends the test API’s caches, rate limits and idempotency keys to the dev Redis. Do not exportREDIS_PORTin the shell you run the suite from, or exportREDIS_PORT=56380. - Select storefront specs with
--grep. The Nx Playwright executor exposes Playwright’sgrepoption and atestFilesoption, and passes them through; a bare file path after the project name is not an option it knows.npx nx e2e storefront-e2e --grep=checkoutruns the checkout journey. Playwright’s--projectis exposed too. - The admin button takes a
testIdinput.libs/admin-ui/src/atoms/button/button.component.tsbinds[attr.data-testid]="testId()"on the inner<button>; adata-testidattribute written on<app-button>lands on the wrapper element, which is not what a click targets. PasstestId="save"and select[data-testid="save"]. - The seed refuses the dev database.
prisma/seed.tsexits with a message namingSEED_DEV_OK=1whenDATABASE_URLis not the test database. That is the guard working; set the variable when you mean it. - A Redis cache outlives a reseed. The API caches sections, sitemaps and product lists in Redis. Every harness flushes the test Redis after seeding; on the dev pair a reseed leaves the dev Redis as it was, so a page can show a catalogue that no longer exists until the keys expire or you flush
redis-dev. - Windows can reserve a 5xxxx port. Hyper-V’s dynamic exclusion range sometimes covers 53001 or 53211. The knobs are
E2E_API_PORTandADMIN_DEV_PORTfor the admin suite,STOREFRONT_E2E_API_PORTandSTOREFRONT_DEV_PORTfor the storefront suite. There is deliberately no knob that points a suite at the dev stack.
Unit tests and lint
Section titled “Unit tests and lint”npx nx test api # Jestnpx nx test admin # Jest with jest-axenpx nx test storefront # Vitestnpm test # every project's unit tests, two in parallelnpx nx lint apinpm run test:scripts # the repository scripts, this docs site's guard includedWhat each gate proves and which ones block a release is on Testing and gates.