Architecture overview

Welcome. This page is the single best place to start when you join Mercentia as a developer. It tells you what lives where, why it lives there, and where to look for the next layer of detail.

The 30-second pitch

Mercentia is a 2026-vintage e-commerce platform competing with Shopify, BigCommerce, Wix, and 10Web. Merchants get a hosted storefront, a dashboard, a multi-tenant gateway, payment + tax + fulfilment + multi-channel + loyalty + agentic-commerce features. Developers get a typed SDK, a CLI, a headless starter, an app marketplace, and an MCP server for AI agents.

The repo layout

This is a pnpm workspace monorepo at /home/user/mercentia. Three top-level dirs matter:

apps/
  ├── gateway/          ← Hono-on-Node REST API. The brain.
  ├── worker/           ← BullMQ workers. Async jobs (sync, webhooks, AI builds).
  ├── dashboard/        ← Nuxt 3 merchant dashboard.
  ├── storefront/       ← Nuxt 3 multi-tenant themed storefront.
  ├── docs/             ← Nuxt Content docs site (where this file lives).
  ├── marketplace/      ← App marketplace UI.
  ├── theme-store/      ← Theme browser/installer.
  └── website/          ← Marketing site.

packages/
  ├── database/         ← Drizzle schema + migrations + tenancy helpers.
  ├── shared/           ← Cross-app helpers (env-preflight, queue names, types).
  ├── ai/               ← AI builder + Claude/OpenAI wrappers + typed Zod outputs.
  ├── mcp-server/       ← MCP server for agentic commerce (ChatGPT / Perplexity).
  ├── cli/              ← @mercentia/cli — publishable npm package.
  ├── storefront-sdk/   ← TypeScript SDK for headless builds.
  ├── connectors/       ← Email / SMS / helpdesk adapters.
  ├── *-migrator/       ← Per-platform migration packages
  │                        (shopify, bigcommerce, magento, wix, woo, squarespace).
  ├── *-channel/        ← Per-channel packages (ebay, etc.).
  ├── auth/             ← Better Auth wrapper + session helpers.
  ├── discount-engine/  ← Pure-functional discount applier (BXGY-aware).
  ├── inventory-allocator/, risk-engine/, segments/, ...

storefront-starter/     ← Next.js 14 headless template. Cloned via degit.

Request lifecycle — the merchant path

[Browser]                 [Nuxt Dashboard]            [Gateway (Hono)]            [Worker]
   │                          │                              │                       │
   │  navigate /loyalty       │                              │                       │
   │─────────────────────────▶│                              │                       │
   │                          │ useApi().get('/loyalty')     │                       │
   │                          │─────────────────────────────▶│                       │
   │                          │                              │ Hono router → loyalty-│
   │                          │                              │   admin.ts handler    │
   │                          │                              │ withTenantContext()   │
   │                          │                              │ Drizzle query (RLS    │
   │                          │                              │   enforced)           │
   │                          │                              │ if heavy work:        │
   │                          │                              │ ─enqueueChannelSync()─▶ BullMQ job
   │                          │                              │ JSON response         │
   │                          │◀─────────────────────────────│                       │
   │                          │ render page                  │                       │
   │◀─────────────────────────│                              │                       │

