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:1401→runEnvPreflight('gateway')worker/src/worker.ts→runEnvPreflight('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:
- Add the entry there with a doc-comment.
- Write the worker at
apps/worker/src/jobs/<name>.ts. - Register the worker in
apps/worker/src/worker.tswith concurrency + lockDuration tuned to the upstream rate limit. - Add to the graceful-shutdown list (
const allWorkers = [ … ]). - Use
withSystemContextorwithJobTenantContextfor 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.tomlat the repo root. - Edge / KV — Cloudflare (
cloudflare/dir). KV namespace stores theme overrides and the agent-feed CDN payload. - Migrations —
packages/database/scripts/migrate.tsruns the SQL files inpackages/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:
- 2026-05 audit (commit
f02b601) — set the 8.4/10 baseline. - 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. - Amazon dashboard wiring fix (commit
ec8b83d) — added/channels/amazonto the sidebar + channels grid. - 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.ymlworkflow). - 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-preflightentry +.env.exampledoc.
Welcome aboard.