Every dashboard request goes through useApi() → Nitro proxy → gateway. The dashboard’s Nitro server proxies /api/v1/* and /auth/* to the configured gatewayUrl, rewriting Set-Cookie so the session cookie stays first-party.

Request lifecycle — the shopper path

[Shopper]                 [Nuxt Storefront]           [Gateway]                   [Stripe]
   │                          │                              │                       │
   │  GET /products/foo       │                              │                       │
   │─────────────────────────▶│                              │                       │
   │                          │ ssr fetch storefront-api     │                       │
   │                          │─────────────────────────────▶│                       │
   │                          │                              │ product detail JSON   │
   │                          │◀─────────────────────────────│                       │
   │                          │ render PDP + PdpExpressBuy   │                       │
   │◀─────────────────────────│                              │                       │
   │                          │                              │                       │
   │  tap Apple Pay           │                              │                       │
   │─────────────────────────▶ Stripe.js paymentRequest ────────────────────────────▶│
   │                          │ ev.paymentMethod returned    │                       │
   │                          │ POST /checkout/express/wallet│                       │
   │                          │─────────────────────────────▶│                       │
   │                          │                              │ walletExpressCheckout │
   │                          │                              │ create PaymentIntent ▶│
   │                          │                              │◀──────────────────────│
   │                          │ outcome (completed/3DS/fail) │                       │
   │                          │◀─────────────────────────────│                       │

Multi-tenancy

Every table in packages/database/src/schema/ that touches a merchant row carries a store_id (or organisation_id) FK and a Postgres RLS policy that requires current_setting('app.current_store_id') to match.

Application contract: every gateway DB call wraps in either:

  • withTenantContext({ storeId, orgId }, async (tx) => { … }) — sets the GUC for the duration of the transaction.
  • withSystemContext(async () => { … }) — sets a special sentinel for cross-tenant reads (cron jobs, housekeeping).

If you forget the wrapper, RLS filters every row to zero. A apps/worker/src/lib/jobs-tenant-context.guard.test.ts test scans every worker file and flags any handler that imports db but doesn’t call one of the wrappers. Do not bypass this guardrail — it’s saved real RLS bugs in production.

See packages/database/src/rlsManifest.ts for the table-by-table RLS policy registry and packages/database/src/verifyRls.ts for the CI-time verifier.

Boot order

Every long-running app calls runEnvPreflight(role) from @mercentia/shared/env-preflight before binding a port or pulling a job:

  • gateway/src/index.ts:1401runEnvPreflight('gateway')
  • worker/src/worker.tsrunEnvPreflight('worker')

env-preflight.ts enforces three tiers: required (throw + exit 1), required_in_prod (throw in NODE*ENV=production; warn in dev), recommended (warn). It also validates secret length/format — BETTER_AUTH_SECRET ≥ 32 chars, STRIPE_WEBHOOK_SECRET starts with whsec*, ALLOWED_ORIGINSmay not contain\* in production.

When you add a new feature with new env vars: add the spec entry in env-preflight.ts AND a doc-commented section in .env.example.

Queues

Every BullMQ queue has a name in packages/shared/src/jobs/queue-names.ts. Adding a queue:

  1. Add the entry there with a doc-comment.
  2. Write the worker at apps/worker/src/jobs/<name>.ts.
  3. Register the worker in apps/worker/src/worker.ts with concurrency + lockDuration tuned to the upstream rate limit.
  4. Add to the graceful-shutdown list (const allWorkers = [ … ]).
  5. Use withSystemContext or withJobTenantContext for every DB call.

A diff in queue-names.ts without a matching worker fails the build because of the ALL_QUEUE_NAMES consistency check.

Tests

# Gateway
cd apps/gateway && pnpm test

# Worker
cd apps/worker && pnpm test

# CLI
cd packages/cli && pnpm test

# Storefront type-check
cd apps/storefront && pnpm exec vue-tsc --noEmit

# Dashboard type-check
cd apps/dashboard && pnpm exec vue-tsc --noEmit

Lock-step CI workflow lives at .github/workflows/ci.yml. It mirrors what Railway’s nixpacks runs.

Deploy

  • Hosted prod — Railway (gateway + worker + storefront + dashboard each in their own service). railway.toml + nixpacks.toml at the repo root.
  • Edge / KV — Cloudflare (cloudflare/ dir). KV namespace stores theme overrides and the agent-feed CDN payload.
  • Migrationspackages/database/scripts/migrate.ts runs the SQL files in packages/database/drizzle/ in numerical order. Idempotent — re-running is safe.

The “what changed in 2026” cheat sheet

If you’re inheriting this codebase mid-2026, here’s the order of recent commits worth catching up on:

  1. 2026-05 audit (commit f02b601) — set the 8.4/10 baseline.
  2. Channel sync wiring + loyalty depth + N+1 fixes + CLI publishable (commit 37499a6) — closed Tier 1 + 3 + 5 of the post-audit punch list. loyalty.md, amazon-sp-api.md, channel-sync-architecture.md.
  3. Amazon dashboard wiring fix (commit ec8b83d) — added /channels/amazon to the sidebar + channels grid.
  4. Cart express buy + 20 payment methods + CLI release CI (commit f37cd05) — closed items 2 + 3 + 5 of the post-audit follow-up. express-checkout-wallet.md, payment-methods.md, cli.md (cli-release.yml workflow).
  5. Dashboard wiring for express + methods + docs — current commit. New pages: /settings/payments/express, /settings/payments/methods. Docs: this overview + sibling pages.

Where to ask the next question

You want to … Read
Understand the API surface getting-started.md
Wire payment providers + methods payment-methods.md
Add a wallet (Apple Pay / Google Pay) express-checkout-wallet.md
Integrate Amazon Seller Central amazon-sp-api.md
Add a new sales channel channel-sync-architecture.md
Configure loyalty + analytics loyalty.md
Build a headless storefront headless-starter.md
Use the CLI cli.md
Submit to the app marketplace app-marketplace.md
Wire server-side GA4 ga4-server-side.md
Migrate a merchant from a competitor magento-migration.md, wix-migration.md
Run the platform-admin panel platform-admin.md
Build a marketplace app (developer hub) developer-hub.md
Understand settings IA settings-ia.md

If something isn’t documented, flag it before you write code against it. The team commits to keeping these docs in sync with the codebase; if a doc is stale, that’s a bug worth reporting.

Conventions

  • Comments: WHY only, never WHAT. If the code is obvious, no comment. If a future reader will be confused, one short comment is enough.
  • Tests: every new feature ships with at least one test. Pre-existing test failures are diagnosed (see the audit for the 8/9 we already know about) and tracked, not ignored.
  • Errors: validate at boundaries (user input, external API), trust internal calls. Don’t catch what you can’t meaningfully handle.
  • Migrations: never edit a published migration. Add a new one.
  • Secrets: never in code. Always via process.env + env-preflight entry + .env.example doc.

Welcome aboard.